2

I post the minimum code to reproduce the behavior. Tested on latest macOS and Xcode.

The picker in this example is just a wrapper for the default picker and conforms to Equatable (this is a must to prevent the view from updating when properties doesn't change in the real world view) and categories:

enum Category: Int, Identifiable, CaseIterable {
    case one, two, three
    
    var id: Self { self }
    
    var name: String {
        switch self {
        case .one:
            return "First"
        case .two:
            return "Second"
        case .three:
            return "Third"
        }
    }
}
struct CustomPicker: View, Equatable {
    
    static func == (lhs: CustomPicker, rhs: CustomPicker) -> Bool {
        lhs.selectedCategory == rhs.selectedCategory
    }
    
    @Binding var selectedCategory: Category
    
    var body: some View {
        VStack {
            Picker("Picker", selection: $selectedCategory) {
                ForEach(Category.allCases) { category in
                    Text(category.name)
                }
            }
        }
    }
}

And a simple model to bind to:

final class Model: ObservableObject {
    @Published var selectedCategory: Category = .two
}

Now in ContentView:

struct ContentView: View {
    
    @StateObject private var model = Model()
    @State private var selectedCategory: Category = .two
    
    var body: some View {
        VStack {
            Text("Picker bug")
            HStack {
                CustomPicker(selectedCategory: $selectedCategory)
                CustomPicker(selectedCategory: $model.selectedCategory)
            }
        }
        .padding()
    }
}

Here is the problem. If I bind the CustomPicker to the @Stateproperty it works as expected. However, if bound to the model's @Published property the value doesn't change when interacting with the control. When not using the Equatable conformance it works as expected.

What's more interesting is that if I change the pickerStyle to something else like segmented or inline it does work again.

Any idea why this happens? Probably a bug?

EDIT:

I found a hack/workaround... the thing is if the CustomPicker is inside a regular TabView it works fine.

TabView {
    CustomPicker(selectedCategory: $model.selectedCategory)
     .tabItem {
        Text("Hack")
     }
}

Strange behavior...

8
  • why do you have CustomPicker Equatable? Can you explain If I bind the CustomPicker to the @State property it works as expected. CustomPicker is a View it is not to be assigned to a @State property. Commented Jan 24, 2023 at 8:45
  • This simplified CustomPicker has a @Binding property selectedCategory. In this example I put two of them side by side binding their properties to the parent in two different ways. One being an @State property in the parent and the other being an @Published property en an ObservableObject declared @StateObject in the parent. Commented Jan 24, 2023 at 8:54
  • not sure what motivates you to have CustomPicker Equatable, just remove the Equatable and the static func. Commented Jan 24, 2023 at 8:58
  • For performance reasons I need to do my own diffing instead of SwiftUI's to avoid unnecessary redraws. Commented Jan 24, 2023 at 9:10
  • just give the views an id or something, this Equatable may be intersting but let the system take care of things. Commented Jan 24, 2023 at 9:44

1 Answer 1

3

Since this only happens with one picker type (and then, only on macOS, iOS is fine), it does look like a bug with this specific picker style. If you add extra logging you can see that for other picker styles, it performs more equality checks, perhaps because there has been user interaction and the other options are visible, so different mechanisms are marking the view as dirty.

When the equality check is happening for this type of picker, it has the new value from the binding for both the left and right hand sides. If you move from a binding to passing the whole model as an observed object, then it works (because it ignores equatable at this level, it seems), but since you're interested in minimising redraws, that's probably not a great solution.

Note that according to the documentation you need to wrap views in EquatableView or use the .equatable() modifier to take advantage of using your own diffing. You might be better off working out where the performance problems you're trying to avoid are coming from, and fixing those instead.

Sign up to request clarification or add additional context in comments.

3 Comments

Yes, in cases like this I always put a print statement in the equality function. That's how I know that it is actually called, but both sides have the same value (when shouldn't). In the real app, the picker (well, many of them) are in an inspector (right sidebar) controlling many relevant properties, all of them belonging together. Any change in the inspector makes all subviews to redraw unless I use equatable. When using it, I get 60fps (but this bug). When not, it drops to 30fps.
I updated the question with a new finding. If inside a TabView, it marks the view dirty as expected and triggers the update. Why that could be?
I have absolutely no idea. How strange...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.