Reputation: 2959
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