Dynamic list sections with observed data with SwiftUI

Hi everyone!

I'm currently struggling with dynamically filtering data in a SwiftUi List view. In the following example, I created some example data and stored them within "TestArray". These data is dynamically filtered and grouped by the starting letter.

The data of the "Testclass" objects can be changed in "EditDetails" view. Unfortunately, when changing the name (as it is the relevant property for filtering here), when closing the modal, the user does not return to DetailView but will break the view hierarchy and end up in the ContentView. I assume the issue is the update within ContentView, which is recreating the DetailView stack.

Is it possible to ensure the return to view, where the modal has been opened from (DetailView)?

Here is some code if you would like to reproduce the issue:

import Foundation
import SwiftUI
import Combine

struct ContentView: View {
    @StateObject var objects = TestArray()

    var body: some View {
        NavigationView{
            List {
               ForEach(objects.objectList.compactMap({$0.name.first}).unique(), id: \.self) { obj in
                    Section(header: Text(String(obj))) {
                        ForEach(objects.objectList.filter({$0.name.first == obj}), id: \.self) { groupObj in
                            NavigationLink(destination: Detailview(testclass: groupObj)) {
                                Text("\(groupObj.name)")
                            }
                        }
                    }
                }
            }
        }
    }
}


struct Detailview: View {
    @ObservedObject var testclass: Testclass
    @State private var showingEdit: Bool = false

    var body: some View {
        VStack {
            Text("Hello, \(testclass.name)!")
            Text("\(testclass.date)!")
        }
        .toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                Button(action: {
                    self.showingEdit.toggle()
                }) {
                    Image(systemName: "square.and.pencil")
                }
            }
        }
        .sheet(isPresented: $showingEdit) {
            EditDetails(testclass: testclass, showEdit: $showingEdit)
        }
    }
}

struct EditDetails: View {
    @ObservedObject var testclass: Testclass
    @Binding var showEdit: Bool
    @State private var name: String
    @State private var date: Date = Date()

    init(testclass: Testclass, showEdit: Binding<Bool>) {
        self.testclass = testclass
        _name = State(initialValue: testclass.name)
        _date = State(initialValue: testclass.date)
        _showEdit = Binding(projectedValue: showEdit)
    }

    var body: some View {
        NavigationView{
            List {
                TextField("Name", text: $name)
                    .onChange(of: name, perform: { newValue in
                        self.testclass.name = newValue
                    })

                DatePicker(selection: $date) {
                    Text("Date")
                }
                .onChange(of: date, perform: { newValue in
                    self.testclass.date = newValue
                })
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        testclass.objectWillChange.send()
                        showEdit.toggle()
                    }) {
                        Image(systemName: "xmark")
                    }
                }
            }
        }
    }
}

extension Sequence where Iterator.Element: Hashable {
    func unique() -> [Iterator.Element] {
        var seen: Set<Iterator.Element> = []
        return filter { seen.insert($0).inserted }
    }
}

class Testclass: NSObject, ObservableObject {
    @Published var name: String
    @Published var date: Date

    init(name: String, date: Date = Date()) {
        self.name = name
        self.date = date
        super.init()
    }
}

class TestArray: NSObject, ObservableObject {
    @Published var objectList: [Testclass]
    private var cancellables = Set<AnyCancellable>()

    override init() {
        self.objectList = [
            Testclass(name: "A1"),
            Testclass(name: "B1"),
            Testclass(name: "Z2"),
            Testclass(name: "C1"),
            Testclass(name: "D1"),
            Testclass(name: "D2"),
            Testclass(name: "A2"),
            Testclass(name: "Z1")
        ]
        super.init()

        objectList.forEach { object in
            object.objectWillChange
                .receive(on: DispatchQueue.main) // Optional
                .sink(receiveValue: { [weak self] _ in
                    self?.objectWillChange.send()
                    print("changed value \(object.name)")
                })
                .store(in: &cancellables)
        }
    }
}

I ran your code with Xcode 13.4.1 & iOS 15.5 and it works as expected and not as described: one thing I will do is sort the data at source

        self.objectList = [
            Testclass(name: "A1"),
            Testclass(name: "B1"),
            Testclass(name: "Z2"),
            Testclass(name: "C1"),
            Testclass(name: "D1"),
            Testclass(name: "D2"),
            Testclass(name: "A2"),
            Testclass(name: "Z1")
        ].sorted(by: {$0.name < $1.name})

Unfortunatley using Xcode 13.4.1 & iOS 15.5 I can still reproduce this issue. The problem only occurs, when change affects the sorting, e.g.

  • change "A1" to "AB82" will return to DetailView when closing the modal
  • change "A1"to "K84" will return to ContentView when closing the modal
Dynamic list sections with observed data with SwiftUI
 
 
Q