Как стать автором
Обновить
137.52
Surf
Создаём веб- и мобильные приложения

На смену CoreData пришёл новый фреймворк SwiftData. Разбираемся, как он упрощает хранение данных

Время на прочтение7 мин
Количество просмотров7.4K

Фреймворк для хранения данных Core Data был написан еще во времена Objective-C. Многим iOS-разработчикам хотелось иметь более современный инструмент, который бы поддерживал все новые возможности языка Swift. И теперь такой инструмент появился.

На WWDC 2023 представили новый фреймворк SwiftData: он создан, чтобы заменить Core Data. Он упрощает создание схемы данных, конфигурацию хранилища, а также саму работу с данными.

Я — Светлана Гладышева, iOS-разработчик компании Surf. Давайте разберёмся, что из себя представляет новый фреймворк SwiftData. А также попробуем использовать его на практике, написав небольшое приложение.

Обзор SwiftData

Фреймворк SwiftData создан на основе Core Data. Он является более высокоуровневой обёрткой над Core Data, и у него более удобный и простой синтаксис для хранения данных.

SwiftData — «Swift-native» фреймворк: всё пишется на чистом Swift. Не используются никакие другие форматы данных — в отличие от Core Data, где, например, для хранения схемы применялся формат .xcdatamodeld. SwiftData использует современные возможности языка, включая макросы, появившиеся в Swift 5.9.

SwiftData может сосуществовать вместе с Core Data. Можно настроить их таким образом, что они будут обращаться к одному и тому же хранилищу данных. Это даёт возможность переходить на новый фреймворк постепенно.

Важный момент: фреймворк SwiftData можно использовать только начиная с iOS 17.

На момент написания статьи SwiftData находится в статусе Beta. Нужно учитывать, что к моменту релиза API может немного измениться.

Создание схемы данных

Чтобы создать схему данных, не нужно создавать никаких дополнительных файлов. Достаточно добавить макрос @Model к классу, и тогда схема для него сгенерируется автоматически:

@Model
class Person {
    var name: String
    var birthDate: Date
    var address: Address
    var cars: [Car]
}

SwiftData поддерживает базовые типы данных, а также все типы, которые соответствуют протоколу Codable.

Если мы в Xcode раскроем макрос @Model, то увидим, что именно он добавляет:

Модель также можно кастомизировать с помощью макроса @Attribute . Например, можно добавить атрибут unique к полю name, чтобы сделать имя уникальным:

@Attribute(.unique) var name: String

Также с помощью макроса @Attribute можно добавить шифрование, использовать внешнее хранилище или сохранять удаленные значения.

Можно управлять связями между сущностями с помощью макроса @Relationship , например, сделать так, чтобы при удалении связанные сущности тоже удалялись:

@Relationship(.cascade) var cars: [Car]

Если вы не хотите, чтобы какое-то свойство хранилось, можно использовать для него макрос @Transient:

@Transient var accommodation: Accommodation

Конфигурация хранилища

После создания схемы данных нужно создать ModelContainer, который управляет схемой данных и конфигурацией хранилища. ModelContainer можно назвать посредником между схемой данных и самим хранилищем. Самый простой способ его создать — указать только типы данных, которые вы хотите хранить:

let modelContainer = try ModelContainer(for: [Person.self, Car.self])

Чтобы кастомизировать настройки хранилища, можно использовать ModelConfiguration. С её помощью можно указать, где будут храниться данные: в памяти или на диске. Можно указать конкретный url, где будет храниться файл с данными. Также можно дать доступ только на чтение. Есть возможность создать несколько конфигураций для разных типов данных:

