iosswiftuihierarchysubviewmodifier

SwiftUI apply view modifier to specific subviews in view hierarchy


I'm curious on how does SwiftUI applies some of the available view modifiers like keyboardShortcut, which can be applied to any View but it will only work on the first Button child it finds down the view hierarchy:

The target of a keyboard shortcut is resolved in a leading-to-trailing, depth-first traversal of one or more view hierarchies. [...] If multiple controls are associated with the same shortcut, the first one found is used.

Or same thing with .navigationTitle, which apparently you can place anywhere in the view hierarchy and it will be picked up by the next NavigationView, but how? I mean, how can i do this in SwiftUI by myself?

It would be awesome to just place some random view modifier which does this magic things to some items down the view hierarchy.


Solution

  • Those work using PreferenceKey you can define your own and then listen to changes on the PreferenceKey on the parent view while setting the value on some child view.

    Some very basic example where you can set the title of MyView using a PreferenceKey somewhat similar how navigationTitle works

    struct MyPreferenceKey: PreferenceKey {
        static var defaultValue: String?
    
        static func reduce(value: inout String?, nextValue: () -> String?) {
            value = nextValue()
        }
    }
    
    extension View {
        func myTitle(_ value: String) -> some View {
            preference(key: MyPreferenceKey.self, value: value)
        }
    }
    
    struct ContentView: View {
        var body: some View {
            MyView {
                VStack {
                    Text("Test")
                        .myTitle("My Title Value")
                }
            }
        }
    }
    
    struct MyView<Content>: View where Content: View {
        @State private var myTitle: String?
        @ViewBuilder var content: () -> Content
        
        var body: some View {
            VStack(alignment: .leading) {
                if let myTitle = myTitle {
                    Text(myTitle)
                        .font(.callout)
                        .foregroundColor(.secondary)
                }
                
                content()
            }
            .onPreferenceChange(MyPreferenceKey.self) {
                myTitle = $0
            }
        }
    }