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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

За годы работы разработчиком 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:

  • Swift Utilities — Работа с Динамическими Цветами

  • Swift Utilities — Упрощаем работу с UserDefaults

  • Swift Utilities — Потокобезопасное свойство

  • Swift Utilities — Equatable для сложных Enum

  • Swift Utilities — Интеграция SwiftUI в UIKit

Источник: https://habr.com/ru/articles/779450/


Интересные статьи

Интересные статьи

Где и как можно ужать проект, чтобы бережный расход денег и ресурсов не сказался на качестве? Разбираю это на примерах разработки из нашей практики. Помимо разбора в статье вы найдете схему информацио...
Это главы 39 и 40 раздела «HTTP API & REST» моей книги «API». Второе издание книги будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа...
Тут можно найти реализацию готового проектаНа сегодняшний день во многих приложениях мы можем наблюдать stepper, большинство из них кастомные. Несмотря на то, что Apple предоставляет уже реализацию го...
В прошлой статье я рассказывал о том, как мы реализовали на основе Google Sheets собственную диаграмму Ганта для работы над игровыми проектами. Если вам зашла такая реализация или просто интересно глу...
Первая часть: Основы работы с видео и изображениями Что? Видеокодек — это часть программного/аппаратного обеспечения, сжимающая и/или распаковывающая цифровое видео. Для чего? Невзирая ...