Swift Utilities — Работа со SwiftData в Background
За годы работы разработчиком iOS, я собрал множество инструментов и полезных штук, которые облегчают процесс разработки. В этой статье, я хочу поделиться одним из таких инструментов. Это будет не большая статья. Я покажу, как пользоваться этой утилитой, продемонстрирую её в действии. Надеюсь, что статья окажется полезной для вас.
SwiftData отлично функционирует внутри View: достаточно добавить декоратор @Query
к свойству, и все будет работать 'из коробки'. Однако, когда возникает желание вынести работу со SwiftData в отдельный модуль, начинают появляться сложности, особенно касаемо выполнения операций в фоновом режиме.
Как можно делать запрос без @Query:
func getMyModels() -> [MyModel] {
let context = ModelContext(modelContainer)
let result = try context.fetch(FetchDescriptor())
return result
}
Данный код не потокобезопасен. При обращении из разных потоков к контейнеру, в лучшем случае, будет краш, в худшем, операция выполниться, но с непредсказуемым результатом.
Самым очевидным, кажется, это работать через мьютекс (NSLock, UnfairLock, DispatchSemaphore или другие)
func getMyModels() -> [MyModel] {
// в этом примере реализация мьюекса не важна
mutex {
let context = ModelContext(modelContainer)
let result = try context.fetch(FetchDescriptor())
return result
}
}
При такой реализации я переодически сталкивался с крашами в приложении. В этом случае потокобезопасность не достигалась.
Ситуацию исправляет DefaultSerialModelExecutor
. Он гарантирует потокобезопасности. Для удобства я сделал Дженерик актор BackgroundSerialPersistenceActor
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(
predicate: Predicate? = nil,
sortBy: [SortDescriptor] = []
) throws -> [T] {
let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: sortBy)
let list: [T] = try context.fetch(fetchDescriptor)
return list
}
public func fetchCount(
predicate: Predicate? = nil,
sortBy: [SortDescriptor] = []
) throws -> Int {
let fetchDescriptor = FetchDescriptor(predicate: predicate, sortBy: sortBy)
let count = try context.fetchCount(fetchDescriptor)
return count
}
public func insert(data: T) {
let context = data.modelContext ?? context
context.insert(data)
}
public func save() throws {
try context.save()
}
public func remove(predicate: Predicate? = nil) throws {
try context.delete(model: T.self, where: predicate)
}
public func saveAndInsertIfNeeded(
data: T,
predicate: Predicate
) throws {
let descriptor = FetchDescriptor(predicate: predicate)
let context = data.modelContext ?? context
let savedCount = try context.fetchCount(descriptor)
if savedCount == 0 {
context.insert(data)
}
try context.save()
}
}
Актор BackgroundSerialPersistenceActor
представляет собой решение для работы с данными в фоновом режиме, обеспечивая последовательную и безопасную работу с данными.
Актор инкапсулирует в себе контейнер модели (ModelContainer
) и исполнителя модели (ModelExecutor
), обеспечивая изолированное пространство для работы с данными модели.
Инициализация
Для начала работы с актором необходимо создать его экземпляр, передав в конструктор контейнер модели.
Важно, этот актор работает как мьютекс, по этому необходимо иметь один экземпляр Актора для одного контейнера для корректной работы.
Заключение
BackgroundSerialPersistenceActor
ообеспечивает безопасность, гибкость и удобство в управлении данными. Однако важно помнить, что после операции fetch, когда модели (PersistentModel) передаются в другие функции и потоки, они сохраняют в себе контекст. Изменение поля у одной модели в разных потоках может привести к крашу приложения. Поэтому безопаснее всего после fetch мапить результат в другую структуру.
SwiftData — удобный инструмент, но все еще нужно знать, как правильно его готовить