SwiftUIでCoreDataを扱ったらPreviewがクラッシュしたときの解決方法

2020/09/30に公開

CoreDataとSwiftUIの勉強をしていてTodoアプリを作っていたらCoreDataのFetchRequestを実装している画面のプレビューがクラッシュして2日くらいハマったので解決方法を紹介します。

ハマった編

プレビューがクラッシュしたときの実装が以下になります。

// View
struct ContentView: View {
    @Environment(\.managedObjectContext) var viewContext
    
    @State private var isPresent: Bool = false
    @Environment(\.timeZone) private var timeZone
    
    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Todo.limitDate, ascending: true)], animation: .default)
    
    private var todos: FetchedResults<Todo>
    
    ...
    
    var body: some View {
        ...
    }
}

// Model
class TodoModel: ObservableObject {
    static let shared = TodoModel()
    
    static var fetchRequest: NSFetchRequest<Todo> {
        let request: NSFetchRequest<Todo> = Todo.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Todo.limitDate, ascending: true)]
        return request
    }
    
    static var preview: TodoModel {
        let model = TodoModel(inMemory: true)
        let viewContext = model.container.viewContext
        viewContext.reset()
        for i in 0..<10 {
            let item = Todo(context: viewContext)
            item.title = "Todo \(i + 1)"
            item.limitDate = Date()
        }
        do {
            try viewContext.save()
        } catch {
            fatalError(error.localizedDescription)
        }
        return model
    }
    
    let container: NSPersistentContainer
    
    private init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "TodoList")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Error \(error), \(error.userInfo)")
            }
        }
    }
    
    ...
}

SwiftUIにはCoreDataにからデータを取得する@FetchRequestというアノテーションがあります。
それを実装するとなぜかプレビューがクラッシュします。

解決編

色々調べている中でこの情報に行き着きました。
ページの最後の方に
フェッチリクエストの作成を新しい静的変数に移動し、別の@FetchRequestラッパーを使用すると、クラッシュしなくなりました
との記載があったので記載されているとおりに修正してみました。

// Model
class TodoModel: ObservableObject {
    ...
    // FetchRequestのstatic変数を実装
    static var fetchRequest: NSFetchRequest<Todo> {
        let request: NSFetchRequest<Todo> = Todo.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Todo.limitDate, ascending: true)]
        return request
    }
    ...
}
// View
struct ContentView: View {
    ...
    // 引数にModelで実装したFetchRequestを指定
    @FetchRequest(fetchRequest: TodoModel.fetchRequest)
    ...
}

するとクラッシュがなくなり無事プレビューが表示されました!🎉

明確な原因はまだわかっていませんが分かり次第更新したいと思います。
もし同じ状態でハマっている方の役に立てばと思います。

Discussion