🦀

[SwiftUI] CoreDataのDataModelをイニシャライザの引数に受け取るViewのCanvas Previewを上手く書く

2021/02/15に公開

概要

例えば、以下の様なCore DataのData Modelを想定します。

extension ToDo {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<ToDo> {
        return NSFetchRequest<ToDo>(entityName: "ToDo")
    }
    
    @NSManaged public var id: UUID?
    @NSManaged public var title: String?
    @NSManaged public var content: String?
}

このData Modelをイニシャライザの引数に取るViewを適当に以下の様に書きます。

import SwiftUI

struct ContentView: View {
    var todo: ToDo
    var body: some View {
	VStack(alignment: .leading) {
            Text(todo.title!)
	    Text(todo.content!)
	}
    }
}

この時、このContentViewのCanvas Previewを表示するコードをどう書けば上手くいくのか悩みました。

色々ググった結果、以下の記事を見つけました。

https://lailo.ch/tips/swiftui-preview-with-coredata

解決

前提として、CoreDataのPersistenceController周りのコードを一つの構造体にまとめておきます。

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    init() {
        container = NSPersistentCloudKitContainer(name: "ToDo")
        container.loadPersistentStores(completionHandler: { _, error in
            if let error = error as NSError? {
                print(error.localizedDescription)
            }
        })
    }

    static func saveContext() {
        let context = Self.shared.container.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

そして、以下の様なCanvas PreviewのWrapperになるコードを書きます。

import CoreData
import SwiftUI

struct PreviewWrapper<Content: View>: View {
    let content: (NSManagedObjectContext, ToDo) -> Content

    var body: some View {
        let context = PersistenceController.shared.container.viewContext
        let todo = ToDo(context: context)
        
        todo.id = UUID()
        todo.title = "宿題"
        todo.content = "算数のドリル10ページ、英単語の書取りの練習100語"

        return self.content(context, todo)
    }

    init(@ViewBuilder content: @escaping (NSManagedObjectContext, ToDo) -> Content) {
        self.content = content
    }
}

こうすることで、肝心のCanvas Previewのコードが以下の様に書けます。

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        PreviewWrapper { _, todo in
            ContentView(todo: todo)
        }
    }
}

Canvas Previewは以下の様になります。

Listの場合は?

例えば、次の様にイニシャライザに[ToDo]型の引数を取るListを表示したい時はどうでしょうか。
コード内のContentViewは前の例のContentViewをそのまま使用しています。

import SwiftUI

struct ContentListView: View {
    
    var todos: [ToDo]
    
    var body: some View {
        List {
            ForEach(todos) { todo in
                ContentView(todo: todo)
            }
        }
    }
}

この場合は一例として次の様にPreviewのWrapperを作ることで解決できます。
[ToDo]型の配列の長さは適当に10にしています。

struct PreviewListWrapper<Content: View>: View {
    let content: (NSManagedObjectContext, [ToDo]) -> Content

    var body: some View {
        let context = PersistenceController.shared.container.viewContext
        let todo = ToDo(context: context)
        
        let todos: [ToDo] = ([Int])(0 ..< 10).map { _ in
            todo.id = UUID()
            todo.title = "宿題"
            todo.content = "算数のドリル10ページ、英単語の書取りの練習100語"
            
            return todo
        }

        return self.content(context, todos)
    }

    init(@ViewBuilder content: @escaping (NSManagedObjectContext, [ToDo]) -> Content) {
        self.content = content
    }
}

Canvas Previewのコード:

struct ContentListView_Previews: PreviewProvider {
    static var previews: some View {
        PreviewListWrapper { _, todos in
            ContentListView(todos: todos)
        }
    }
}

Canvas Preview:

Discussion