関連エンティティがSwiftUIで変更されたときに@FetchRequestを更新する方法


13

SwiftUIでは、エンティティのデータとビアの関係に接続されたエンティティのデータを表示ViewすることにList基づいています。そしてそのは私が新しい追加するときに、正しく更新され、新たな関連二エンティティとエンティティ。@FetchRequestPrimarySecondaryViewListPrimary

問題は、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を使用してみました。ただし、変更はプライマリビューに反映されません。
ビョルン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")
        }
    )
}

Appleが将来的にCore Data <-> SwiftUI統合を改善することを望んでいます。提供されたベストアンサーに賞金を授与します。Asperiに感謝します。
ダレルルート

お返事ありがとうございます!しかし、@ FetchRequestはデータベースの変更に反応する必要があります。ソリューションを使用すると、関係するアイテムに関係なく、データベースに保存するたびにビューが更新されます。私の質問は、@ FetchRequestがデータベースの関係を含む変更に反応するようにする方法でした。ソリューションには、@ FetchRequestと並行して2番目のサブスクライバー(NotificationCenter)が必要です。また、追加の偽トリガー `+(self.refreshing?" ":" ")`を使用する必要があります。たぶん@Fetchrequest自体は適切なソリューションではありませんか?
ビョルンB.

はい、そうですが、サンプルで作成されたフェッチリクエストは、最近行われた変更の影響を受けないため、更新/再フェッチされません。別のフェッチ要求基準を検討する理由があるかもしれませんが、それは別の質問です。
アスペリ

2
@Asperi私はあなたの答えを受け入れます。あなたが述べたように、問題はなんらかの変更を認識するレンダリングエンジンにあります。変更されたオブジェクトへの参照を使用するだけでは不十分です。変更された変数はビューで使用する必要があります。体のどの部分にも。リストの背景に使用しても動作します。RefreshView(toggle: Bool)本体に単一のEmptyView を使用します。使用してList {...}.background(RefreshView(toggle: self.refreshing))動作します。
ビョルンB.

リストの更新/ 再フェッチを強制するより良い方法を見つけました。これはSwiftUIで提供されていますすべてのコアデータエンティティエントリを削除した後、リストが自動的に更新されません。念のため。
Asperi

1

私は次のように詳細ビューで主オブジェクトに触れようとしました:

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

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

次に、プライマリリストが更新されます。ただし、詳細ビューは親オブジェクトについて知っている必要があります。これは動作しますが、これはおそらくSwiftUIまたはCombineの方法ではありません...

編集:

上記の回避策に基づいて、グローバルなsave(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.