I have a SwiftUI view that switches between two completely different container layouts:
- a TabView layout (for compact width)
- a sidebar + content layout using HStack (for regular width). I am using a custom sidebar since there is more layout and customization involved)
The container can change dynamically at runtime, e.g. by rotating the device or via a toggle (see. demo code below). The child pages contain their own @StateObject view models.
Important: The different layout are not SwiftUI Layout protocols but Views which not only handle the placement of the different views but also manage when which subview is shown, etc. (e.g. the TabView holds the subviews and show a view when the corresponding tab is selected).
Problem:
if useTabLayout {
TabLayoutContainer(...)
} else {
SidbarLayoutContainer(...)
}
When I switch between the two container layouts the pages are recreated, which means their @StateObject view models are re-initialized and the view state is lost.
What I want:
- PageOne, PageTwo, and PageThree should keep their state
- switching between Tab layout and Sidebar layout should not recreate the pages
- only the outer container should change
Tried so far:
- Using
.id(...)on the pages (e.g.PageOne().id("one")). View Model is still re-created. - Creating page
viewModelin Root-View and injecting it into the pages as@ObservedObject. While this works this requires the Root-View to know about the page views and theire content. This seems not to be a clean solution. - Creating pages only once:
let pageOne = PageOne()andcase .pageOne: return pageOne. This seem to work as well. But are there any downsides of keeping the page alive like this? - Considering
AnyLayout(but it only works for real Layouts likeVStackLayout, not complex containers like TabView)
Question
What is the correct SwiftUI approach for keeping child view state (@StateObject) alive while switching between two completely different container views (TabView vs Sidebar+Content)?
Demo Code
enum MyPage: Hashable {
case pageOne, pageTwo, pageThree
}
struct MyRootView: View {
@State var useTabLayout = true
@State var selectedPage: MyPage = .pageOne
var body: some View {
VStack {
Toggle("TabLayout", isOn: $useTabLayout)
if useTabLayout {
TabLayoutContainer(selectedPage: $selectedPage)
} else {
SidbarLayoutContainer(selectedPage: $selectedPage)
}
}
}
}
struct TabLayoutContainer: View {
@Binding var selectedPage: MyPage
var body: some View {
TabView(selection: $selectedPage) {
PageFactory.view(for: .pageOne)
.tag(MyPage.pageOne)
PageFactory.view(for: .pageTwo)
.tag(MyPage.pageTwo)
PageFactory.view(for: .pageThree)
.tag(MyPage.pageThree)
}
}
}
struct SidbarLayoutContainer: View {
@Binding var selectedPage: MyPage
var body: some View {
HStack {
// Sidebar
VStack {
Button("One") { selectedPage = .pageOne }
Button("Two") { selectedPage = .pageTwo }
Button("Three") { selectedPage = .pageThree }
}
// Content
VStack {
PageFactory.view(for: selectedPage)
}
}
}
}
@ViewBuilder
static func view(for page: MyPage) -> some View {
switch page {
case .pageOne: PageOne()
case .pageTwo: PageTwo()
case .pageThree: PageThree()
}
}
struct PageOne: View {
@StateObject var viewModel: ViewModelOne = .init()
var body: some View {
Text("Page One")
}
class ViewModelOne: ObservableObject {
init() {
print("Initialized ViewModelOne")
}
}
}
.opacity(0)to hide the view that is currently not active?.opacticy(0)solve the described problem? This could only be used within theSidebarLayoutContainerand the views there would still be completly independed from the views within theTabBarLayoutContainerTabBarLayoutContainer(…).opacity(useTabLayout ? 1 : 0). Whats wrong with that?