How can I read the value of modifier used in child elements in my custom view, I want to achieve the effect like TabView ?
struct TabItem: View {
var body: some View {
TabView {
View1()
.tabItem {
Label("Menu", systemImage: "list.dash")
}
View2()
.tabItem {
Label("Order", systemImage: "square.and.pencil")
}
}
}
}
I want the parent view, i.e. my CustomView, to read whether the child element has a custom modifier, e.g. .inCustomView, and read the content from it and create a button or something else based on it, just like in the case of e.g. TabView
In iOS 18, you can now read ContainerValues
using ForEach(subview:)
or Group(subviews:)
.
First, declare a container value that you want to read like this:
extension ContainerValues {
@Entry var someCustomValue: Int = 0
}
extension View {
func customValue(_ x: Int) -> some View {
containerValue(\.someCustomValue, x)
}
}
You can read this in your container view like this:
struct CustomView<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
Group(subviews: content) { subviews in
// here you can write
let customValueOfFirstView = subviews[0].containerValues.someCustomValue
}
}
}
// usage:
CustomView {
// customValueOfFirstView will be 1
Text("Foo").customValue(1)
}
The rest of the answer is for iOS 17 and below.
You would need to use some underscore-prefixed (meaning they are unstable) APIs to read the views that are in a ViewBuilder
. Then you can read a view trait using your own trait key, again using underscore-prefixed APIs. The trait can store an AnyView
representing the view passed to your custom modifier.
To extracting the views from a ViewBuilder
, you can use the Swift Package View Extractor. You can look at how they do this - it's not a lot of code.
As an example, let's create a VStack
that allows you to add buttons at the bottom of its subviews by using a custom modifier, vstackButtonLabel
. Users can use this to specify the view they want for the button's label.
struct VStackWithButtons<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
VStack {
content()
HStack {
ExtractMulti(content) { views in
// "views" is a RandomAccessCollection
ForEach(views) { view in
// You can also access "view.id" here if needed
if let buttonLabel = view[VStackButtonLabelTrait.self] {
Button {
print("Do something")
} label: {
buttonLabel
}
}
}
}
}
}
}
}
VStackButtonLabelTrait
is a _ViewTraitKey
:
struct VStackButtonLabelTrait: _ViewTraitKey {
static let defaultValue: AnyView? = nil
}
extension View {
func vstackButtonLabel<Content: View>(@ViewBuilder content: () -> Content) -> some View {
_trait(VStackButtonLabelTrait.self, AnyView(content()))
}
}
Example usage:
VStackWithButtons {
Text("This has no buttons")
Text("This has a button")
.vstackButtonLabel {
Label("Foo", systemImage: "globe")
}
Text("This also has a button")
.vstackButtonLabel {
Label("Bar", systemImage: "rectangle")
}
}
Result:
As another example, here is a very simple "tab view" (if you can even call it that) that allows you to select tabs using a picker.
struct ContentView: View {
@State var selectedTab = 0
var body: some View {
CustomTabView(selectedTab: $selectedTab) {
Text("Tab 1")
.id(0)
Text("Tab 2")
.id(1)
.customTabItem {
Label("Foo", systemImage: "globe")
}
Text("Tab 3")
.id(2)
.customTabItem {
Label("Bar", systemImage: "rectangle")
}
}
}
}
struct CustomTabView<Content: View, Selection: Hashable>: View {
@Binding var selectedTab: Selection
@ViewBuilder let content: () -> Content
var body: some View {
VStack {
ExtractMulti(content) { views in
ForEach(views) { view in
if view.id(as: Selection.self) == selectedTab {
view
}
}
}
Picker("Pick", selection: $selectedTab) {
ExtractMulti(content) { views in
ForEach(views) { view in
if let id = view.id(as: Selection.self) {
if let label = view[CustomTabItemTrait.self] {
label.tag(id)
} else {
Text("Unnamed").tag(id)
}
}
}
}
}
}
}
}
extension View {
func customTabItem<Content: View>(@ViewBuilder content: () -> Content) -> some View {
_trait(CustomTabItemTrait.self, AnyView(content()))
}
}
struct CustomTabItemTrait: _ViewTraitKey {
static let defaultValue: AnyView? = nil
}