Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Changes in detail view to @fetchRequest pops navigation link view

Tags:

I have two views. One view is a list of objects (persons) fetched from a core data store using @fetchRequest. The other view is a detail view which the user can navigate to by tapping on a person item in the list view. The idea is that the user can edit the details of a person in the detail view (e.g. name, age). So far so good.

The crucial point is that the list view is designed such that not all persons are necessarily fetched from the core data store. Only persons which fulfil a certain criteria are fetched. Let us assume that the criteria is that the person must be between the age of 30 and 40 years.

My problem is that when the user changes the age of a person in the detail view to some age which does not fulfil the criteria of the fetch request (e.g. he changes the age from 35 to 20 years), the detail view will pop once the user taps the save button and the managed object context saves, which is registered by @fetchRequest in the list view.

I understand that this happens, because the fetchRequest of persons driving the list view changes, as the edited person is removed, because he does not fulfil being between 30 and 40 years anymore. But I don't want this to happen while the user is still in the detail view. Tapping the save button should not automatically pop the detail view. Is there any way to prevent this from happening while using @fetchRequest?

Here is a condensed version of the code resulting in the described issue:

struct PersonList: View {
    @FetchRequest var persons: FetchedResults<Person>
    
    init() {
        let lowerAge = NSPredicate(format: "%K >= %@", #keyPath(Person.age), 30 as CVarArg)
        let upperAge = NSPredicate(format: "%K <= %@", #keyPath(Person.age), 40 as CVarArg)
        let agePredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [lowerAge, upperAge])
        _persons = FetchRequest(entity: Person.entity(), sortDescriptors: [], predicate: agePredicate)
    }
    
    var body: some View {
      NavigationView {
        ScrollView(showsIndicators: false) {
            LazyVStack(spacing: 10) {
                ForEach(persons, id: \.id) { person in
                    NavigationLink(destination: PersonDetail(person: person)) {
                        Text(person.name)
                    }
                }
            }
        }
      }
}

struct PersonDetail: View {
    @Environment(\.managedObjectContext) var viewContext
    @State var ageInput: String = ""
    var person: Person
    
    var body: some View {
        TextField("Enter Age", text: $ageInput)
        .onAppear(perform: {
           ageInput = "\(person.age)"
        })
        Button("Save", action: { save() } )
        }
    }

    func save() {
        if let age = Int(ageInput) {
            viewContext.saveContext()
        }     
    }
}
like image 621
DjangoTenenbaum Avatar asked Dec 29 '20 21:12

DjangoTenenbaum


1 Answers

As you pointed out, the NavigationLink is popped because the underlying model is removed from the fetch request. Unfortunately this means that you'll have to move the navigation link outside your list view so that you can cache the model even if it gets removed from the fetch results list.

One way to do that is to embed your VStack/ScrollView inside a ZStack with a hidden "singleton" navigation link and a @State variable that keep track of the actively selected Person:

struct PersonList: View {
    @FetchRequest var persons: FetchedResults<Person>

    @State private var selectedPerson = Person(entity: Person.entity(), insertInto: nil)
    @State private var showPersonDetail = false

    var body: some View {
        NavigationView {
            ZStack {
                NavigationLink(destination: PersonDetail(person: selectedPerson), isActive: $showPersonDetail) { EmptyView() }.hidden()
                ScrollView(showsIndicators: false) {
                    LazyVStack(spacing: 10) {
                        ForEach(persons, id: \.id) { person in
                            Button(action: {
                                selectedPerson = person
                                showPersonDetail = true
                            }, label: {
                                // You could hack out the click/tap listener for a navigation link instead of
                                // emulating its appearance, but that hack could stop working in a future iOS release.
                                HStack {
                                    Text(person.name)
                                    Spacer()
                                    Image(systemName: "chevron.right")
                                }
                            })
                        }
                    }
                }
          }
      }
}
like image 143
Joseph Quigley Avatar answered Oct 03 '22 01:10

Joseph Quigley