SwiftUI에서 관련 엔티티가 변경되면 @FetchRequest를 업데이트하는 방법은 무엇입니까?


13

SwiftUI에서 View나는있다 List에 기초하여 @FetchRequest(A)의 데이터를 나타내는 Primary개체와의 관계를 통해 연결된 Secondary엔티티. View하고는 List나는 새로운 추가 할 때 제대로 업데이트되어 Primary새로운 관련 보조 엔티티와 엔티티.

문제는 Secondary상세보기에서 연결된 항목을 업데이트하면 데이터베이스가 업데이트되지만 변경 사항이 Primary목록에 반영되지 않는다는 것 입니다. 분명히, @FetchRequest다른보기의 변경 사항에 의해 트리거되지 않습니다.

그 후 기본보기에 새 항목을 추가하면 이전에 변경된 항목이 최종적으로 업데이트됩니다.

해결 방법으로, Primary상세보기에서 엔티티 의 속성을 추가로 업데이트 하면 변경 사항이 Primary보기에 올바르게 전파됩니다 .

내 질문은 : @FetchRequestsSwiftUI Core Data 와 관련된 모든 것을 어떻게 강제 업데이트 할 수 있습니까? 특히 관련 엔터티에 직접 액세스 할 수없는 경우 / @Fetchrequests?

데이터 구조

import SwiftUI

extension Primary: Identifiable {}

// Primary View

struct PrimaryListView: View {
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        entity: Primary.entity(),
        sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View {
        List {
            ForEach(fetchedResults) { primary in
                NavigationLink(destination: SecondaryView(primary: primary)) {
                VStack(alignment: .leading) {
                    Text("\(primary.primaryName ?? "nil")")
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                }
                }
            }
        }
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: {self.addNewPrimary()} ) {
                Image(systemName: "plus")
            }
        )
    }

    private func addNewPrimary() {
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    }
}

struct PrimaryListView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView {
            PrimaryListView().environment(\.managedObjectContext, context)
        }
    }
}

// Detail View

struct SecondaryView: View {
    @Environment(\.presentationMode) var presentationMode

    var primary: Primary

    @State private var newSecondaryName = ""

    var body: some View {
        VStack {
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
            Button(action: {self.saveChanges()}) {
                Text("Save")
            }
            .padding()
        }
    }

    private func saveChanges() {
        primary.secondary?.secondaryName = newSecondaryName

        // TODO: ❌ workaround to trigger update on primary @FetchRequest
        primary.managedObjectContext.refresh(primary, mergeChanges: true)
        // primary.primaryName = primary.primaryName

        try? primary.managedObjectContext?.save()
        presentationMode.wrappedValue.dismiss()
    }
}

도움이되지 않았습니다. 죄송합니다. 하지만이 같은 문제가 발생합니다. 내 상세도에는 선택한 기본 객체에 대한 참조가 있습니다. 보조 개체 목록이 표시됩니다. 모든 CRUD 기능은 Core Data에서 제대로 작동하지만 UI에는 반영되지 않습니다. 이것에 대한 더 많은 정보를 얻고 싶습니다.
PJayRushton

를 사용해 보셨습니까 ObservableObject?
kdion4891

상세보기에서 @ObservedObject var primary : Primary를 사용해 보았습니다. 그러나 변경 사항이 기본보기로 다시 전파되지는 않습니다.
Björn B.

답변:


15

컨텍스트의 변경 사항에 대한 이벤트 및 기본보기의 일부 상태 변수를 생성하여 해당 게시자의 수신 이벤트에서보기를 다시 작성하도록하는 게시자가 필요합니다.
중요 사항 : 상태 변수 뷰 빌더 코드에서 사용해야합니다 . 그렇지 않으면 렌더링 엔진이 변경된 사항을 알 수 없습니다.

다음은 코드의 영향을받는 부분을 간단하게 수정하여 필요한 동작을 제공합니다.

@State private var refreshing = false
private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)

var body: some View {
    List {
        ForEach(fetchedResults) { primary in
            NavigationLink(destination: SecondaryView(primary: primary)) {
                VStack(alignment: .leading) {
                    // below use of .refreshing is just as demo,
                    // it can be use for anything
                    Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                }
            }
            // here is the listener for published context event
            .onReceive(self.didSave) { _ in
                self.refreshing.toggle()
            }
        }
    }
    .navigationBarTitle("Primary List")
    .navigationBarItems(trailing:
        Button(action: {self.addNewPrimary()} ) {
            Image(systemName: "plus")
        }
    )
}

애플이 미래에 Core Data <-> SwiftUI 통합을 개선하기를 희망합니다. 제공된 최고의 답변으로 현상금을 수여합니다. 감사합니다 Asperi.
Darrell Root

