🙆

SwiftDataでCRUD

2023/12/16に公開

SwiftDataでCRUDのコード例です。
コード例を検索しても、SwiftUI(View)の中でmodelContextを宣言したりするのではなく、ViewModelやServiceから使いたいときの例があまりなかったので残しておきます。

あまり詳しく調べてはいないので、まずい書き方もあるかもしれません。
開発でデバッグしている限りでは動いてると思います。

参考文献

https://stackoverflow.com/questions/77253448/how-to-save-swiftdata-outside-swiftui-to-persist

コード例

SwiftDataCrudApp.swift
import SwiftUI
import SwiftData

@main
struct SwiftDataCrudApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(Persistance.sharedModelContainer)
    }
}
Todo.swift
import Foundation
import SwiftData

@Model
final class Todo {
    var id: UUID
    var content: String
    var dateModified: Date
    
    init() {
        self.id = UUID()
        self.content = ""
        self.dateModified = Date()
    }
}
TodoService.swift
import Foundation
import SwiftData

class Persistance {
    static var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Todo.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("error create sharedModelContainer: \(error)")
        }
    }()
    
}

actor PersistanceActor: ModelActor {
    let modelContainer: ModelContainer
    let modelExecutor: any ModelExecutor
    
    init(modelContainer: ModelContainer) {
        self.modelContainer = modelContainer
        let context = ModelContext(modelContainer)
        modelExecutor = DefaultSerialModelExecutor(modelContext: context)
    }
    
    func save() {
        do {
            try modelContext.save()
        }catch {
            print("error save")
        }
    }

    func insert<T:PersistentModel>(_ value:T) {
        do {
            modelContext.insert(value)
            try modelContext.save()
        }catch {
            print("error insert")
        }
    }
    
    func delete<T:PersistentModel>(_ value:T) {
        do {
            modelContext.delete(value)
            try modelContext.save()
        }catch {
            print("error delete")
        }
    }
    
    func get<T:PersistentModel>(_ descriptor:FetchDescriptor<T>)->[T]? {
        var fetched:[T]?
        do {
            fetched = try modelContext.fetch(descriptor)
        }catch {
            print("error get")
        }
        return fetched
    }
    
}

final class TodoService {
    static let shared = TodoService()
    
    lazy var actor = {
        return PersistanceActor(modelContainer: Persistance.sharedModelContainer)
    }()
    
    func createTodo() async -> Todo {
        let todo = Todo()
        await actor.insert(todo)
        return todo
    }
    
    func searchTodos(keyword: String) async -> [Todo] {
        let predicate = #Predicate<Todo> { todo in
            todo.content.contains(keyword)
        }

        let descriptor = FetchDescriptor(predicate: predicate)
        return await actor.get(descriptor) ?? []
    }
    
    func getTodoById(id: UUID) async -> Todo? {
        let predicate = #Predicate<Todo> { todo in
            todo.id == id
        }

        let descriptor = FetchDescriptor(predicate: predicate)
        let todos = await actor.get(descriptor)
        guard let todos = todos,
              let todo = todos.first
        else {
            return nil
        }
        return todo
    }
    
    func deleteTodo(id: UUID) async -> Bool {
        guard let todo = await getTodoById(id: id) else { return false }
        await actor.delete(todo)
        return true
    }
    
    func updateTodo(id: UUID, content: String? = nil) async -> Todo? {
        guard var todo = await getTodoById(id: id) else { return nil }
        if let content = content {
            todo.content = content
            todo.dateModified = Date()
        }
        await actor.save()
        return todo
    }
    
    func getAllTodos() async -> [Todo] {
        let predicate = #Predicate<Todo> { todo in
            return true
        }

        let descriptor = FetchDescriptor(predicate: predicate)
        return await actor.get(descriptor) ?? []
    }
}

ViewModelからの利用例

TodoViewModel.swift
extension TodoViewModel {
    func search() async {
        if (searchWord == "") {
            displayItems = await TodoService.shared.getAllTodos()
            return
        }
        displayItems = await TodoService.shared.searchTodos(keyword: searchWord)
    }
}

Discussion