I would like to make a view that receives heterogeneous Content via a viewBuilder, like normal, and then selectively displays only one of the child views so provided, hiding the rest.
Something like what TabView does, but under my programmatic control, without the actual tab bar, forced background, and fullscreen-grabbing. It should be able to accept content items of any mix of types, as ForEach
output, Group
elements, etc. Just like TabView does, or VStack or HStack or List or any Apple view that accepts many children and treats each differently.
What I have:
But I want just one of those three subviews to be visible (my choice).
Here is the code that I would like to make work, the goal should be clear...
import SwiftUI
struct SwitcherView<Content>: View where Content: View {
let content: () -> Content
// NOT this, yuck:
// let content: [AnyView]
@State var selection: Int = 0 // will be changed by logic not shown
public init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
@MainActor public var body: some View {
VStack {
//// bad approaches:
//// obviously this doesn't work, I wish...
// (Group { content() })[selection]
//// This would work, if Layouts could hide
//// subviews (they can't)
// MyPickOneLayout(selection) { content() }
//// With the [AnyView] above, this "works" but
//// is slow, and awful to use as a client
// content[selection]
//// Would work, if modifyEach existed, and would
//// connect back to this view via Preferences
//// or Environment:
// content().modifyEach( MyHideShowModifier() )
//// Requiring the client to wrap each view in
//// MyHideShowModifier itself might work but is
//// intrusive to the client
//// not what I want:
content()
}
}
}
struct SwitcherView_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text("I would like to show just one of these at a time:")
SwitcherView {
// Note heterogeneous content: each element
// has its own type and state, none of which
// SwitcherView gets to know about explicitly..
// just like TabView!
Text("First Content")
.frame(width: 100, height: 100)
.background(.red)
Button(action: {}, label: {
Text("I'm a yellow button!")
})
.buttonStyle(.borderedProminent)
.tint(.green)
.frame(width: 100, height: 100)
HStack {
Text("Some thing").background(.yellow)
Text("Else").background(.blue)
}
.frame(width: 100, height:100)
.background(.brown)
}
}
}
}
TabView does it, so it's clearly possible! (for Apple.)
Approaches tried so far, as mentioned in the comments above:
- Privately wrap the content in a custom
Layout
:Layout
s can't hide subviews, they must position and show them all - Receive content as an array instead: horribly un-SwiftUI like to use, and forces use of
AnyView
, which is performance poison - Use a special
ViewModifier
to wrap each subview, then use the preferences or environment system to connect back to the SwitcherView for show/hide logic: forces all content's outermostViewModifier
to be this modifier, and shows everything lacking it. oops - List my sub-views explicitly, and attach show/hide logic to each: Bad encapsulation, large view. (but it's what my real app is doing now, and I'd like to improve)
Am I barking up the wrong tree? Where should I be looking?