AirXygène
AirXygène

Reputation: 2959

Why is Binding wrapped value not updated?

I have a bug that I could tracked down the the following minimal case:

// This is the Model. Just for the example,, a simple Key-Value pairs list.
struct KVPair {
    var keyValuePairs: [String:String] = [
        "key_one"   : "value_one",
        "key_two"   : "value_two"
    ]
    
    subscript (key: String) -> String {
        get { keyValuePairs[key] ?? "" }
        set { keyValuePairs[key] = newValue }
    }
}


//  This enum list anything that can be selected. For the example, it's either nothing (none)
//  or a key/Value pair, but in the real app, there are many different other cases.
enum SelectableItem: Equatable {
    case none
    case keyValue(key: String, value: Binding<String>)
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch lhs {
        case .none:
            if case .none = rhs {
                return true
            }
        case .keyValue(let lhsKey, let lhsValue):
            if case .keyValue(let rhsKey, let rhsValue) = rhs {
                return (lhsKey == rhsKey) && (lhsValue.wrappedValue == rhsValue.wrappedValue)   //  (1)
            }
        }
        return false
    }
}


//  This view is wrapper that can show any other view as selected. It compares
//  a given item to the selected item, and if they are equal, the view that will
//  actually show the given item is highlighted, and can be tapped to be selected
//  or unselected.
struct SelectableItemView<ViewType: View>: View {

    /// A binding to the current selected Item
    @Binding var selectedItem:  SelectableItem

    /// The selectable item that will be shown in the view
    ///
    /// It can be the selected item (in which case it shall be highlighted), or another one.
    let viewItem:       SelectableItem
    
    /// The builder for the view.
    let content:        () -> ViewType

    var body: some View {
        
        print("-------------------")
        print("Comparing view item: \(viewItem)")
        print("   with selected item: \(selectedItem)\n")
        //  When changing the value of the text field, we can see that the model value is changed, so
        //  the KVPairView is updated with the typed value.
        //  But we can also see that 'selectedItem' wrapped value is not updated, and as consequence:
        //  - the selection highlight disappears, as there is never "(lhsValue.wrappedValue == rhsValue.wrappedValue)" (1)
        //  - if return key is typed, the TextField value goes back to the old value (the one of selectedItem)
        
        return self.content()
            .padding(.horizontal, 5)
            .padding(.vertical, 1)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(self.selectedItem == self.viewItem ? Color.accentColor.opacity(0.3) : Color.clear)
            .clipShape(RoundedRectangle(cornerRadius: 5.0, style: .continuous))
            .onTapGesture {
                self.selectedItem = (self.selectedItem == self.viewItem) ? .none : self.viewItem
            }
    }
}


//  This view is shows the list of model key/value pairs, and allows to select one.
struct KVPairView: View {
    
    @Binding var kvPair:         KVPair
    @Binding var selectedItem:   SelectableItem

    var body: some View {
        VStack {
            ForEach(Array(kvPair.keyValuePairs.keys), id: \.self) { key in
                SelectableItemView(selectedItem:    _selectedItem,
                                   viewItem:        SelectableItem.keyValue(key:    key,
                                                                            value:  Binding(get: { kvPair[key] },
                                                                                            set: { nv in kvPair[key] = nv }))) {
                    Text(key + ": " + kvPair[key])
                }
            }
        }
    }
}


//  This view is where we can edit the value for a key
struct EditValueView: View {
    
    let key:            String
    let valueBinding:   Binding<String>
    
    public var body: some View {
        Form {
            TextField("Key",    text: Binding(get: { key },
                                              set: { _ in }))
                .disabled(true)
            TextField("Value",  text: valueBinding)
        }
    }
}


//  This view knows how to show the edit view for a selected item.
//  If the selected item is nothing, then an empty view is shown, but if the selected item
//  is a key/value pair, then a view to edit the value is shown.
struct SelectedItemView: View {

    @Binding var selectedItem:   SelectableItem

    var body: some View {
        
        switch selectedItem {
        case .none:
            EmptyView()
            
        case .keyValue(let key, let value):
            EditValueView(key:  key, valueBinding: value)
        }
    }
}



struct MainView: View {
    
    @State var data:            KVPair          = .init()
    @State var selectedItem:    SelectableItem  = .none
    
    var body: some View {
        VStack {
            KVPairView(kvPair:          $data,
                       selectedItem:    $selectedItem)
            SelectedItemView(selectedItem: $selectedItem)
        }
            .padding()
    }
}

If I select a key/value pair in the list and edit the value in the TextField :

It seems that the wrapped value of the selectedItem binding is never updated. As it is (supposed to be) bound to the model value that is updated, I guess there is a copy somewhere, but I can't figure where, or what is the problem.

Upvotes: 0

Views: 56

Answers (0)

Related Questions