답변 주셔서 감사합니다! 그러나 @FetchRequest는 데이터베이스의 변경 사항에 반응해야합니다. 솔루션을 사용하면 관련 항목에 관계없이 데이터베이스에 저장할 때마다보기가 업데이트됩니다. 내 질문은 데이터베이스 관계와 관련된 변경 사항에 @FetchRequest를 반응시키는 방법이었습니다. 솔루션에는 @FetchRequest와 동시에 두 번째 가입자 (NotificationCenter)가 필요합니다. 또한 추가 가짜 트리거`+ (self.refreshing? "": "")``를 사용해야합니다. 어쩌면 @Fetchrequest가 적합한 솔루션이 아닐까요?
Björn B.

예, 맞습니다. 그러나 예에서 생성 된 페치 요청은 최근에 변경 한 사항의 영향을받지 않으므로 업데이트 / 리 페치되지 않습니다. 다른 인출 요청 기준을 고려해야하는 이유가있을 수 있지만 다른 질문입니다.
Asperi

2
@ Asperi 나는 당신의 대답을 받아들입니다. 언급했듯이 문제는 렌더링 엔진에서 변경 사항을 인식하는 데 어쨌든 있습니다. 변경된 객체에 대한 참조를 사용해도 충분하지 않습니다. 변경된 변수는보기에서 사용해야합니다. 신체의 어느 부분에서. 목록의 배경에 사용하더라도 작동합니다. RefreshView(toggle: Bool)본문에 단일 EmptyView와 함께를 사용합니다 . 사용 List {...}.background(RefreshView(toggle: self.refreshing))이 작동합니다.
Björn B.

List refresh / refetch 를 강제 실행하는 더 좋은 방법을 찾았습니다. SwiftUI 에서 제공됩니다 . 모든 Core Data Entity 항목을 삭제 한 후 List가 자동으로 업데이트되지 않습니다 . 만일을 위해서.
Asperi

1

다음과 같이 상세보기에서 기본 객체를 터치하려고했습니다.

// TODO: ❌ workaround to trigger update on primary @FetchRequest

if let primary = secondary.primary {
   secondary.managedObjectContext?.refresh(primary, mergeChanges: true)
}

그런 다음 기본 목록이 업데이트됩니다. 그러나 세부 사항보기는 상위 오브젝트에 대해 알아야합니다. 이것은 작동하지만 이것은 아마도 SwiftUI 또는 Combine 방법이 아닙니다 ...

편집하다:

위의 해결 방법을 기반으로 전역 저장 (managedObject :) 함수를 사용하여 프로젝트를 수정했습니다. 이것은 모든 관련 엔티티를 터치하여 모든 관련 @FetchRequest를 업데이트합니다.

import SwiftUI
import CoreData

extension Primary: Identifiable {}

// MARK: - Primary View

struct PrimaryListView: View {
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Primary.primaryName, ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View {
        print("body PrimaryListView"); return
        List {
            ForEach(fetchedResults) { primary in
                NavigationLink(destination: SecondaryView(secondary: primary.secondary!)) {
                    VStack(alignment: .leading) {
                        Text("\(primary.primaryName ?? "nil")")
                        Text("\(primary.secondary?.secondaryName ?? "nil")")
                            .font(.footnote).foregroundColor(.secondary)
                    }
                }
            }
        }
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: {self.addNewPrimary()} ) {
                Image(systemName: "plus")
            }
        )
    }

    private func addNewPrimary() {
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    }
}

struct PrimaryListView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView {
            PrimaryListView().environment(\.managedObjectContext, context)
        }
    }
}

// MARK: - Detail View

struct SecondaryView: View {
    @Environment(\.presentationMode) var presentationMode

    var secondary: Secondary

    @State private var newSecondaryName = ""

    var body: some View {
        print("SecondaryView: \(secondary.secondaryName ?? "")"); return
        VStack {
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear {self.newSecondaryName = self.secondary.secondaryName ?? "no name"}
            Button(action: {self.saveChanges()}) {
                Text("Save")
            }
            .padding()
        }
    }

    private func saveChanges() {
        secondary.secondaryName = newSecondaryName

        // save Secondary and touch Primary
        (UIApplication.shared.delegate as! AppDelegate).save(managedObject: secondary)

        presentationMode.wrappedValue.dismiss()
    }
}

extension AppDelegate {
    /// save and touch related objects
    func save(managedObject: NSManagedObject) {

        let context = persistentContainer.viewContext

        // if this object has an impact on related objects, touch these related objects
        if let secondary = managedObject as? Secondary,
            let primary = secondary.primary {
            context.refresh(primary, mergeChanges: true)
            print("Primary touched: \(primary.primaryName ?? "no name")")
        }

        saveContext()
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.