let fullSchema = Schema([Person.self, Address.self, Car.self])
let personConfiguration = ModelConfiguration(
    schema: Schema([Person.self, Address.self]),
    url: URL(filePath: "/path/to/person/data.store")
  
let carConfiguration = ModelConfiguration(
    schema: Schema([Car.self]),
    url: URL(filePath: "/path/to/car/data.store")
  
let modelContainer = try ModelContainer(for: fullSchema, personConfiguration, carConfiguration)

Если нужна миграция данных с одной версии на другую, то при создании ModelContainer нужно указать план миграции:

let modelContainer = try ModelContainer(
    for: Schema([Person.self, Car.self]),
    migrationPlan: AppMigrationPlan.self
)

В SwiftUI появился специальный модификатор .modelContainer для создания контейнера:

ContentView()
    .modelContainer(for: [Person.self, Car.self])

Этот модификатор также добавляет modelContainer и связанный с ним modelContext в Environment для дальнейшего использования во всех вложенных view:

@Environment(\.modelContext) var modelContext

Изменение данных

Для создания, изменения или удаления данных в SwiftData нужен ModelContext. Это сущность, которая хранит в памяти модель данных, наблюдает за всеми сделанными изменениями, а также занимается сохранением данных.

У каждого ModelContainer есть mainContext — это специальный контекст, который привязан к MainActor. Он предназначен для работы с данными из Scenes и Views.

let modelContext = modelContainer.mainContext

Контекст также можно создать, передав ему в конструктор modelContainer:

let modelContext = ModelContext(modelContainer)

Чтобы создать сущность, нужно вызвать у контекста метод insert:

var person = Person(name: name)
modelContext.insert(person)

Для удаления есть метод delete:

modelContext.delete(person)

ModelContext загружает в память все данные, с которыми работает. Когда мы что-то создаём, изменяем или удаляем, контекст отслеживает все эти изменения и хранит их внутри себя. Даже если удалённый объект уже не отображается в списке, он все равно существует внутри контекста. Когда вызывается метод save, контекст сохраняет изменения в modelContainer и очищает своё состояние.

ModelContext поддерживает транзакции, действия undo и redo, а также автосохранение. При включенном автосохранении метод save будет вызываться по таким событиям, как уход в background или возвращение в foreground, а также будет периодически вызываться, когда приложение активно. Для MainContext автосохранение включено по умолчанию. Для контекстов, созданных вручную, его можно включить с помощью параметра isAutosaveEnabled.

Получение данных

Для получения данных в modelContext есть метод fetch, в который мы должны передать FetchDescriptor. В FetchDescriptor мы описываем, какие именно данные нам нужны и в каком порядке мы хотим их получить:

let upcomingTrips = FetchDescriptor<Trip>(
    predicate: #Predicate { $0.startDate > Date.now },
    sort: \.startDate
)

Также в FetchDescriptor можно указать другие параметры, такие как fetchLimit и fetchOffset.

Предикат может быть и более сложным, например, вот таким:

let predicate = #Predicate<Trip> { trip in
    trip.livingAccommodations.filter {
        $0.hasReservation == false
    }.count > 0
}

В SwiftUI появился новый property wrapper — @Query, который делает получение данных ещё более простым и удобным. Но основное его преимущество — Query автоматически обновляет view при каждом изменении в полученных данных.

@Query(sort: \.startDate, order: .reverse) var allTrips: [Trip]

Пример приложения

Давайте напишем небольшой словарь иностранных слов с использованием SwiftData и SwiftUI. Для удобства использования слова будут разбиты по категориям, и каждое слово будет относиться к своей категории.

Нам нужны две сущности: категория и слово.

@Model
class Category {
    @Attribute(.unique) var name: String
    @Relationship(.cascade, inverse: \Word.category) var words: [Word] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
class Word {
    var original: String
    var translation: String
    var category: Category?
    
    init(original: String, translation: String) {
        self.original = original
        self.translation = translation
    }
}

Мы хотим, чтобы у всех категорий имена не повторялись, поэтому добавляем атрибут unique и полю name. Сущности Category и Word связаны между собой. При удалении категории мы хотим удалять все слова, которые относятся к этой категории. Поэтому указали .cascade.

Теперь нужно добавить modelContainer. В SwiftUI мы можем использовать специальный модификатор .modelContainer, который добавит контейнер в наш App. При создании укажем типы двух созданных сущностей:

@main
struct WordsApp: App {
    var body: some Scene {
        WindowGroup {
            CategoriesView()
        }
        .modelContainer(for: [Category.self, Word.self])
    }
}

Далее создадим экран категорий, на котором будем отображать список категорий. Для получения категорий используем макрос @Query. Чтобы категории отображались упорядоченно, добавляем в Query сортировку по названию. При добавлении, изменении или удалении категории список будет изменяться автоматически.

struct CategoriesView: View {
    @Query(sort: \.name) var categories: [Category]
    
    var body: some View {
        List {
            ForEach(categories, id: \.id) { category in
                Text("\(category.name)")
            }
        }
    }
}

При нажатии на категорию мы хотим переходить на экран слов, относящихся к этой категории. Создадим этот экран:

struct WordsView: View {
    var category: Category
    
    var body: some View {
        List {
            ForEach(category.words, id: \.id) { word in
                VStack {
                    Text("\(word.original)")
                    Text("\(word.translation)")
                }
            }
        }
    }
}

Сюда передаём категорию, и из неё берём список слов для отображения.

Затем нам нужно добавить возможность создавать категории и удалять их. Для этого нужен modelContext, который можно получить из Environment:

@Environment(\.modelContext) var modelContext

Создание категории выглядит вот так:

func createCategory(name: String) {
    let category = Category(name: name)
    modelContext.insert(category)
}

Для удаления воспользуемся методом delete:

func deleteCategory(_ category: Category) {
    modelContext.delete(category)
}

Аналогично будут выглядеть методы создания и удаления слова:

func createWord(original: String, translation: String) {
    let word = Word(original: original, translation: translation)
    word.category = category
    category.words.append(word)
}

func deleteWord(word: Word) {
    modelContext.delete(word)
    category.words.removeAll(where: { $0 == word })
}

Поскольку на экран слов мы берём слова из переданной категории и не используем здесь @Query, то автоматически экран обновляться не будет. Поскольку мы хотим, чтобы экран обновлялся, то мы сами должны обновлять объект category, добавляя или удаляя в нём слова.

Полный код приложения

Усложняем пример

Теперь предположим, что мы не хотим по каким-то причинам использовать SwiftUI в приложении. Либо хотим сделать отдельный data-слой, не связанный с SwiftUI. Давайте попробуем сделать это.

Классы Category и Word останутся такими же: их менять не нужно. А вот инициализацию ModelContainer поменять придётся. Теперь она будет выглядеть вот так:

let modelContainer = try ModelContainer(for: [Category.self, Word.self])

Получение данных тоже поменяется: вместо макроса Query нам нужно использовать метод fetch у modelContext. ModelContext мы можем получить из modelContainer. В метод fetch мы передаем fetchDescriptor с нужной сортировкой по имени:

func fetchCategories() throws -> [Category] {
    let fetchDescriptor = FetchDescriptor(sortBy: [SortDescriptor(\Category.name)])
    return try modelContext.fetch(fetchDescriptor)
}

Для получения слов нам в fetchDescriptor кроме сортировки нужно передать предикат, с помощью которого будем получать слова только из нужной категории.

func fetchWords(category: Category) throws -> [Word] {
    let categoryName = category.name
    let fetchDescriptor = FetchDescriptor(
        predicate: #Predicate { $0.category?.name == categoryName },
        sortBy: [SortDescriptor(\Word.original)]
    )
    return try modelContext.fetch(fetchDescriptor)
}

Создание и удаление сущностей останется без изменений.

К сожалению, на момент написания статьи в SwiftData нет возможности использовать аналог Query вне SwiftUI. Поэтому данные не будут автоматически обновляться при изменениях, и их нужно обновлять вручную.

Полный код этого приложения


Появление SwiftData сильно упростило работу с данными по сравнению с Core Data. К сожалению, его можно использовать только начиная с iOS 17.

Больше информации про SwiftData можно узнать в видео с WWDC 2023:

Больше полезного про iOS — в нашем телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >>

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии3

Публикации

Информация

Сайт
surf.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия