简体   繁体   中英

SwiftUI ObservedObject causes undesirable visible view updates

I am working on an app that applies a filter to an image. The filter has a number of parameters that the user can modify. I have created an ObservableObject that contain said parameters. Whenever one of the parameters changes, there is a visible update for views, even if the view displays the same value as before. This does not happen when I model the parameters as individual @State variables.

If this is to be expected (after all the observed object does change, so each view depending on it will update), is an ObservedObject the right tool for the job? On the other hand it seems to be very inconvenient to model the parameters as individual @State/@Binding variables, especially if a large number of parameters (eg 10+) need to be passed to multiple subviews!

Hence my question:

Am I using ObservedObject correctly here? Are the visible updates unintended, but acceptable, or is there a better solution to handle this in swiftUI?

Example using @ObservedObject:

import SwiftUI

class Parameters: ObservableObject {
    @Published var pill: String = "red"
    @Published var hand: String = "left"
}

struct ContentView: View {

    @ObservedObject var parameters = Parameters()

    var body: some View {
        VStack {

            // Using the other Picker causes a visual effect here...
            Picker(selection: self.$parameters.pill, label: Text("Which pill?")) {

                Text("red").tag("red")
                Text("blue").tag("blue")

            }.pickerStyle(SegmentedPickerStyle())

            // Using the other Picker causes a visual effect here...
            Picker(selection: self.$parameters.hand, label: Text("Which hand?")) {

                Text("left").tag("left")
                Text("right").tag("right")

            }.pickerStyle(SegmentedPickerStyle())
        }
    }
}

Example using @State variables:

import SwiftUI

struct ContentView: View {

    @State var pill: String = "red"
    @State var hand: String = "left"

    var body: some View {
        VStack {

            Picker(selection: self.$pill, label: Text("Which pill?")) {

                Text("red").tag("red")
                Text("blue").tag("blue")

            }.pickerStyle(SegmentedPickerStyle())

            Picker(selection: self.$hand, label: Text("Which hand?")) {

                Text("left").tag("left")
                Text("right").tag("right")

            }.pickerStyle(SegmentedPickerStyle())
        }
    }
}

Warning : This answer is less than ideal. If the properties of parameters will be updated in another view (eg an extra picker), the picker view will not be updated.

The ContentView should not 'observe' parameters; a change in parameters will cause it to update its content (which is visible in case of the Pickers). To prevent the need for the observed property wrapper, we can provide explicit bindings for parameter's properties instead. It is OK for a subview of ContentView to use @Observed on parameters.

import SwiftUI

class Parameters: ObservableObject {
    @Published var pill: String = "red"
    @Published var hand: String = "left"
}

struct ContentView: View {

    var parameters = Parameters()

    var handBinding: Binding<String> {
        Binding<String>(
            get: { self.parameters.hand },
            set: { self.parameters.hand = $0 }
        )
    }

    var pillBinding: Binding<String> {
        Binding<String>(
            get: { self.parameters.pill },
            set: { self.parameters.pill = $0 }
        )
    }

    var body: some View {
        VStack {

            InfoDisplay(parameters: parameters)

            Picker(selection: self.pillBinding, label: Text("Which pill?")) {
                Text("red").tag("red")
                Text("blue").tag("blue")

            }.pickerStyle(SegmentedPickerStyle())

            Picker(selection: self.handBinding, label: Text("Which hand?")) {
                Text("left" ).tag("left")
                Text("right").tag("right")

            }.pickerStyle(SegmentedPickerStyle())
        }
    }
}

struct InfoDisplay: View {
    @ObservedObject var parameters: Parameters

    var body: some View {
        Text("I took the \(parameters.pill) pill from your \(parameters.hand) hand!")
    }
}

Second attempt

ContentView should not observe parameters (this causes the undesired visible update). The properties of parameters should be ObservableObjects as well to make sure views can update when a specific property changes.

Since Strings are structs they cannot conform to ObservableObject; a small wrapper 'ObservableValue' is necessary.

MyPicker is a small wrapper around Picker to make the view update on changes. The default Picker accepts a binding and thus relies on a view up the hierarchy to perform updates.

This approach feels scalable:

  • There is a single source of truth (parameters in ContentView)
  • Views only update when necessary (no undesired visual effects)

Disadvantages:

  • Seems a lot of boilerplate code for something that feels so trivial it should be provided by the platform (I feel I am missing something)
  • If you add a second MyPicker for the same property, the updates are not instantaneous.
import SwiftUI
import Combine

class ObservableValue<Value: Hashable>: ObservableObject {
    @Published var value: Value

    init(initialValue: Value) {
        value = initialValue
    }
}

struct MyPicker<Value: Hashable, Label: View, Content : View>: View {

    @ObservedObject var object: ObservableValue<Value>
    let content: () -> Content
    let label: Label

    init(object: ObservableValue<Value>,
         label: Label,
         @ViewBuilder _ content: @escaping () -> Content) {
        self.object  = object
        self.label   = label
        self.content = content
    }

    var body: some View {
        Picker(selection: $object.value, label: label, content: content)
            .pickerStyle(SegmentedPickerStyle())
    }
}

class Parameters: ObservableObject {
    var pill = ObservableValue(initialValue: "red" )
    var hand = ObservableValue(initialValue: "left")

    private var subscriber: Any?

    init() {
        subscriber = pill.$value
            .combineLatest(hand.$value)
            .sink { _ in
            self.objectWillChange.send()
        }
    }
}

struct ContentView: View {

    var parameters = Parameters()

    var body: some View {
        VStack {
            InfoDisplay(parameters: parameters)

            MyPicker(object: parameters.pill, label: Text("Which pill?")) {
                Text("red").tag("red")
                Text("blue").tag("blue")
            }

            MyPicker(object: parameters.hand, label: Text("Which hand?")) {
                Text("left").tag("left")
                Text("right").tag("right")
            }
        }
    }
}

struct InfoDisplay: View {
    @ObservedObject var parameters: Parameters

    var body: some View {
        Text("I took the \(parameters.pill.value) pill from your \(parameters.hand.value) hand!")
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM