Conditionally sorting `Table` data on focus change causes AttributeGraph cycle error

43 views Asked by At

Goal: macOS SwiftUI, sortable Table, double-click-to-edit-cell-contents. Focused on just String for now.

Passing a binding (e.g., TableColumn("Title", value: \.Binding<Track>.title, ...)) worked great to modify the content.

However, TextField (for String) updates its value binding on each keypress. Table sorts via the same binding, so as the user types, the row is re-sorted on each keypress. Woof.

Considered options:

  1. Create Formatter subclass (TextField with a non-String requires formatter, + doesn't exhibit that trait)
  2. Read @FocusState, dupe our tracks array, and only save the new val when the focus state changes.

Did number 2. However, I'm getting this strange output.

Focused: Optional(Field.text) -> Optional(Field.text)

=== AttributeGraph: cycle detected through attribute 827216 ===
=== AttributeGraph: cycle detected through attribute 827216 ===
=== AttributeGraph: cycle detected through attribute 827216 ===
=== AttributeGraph: cycle detected through attribute 827976 ===
=== AttributeGraph: cycle detected through attribute 817860 ===
Focused: nil -> nil

(And shoutout to onChange not reading right. Good job, buddy!)

Reduced code example:

struct TrackList: View {
    @EnvironmentObject var store: Store
    @FocusState private var focusedField: Field?
    @State private var sortedTracks: [Binding<Track>] = []

    var body: some View {
        Table(of: Binding<Track>.self, sortOrder: $sortOrder) {
            TableColumn(
                "Title", 
                value: \Binding<Track>.wrappedValue, 
                comparator: titleComparator) 
            { trackBinding in
                TextField("Title", text: trackBinding.title)
                    .focused($focusedField, equals: .text)
            }
        } rows: {
            ForEach(sortedTracks) { $track in
                TableRow($track)
            }
        }
        .onAppear {
            // Since the new `.onChange(_ of:initial:)` isn't until macOS v14, we gotta do this for now.
            sortedTracks = $store.tracks.sorted(using: sortOrder)
        }
        .onChange(of: focusedField) { newValue in
            print("Focused: \(focusedField.debugDescription) -> \(newValue.debugDescription)")
            sortedTracks = $store.tracks.sorted(using: sortOrder)
        }
    }
}
0

There are 0 answers