Post

SwiftUI View Preferences

SwiftUI View Preferences

SwiftUI allow us to pass data from parent to child views via environment. But what if you need to send data from child views back to their parent? This is where SwiftUI preference comes in. preference allow child views to communicate their state or data up the view hierarchy to their parent views.

To create a custom preference, simply define a struct that conforms to the PreferenceKey protocol. It’s also a good practice to create a convenience extension method for your view. This keeps the preference key type hidden and simplifies usage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension View {

    func dismissable(_ dismissable: Bool) -> some View {
        self.preference(key: MyPreferenceKey.self, value: dismissable)
    }
}

fileprivate struct MyPreferenceKey: PreferenceKey {

    static let defaultValue: Bool = true

    static func reduce(value: inout Bool, nextValue: () -> Bool) {
        value = nextValue()
    }
}

Usage Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct MyView: View {

    @State private var isDismissable: Bool = true

    var body: some View {
        VStack {
            if isDismissable {
                Button("dismiss") {
                    // ...
                }
            }

            Text("Child View 1")
                .dismissable(false)

            Text("Child View 2")
        }
        .onPreferenceChange(MyPreferenceKey.self) {
            isDismissable = $0
        }
    }
}

The onPreferenceChange modifier allows you to observe and update a parent view based on the preference value set by child views.

Handling Conflicts

Let’s consider a scenario where multiple child views set conflicting preference values. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct MyView: View {

    @State private var isDismissable: Bool = true

    var body: some View {
        VStack {
            if isDismissable {
                Button("dismiss") {
                    // ...
                }
            }

            Text("Child View 1")
                .dismissable(false)

            Text("Child View 2")
                .dismissable(true)
        }
        .onPreferenceChange(MyPreferenceKey.self) {
            isDismissable = $0
        }
    }
}

If any child view sets dismissable to false, the parent should honor that. In this case, the dismiss button will incorrectly appear because the last child view (Child View 2) overrides the preference set by Child View 1. This happens because the reduce function simply replaces the current value with the next one. To handle conflicts properly, we need to update the reduce function.

1
2
3
4
5
6
7
8
fileprivate struct MyPreferenceKey: PreferenceKey {

    static let defaultValue: Bool = true

    static func reduce(value: inout Bool, nextValue: () -> Bool) {
        value = value && nextValue()
    }
}

With this implementation, the reduce function uses a logical AND operation to combine values. This means if any child view sets dismissable to false, the final value will also be false. This ensures the dismiss button only appears if all child views agree that the view is dismissable.

This post is licensed under CC BY 4.0 by the author.