1

I'm trying to abstract some components into smaller parts. For this I created the following listing:

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 ?? "")
        }
    }
}

Now I have a container, which will display the list component, plus some additional things if conditions inside the child components are met:

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

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

            articleList
        }
    }
}

My problem is now that the code crashes with the following exception:

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

Debugging this further the console provides me the following feedback:

(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

Debugging po self.fetchRequest works and it contains an instance of the FetchRequest<Article> instance. po self.fetchRequest.wrappedValue provides the same error as self.results above.

Does anyone has an idea why this code is crashing and what a possible workaround could be?

Thanks.

Tobias Tom
  • 43
  • 5

2 Answers2

3

Your fetch request does not work, because there is no managed object context available yet in the time you create and use ArticleList view.

Anyway... find below modified (I tried minimise changes) your code that works. Tested with 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
  • 228,894
  • 20
  • 464
  • 690
  • While I can confirm that this works, it feels wrong as the state is changed inside the view building. This means that the view will hierarchy will be build twice if the state changes. That will also trigger multiple fetch requests to be executed. – Tobias Tom Mar 20 '20 at 15:45
  • 1
    @TobiasTom, delayed refresh is regular practice, some operations you wan't even made without such approach (actually it is equivalent of `setNeedsLayout` or `setNeedsDisplay`. SwiftUI handles such cases and do not re-render views which were not updated. But regarding to fetching data it is your responsibility and should not depend on or be affected with displaying view. Everything else is up to you. – Asperi Mar 20 '20 at 15:51
0

While the solution from @Asperi works, I did implement it differently now.

I pass a closure into the ArticleList and that callback is executed if a Button is pressed. The Button is only available if the ArticleList is empty, but now the ArticleList is responsible to show the button (which makes it a little more reusable for me:

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
    }
}
Tobias Tom
  • 43
  • 5