Использование результатов @FetchRequest внутри родительского представления

Я пытаюсь абстрагировать некоторые компоненты на более мелкие части. Для этого я создал следующий список:

struct ArticleList: View {
    var fetchRequest: FetchRequest<Article>
    var results: FetchedResults<Article> { fetchRequest.wrappedValue }

    init() {
        fetchRequest = FetchRequest<Article>(
            entity: Article.entity(),
            sortDescriptors: []
        )
    }

    var body: some View {
        ForEach(results) { article in
            Text(article.name ?? "")
        }
    }
}

Теперь у меня есть контейнер, в котором будет отображаться компонент списка, а также некоторые дополнительные вещи, если выполняются условия внутри дочерних компонентов:

struct Container: View {
    var body: some View {
        let articleList = ArticleList2()

        return Group {
            if articleList.results.isEmpty {
                Text("Add")
            }

            articleList
        }
    }
}

Моя проблема теперь в том, что код вылетает со следующим исключением:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

При дальнейшей отладке консоль дает мне следующие отзывы:

(lldb) po self.results
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated

Отладка po self.fetchRequest работает и содержит экземпляр FetchRequest<Article> пример. po self.fetchRequest.wrappedValue дает ту же ошибку, что и self.results над.

Есть ли у кого-нибудь идеи, почему этот код дает сбой и что можно сделать?

Спасибо.

2 ответа

Ваш запрос на выборку не работает, потому что контекст управляемого объекта еще не доступен в то время, когда вы создаете и используете ArticleList Посмотреть.

В любом случае... найдите ниже измененный (я попытался минимизировать изменения) ваш код, который работает. Протестировано с Xcode 11.4 / iOS 13.3

struct ArticleList: View {
    // always keep View memebers private to safe yourself from misconcept
    private var fetchRequest: FetchRequest<Article>
    private var results: FetchedResults<Article> { fetchRequest.wrappedValue }
    private var reportEmpty: () -> ()

    init(_ onEmpty: @escaping () -> ()) {
        reportEmpty = onEmpty

        // FetchRequest needs @Environment(\.managedObjectContext) which is not available (!) yet
        fetchRequest = FetchRequest<Article>(
            entity: Article.entity(),
            sortDescriptors: []
        )
    }

    var body: some View {
        // here (!) results are valid, because before call body SwiftUI executed FetchRequest
        if self.results.isEmpty { 
            self.reportEmpty()
        }
        return Group {
            ForEach(results, id: \.self) { article in
                Text(article.name ?? "")
            }
        }
    }
}

struct Container: View {
    @State private var isEmpty = false

    var body: some View {
        return Group {
            if self.isEmpty { // use view state for any view's conditions
                Text("Add")
            }

            ArticleList { // View must live only in view hierarchy !!
                DispatchQueue.main.async {
                    self.isEmpty = true
                }
            }
        }
    }
}

Хотя решение от @Asperi работает, я реализовал его по-другому.

Я закрываю ArticleList и этот обратный вызов выполняется, если Buttonнажата. ВButton доступно, только если ArticleList пусто, но теперь ArticleList отвечает за отображение кнопки (что делает ее более пригодной для повторного использования:

struct ArticleList: View {
    var fetchRequest: FetchRequest<Article>
    var results: FetchedResults<Article> { fetchRequest.wrappedValue }

    let onCreate: (() -> Void)

    init(onCreate: @escaping (() -> Void)) {
        fetchRequest = FetchRequest<Article>(
            entity: Article.entity(),
            sortDescriptors: []
        )

        self.onCreate = onCreate
    }

    var body: some View {
        Group {
            if results.isEmpty {
                Button(action: onCreate) {
                    Text("Add")
                }
            }

            ForEach(results) { article in
                Text(article.name ?? "")
            }
        }

    }
}

struct Container: View {
    var body: some View {
        ArticleList(onCreate: onCreate)
    }

    func onCreate() {
        // Create the article inside the container
    }
}
Другие вопросы по тегам