I'd like to create my pet project on SwiftUI for iPhone, iPad and MacOs (Mac Catalyst). My idea to have the next layout on all devices: A top bar (with buttons) above, a bottom tab bar at the bottom, and a Viewport view in the middle, taking all remaining area.
I also would like to get the size of the Viewport and save it in a global variable (ObservableObject): But every time I have some issue on different devices (sometimes the Viewport covers all screen or overlapping with a top bar or with a tab bar.
class GlobalUI: ObservableObject {
@Published var viewportSize: CGSize = .zero // Store available viewport size
}
struct ContentView: View {
@EnvironmentObject var globalUI: GlobalUI
@State var selectedTab: TabSelection = .first
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
// Top Bar
CustomTopBar(selectedTab: selectedTab)
.ignoresSafeArea(edges: .top)
// Main Viewport
Viewport(selectedTab: selectedTab)
.frame(height: geometry.size.height - 100)
// Bottom Tab Bar
CustomTabBar(selectedTab: $selectedTab)
.frame(height: 50)
.padding(.bottom, getSafeAreaBottomInset())
.background(Color("BrandGreen"))
.ignoresSafeArea(edges: .bottom)
}
}
.background(Color.black)
.edgesIgnoringSafeArea(.all)
}
}
// another attempt:
struct ContentView: View {
@EnvironmentObject var globalUI: GlobalUI
@State var selectedTab: TabSelection = .first
func getSafeAreaBottomInset() -> CGFloat {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return 0
}
return windowScene.windows.first?.safeAreaInsets.bottom ?? 0
}
var body: some View {
VStack(spacing: 0) {
CustomTopBar(selectedTab: selectedTab)
.expandViewOutOfSafeArea(.top)
GeometryReader { geometry in
VStack(spacing: 0) {
Viewport(selectedTab: selectedTab)
.onAppear {
let newSize = geometry.size
if newSize.width > 0 && newSize.height > 0 {
globalUI.viewportSize = newSize
print("viewport size updated: \(newSize)")
}
}
.onChange(of: geometry.size) { newSize in
if newSize.width > 0 && newSize.height > 0 {
globalUI.viewportSize = newSize
print("viewport size updated: \(newSize)")
}
}
.frame(maxWidth: .infinity, maxHeight: geometry.size.height - getSafeAreaBottomInset() - 100) // Adjust height to exclude CustomTabBar height
Spacer()
}
}
CustomTabBar(selectedTab: $selectedTab)
.frame(height: 50)
.padding(.bottom, getSafeAreaBottomInset())
.background(Color("BrandGreen")) // Ensures a solid background
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.ignoresSafeArea(edges: [.horizontal, .bottom])
}
}
Instead of forcing the Viewport to adopt the size you give it, just let it expand to use all the space available. This is done by applying a frame with maxWidth: .infinity, maxHeight: .infinity
.
If you need to know the size of the viewport, use .onGeometryChange
to read it. A GeometryReader
is not needed.
Btw, when you apply a background color using background(_:ignoresSafeAreaEdges:)
, the safe area insets are ignored by default. So there is no need to add a modifier to ignore them explicitly.
struct ContentView: View {
@EnvironmentObject var globalUI: GlobalUI
@State var selectedTab: TabSelection = .first
var body: some View {
VStack(spacing: 0) {
// Top Bar
CustomTopBar(selectedTab: selectedTab)
.ignoresSafeArea(edges: .top)
// Main Viewport
Viewport(selectedTab: selectedTab)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { size in
globalUI.viewportSize = size
}
// Bottom Tab Bar
CustomTabBar(selectedTab: $selectedTab)
.frame(height: 50)
.background(Color("BrandGreen"))
}
.background(.black)
}
}