За годы работы разработчиком iOS, я собрал множество инструментов и полезных штук, которые облегчают процесс разработки. В этой статье, я хочу поделиться одним из таких инструментов. Это будет не большая статья. Я покажу, как пользоваться этой утилитой, продемонстрирую её в действии. Надеюсь, что статья окажется полезной для вас.
SwiftData отлично функционирует внутри View: достаточно добавить декоратор @Query к свойству, и все будет работать 'из коробки'. Однако, когда возникает желание вынести работу со SwiftData в отдельный модуль, начинают появляться сложности, особенно касаемо выполнения операций в фоновом режиме.
Как можно делать запрос без @Query:
func getMyModels() -> [MyModel] { let context = ModelContext(modelContainer) let result = try context.fetch(FetchDescriptor<MyModel>()) return result }
Данный код не потокобезопасен. При обращении из разных потоков к контейнеру, в лучшем случае, будет краш, в худшем, операция выполниться, но с непредсказуемым результатом.
Самым очевидным, кажется, это работать через мьютекс (NSLock, UnfairLock, DispatchSemaphore или другие)
func getMyModels() -> [MyModel] { // в этом примере реализация мьютекса не важна mutex { let context = ModelContext(modelContainer) let result = try context.fetch(FetchDescriptor<MyModel>()) return result } }
При такой реализации я переодически сталкивался с крашами в приложении. В этом случае потокобезопасность не достигалась.
Ситуацию исправляет DefaultSerialModelExecutor. Он гарантирует потокобезопасности. Для удобства я сделал Дженерик актор BackgroundSerialPersistenceActor<T: PersistentModel>
import Foundation import SwiftData /// ```swift /// // It is important that this actor works as a mutex, /// // so you must have one instance of the Actor for one container // // for it to work correctly. /// let actor = BackgroundSerialPersistenceActor(container: modelContainer) /// /// Task { /// let data: [MyModel] = try? await actor.fetchData() /// } /// ``` @available(iOS 17, *) public actor BackgroundSerialPersistenceActor: ModelActor { public let modelContainer: ModelContainer public let modelExecutor: any ModelExecutor private var context: ModelContext { modelExecutor.modelContext } public init(container: ModelContainer) { self.modelContainer = container let context = ModelContext(modelContainer) modelExecutor = DefaultSerialModelExecutor(modelContext: context) } public func fetchData<T: PersistentModel>( predicate: Predicate<T>? = nil, sortBy: [SortDescriptor<T>] = [] ) throws -> [T] { let fetchDescriptor = FetchDescriptor<T>(predicate: predicate, sortBy: sortBy) let list: [T] = try context.fetch(fetchDescriptor) return list } public func fetchCount<T: PersistentModel>( predicate: Predicate<T>? = nil, sortBy: [SortDescriptor<T>] = [] ) throws -> Int { let fetchDescriptor = FetchDescriptor<T>(predicate: predicate, sortBy: sortBy) let count = try context.fetchCount(fetchDescriptor) return count } public func insert<T: PersistentModel>(data: T) { let context = data.modelContext ?? context context.insert(data) } public func save() throws { try context.save() } public func remove<T: PersistentModel>(predicate: Predicate<T>? = nil) throws { try context.delete(model: T.self, where: predicate) } public func saveAndInsertIfNeeded<T: PersistentModel>( data: T, predicate: Predicate<T> ) throws { let descriptor = FetchDescriptor<T>(predicate: predicate) let context = data.modelContext ?? context let savedCount = try context.fetchCount(descriptor) if savedCount == 0 { context.insert(data) } try context.save() } }
Актор BackgroundSerialPersistenceActor<T: PersistentModel> представляет собой решение для работы с данными в фоновом режиме, обеспечивая последовательную и безопасную работу с данными.
Актор инкапсулирует в себе контейнер модели (ModelContainer) и исполнителя модели (ModelExecutor), обеспечивая изолированное пространство для работы с данными модели.
Пример использования:
let actor = BackgroundSerialPersistenceActor(container: modelContainer) Task { let data: [MyModel] = try? await actor.fetchData() }
Инициализация
Для начала работы с актором необходимо создать его экземпляр, передав в конструктор контейнер модели.
Важно, этот актор работает как мьютекс, по этому необходимо иметь один экземпляр Актора для одного контейнера для корректной работы.
Заключение
BackgroundSerialPersistenceActor ообеспечивает безопасность, гибкость и удобство в управлении данными. Однако важно помнить, что после операции fetch, когда модели (PersistentModel) передаются в другие функции и потоки, они сохраняют в себе контекст. Изменение поля у одной модели в разных потоках может привести к крашу приложения. Поэтому безопаснее всего после fetch мапить результат в другую структуру.
SwiftData — удобный инструмент, но все еще нужно знать, как правильно его готовить
