Swift Utilities — Работа со SwiftData в Background

от автора

За годы работы разработчиком 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 — удобный инструмент, но все еще нужно знать, как правильно его готовить

Еще статьи Swift Utilities:


ссылка на оригинал статьи https://habr.com/ru/articles/779450/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *