Изучение того, как хранить данные в SwiftUI

Ранее я писал статью, объясняющую, что такое FileManager и как его можно использовать с UIKit. Я рекомендую прочитать его, прежде чем перейти к сегодняшней статье:



В этой статье мы узнаем, что такое FileManager и как его использовать. Мы узнаем, как использовать FileManager в простом проекте SwiftUI. Начнем с того, что такое FileManager.

Примечание. Вы можете скачать пример проекта и исходные коды этой статьи на GitHub.

Что такое FileManager?

FileManager позволяет нам создавать и редактировать данные для типов файлов JSON или plist, а также использовать эти данные в наших приложениях.

FileManager очень похож на Core Data, но FileManager более понятен, чем Core Data. Короче говоря, хотя вы можете создавать более сложные структуры данных с помощью Core Data, вы можете создавать более простые структуры данных в FileManager более быстрым способом. Итак, FileManager отлично подходит для небольших инди-приложений.

Как использовать FileManager в SwiftUI

Чтобы хранить и редактировать данные в FileManager, нам нужно использовать декодеры и кодеры. Очевидно, что все файловые менеджеры в основном используют декодеры и кодировщики.

Обычно мы сохраняем значения в FileManager прямо из типа Data. Тип Data можно считать самым основным типом данных, потому что, когда вы храните данные с типом Data, вы напрямую сохраняете 0 и 1 эквивалент этих данных. Вы знаете, что все данные на вашем компьютере в основном состоят из нулей и единиц.

С FileManager мы можем делать в основном две вещи: декодировать и кодировать данные. Например, мы можем декодировать настраиваемый массив объектов, используя PropertyListDecoder:

let decoder = PropertyListDecoder()
let data = try Data(contentsOf: dataSourceURL)
let decodedNotes = try! decoder.decode([Note].self, from: data)

Точно так же мы можем добавить настраиваемый объект в массив настраиваемых объектов, используя объект PropertyListEncoder:

let encoder = PropertyListEncoder()
let data = try encoder.encode(allNotes)
try data.write(to: dataSourceURL)

В отличие от UIKit, мы должны выполнять эти операции отдельно, потому что Swift не позволяет нам использовать оболочку свойства в вычисляемом свойстве. Мы увидим это при разработке типового проекта.

Использование FileManager в простом проекте SwiftUI

Чтобы понять, как использовать FileManager в SwiftUI, мы сделаем проект, аналогичный тому, что я создал в этой статье. SwiftUI работает немного иначе, чем UIKit. Вот почему нам нужно будет внести некоторые изменения в класс FileManager.

Создание проекта Xcode

Давайте создадим новый проект Xcode. Конечно, интерфейс нашего проекта должен быть SwiftUI.

Вы можете дать проекту любое имя. После этого остается только приступить к созданию своего проекта.

Мой совет вам при разработке проекта - разделить файлы по папкам. При выполнении этого проекта вы можете использовать папки так же, как и я:

Построение модели данных

В этой статье мы создадим приложение для заметок. Таким образом, наша модель данных должна представлять один объект заметки. Эта модель имеет три свойства: id, title, description.

struct Note: Codable, Identifiable {
    
    // MARK: - Properties
    var id = UUID()
    let title: String
    let description: String
}

В этой модели данных мы используем протоколы Codable и Identifiable.

Codable - это псевдоним типа для протоколов Encodable и Decodable. Когда вы используете Codable в качестве типа или общего ограничения, он соответствует любому типу, который соответствует обоим протоколам . - Разработчик Apple

Используйте протокол« Идентифицируемый , чтобы обеспечить устойчивое представление об идентичности для класса или типа значения. Например, вы можете определить тип User со свойством id, которое будет стабильным для вашего приложения и хранилища базы данных вашего приложения. Вы можете использовать свойство id для идентификации конкретного пользователя, даже если изменяются другие поля данных, такие как имя пользователя ». - Разработчик Apple

Подготовка класса FileManager

Теперь, когда наша модель данных готова, мы можем приступить к созданию нашего класса FileManager. Для этого создадим новый файл Swift с именем DataProvider:

class DataProvider: ObservableObject {

}

Как видите, мы используем протокол ObservableObject для передачи данных.

По умолчанию ObservableObject синтезирует objectWillChange издателя, который передает измененное значение до того, как любое из его @Published свойств изменится. - Разработчик Apple

В нашем классе свойство @Published будет массивом объектов Note:

class DataProvider: ObservableObject {

    // MARK: - Properties
    @Published var allNotes = [Note]()
}

Определите значение с именем shared для вызова класса DataProvider вне себя:

class DataProvider: ObservableObject {

    // MARK: - Properties
    static let shared = DataProvider()
    @Published var allNotes = [Note]()
}

Для хранения данных с FileManager нам нужен объект URL. Мы определяем для этого свойство:

Как я сказал ранее, значение, использующее оболочку свойства @Published, не может быть вычисляемым свойством. Вот почему мы должны использовать два дополнительных метода для операций получения и установки данных.

Сначала напишите метод под названием getAllNotes, чтобы вернуть массив Note, который мы сохранили ранее. Если мы не сохранили ни одного Note объекта, метод должен вернуть пустой массив. Это очень важно. В противном случае наше приложение выйдет из строя.

Мы будем использовать метод getAllNotes внутри метода init. В противном случае элемент allNotes будет пустым, и наше приложение выйдет из строя. Следовательно, после определения элемента dataSourceURL в методе init, мы выравниваем элемент allNotes с getAllNotes:

Вы можете спросить, почему он таким образом уравнивает эти два значения. Это потому, что allNotes имеет _ в начале, а метод getAllNotes преобразуется в свойство @Published.

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

Чтобы создать новый объект Note, напишите метод ниже. Как видите, после вставки нового объекта Note в allNotes мы вызываем метод saveNotes только для сохранения изменений:

func create(note: Note) {
    allNotes.insert(note, at: 0)
    saveNotes()
}

Напишите метод для замены ранее определенного объекта Note. Точно так же после внесения изменений мы вызываем метод saveNotes:

func changeNote(note: Note, index: Int) {
    allNotes[index] = note
    saveNotes()
}

Напишите новый метод удаления объекта Note. Чтобы удалить элемент в List объекте в SwiftUI, нам нужна offsets информация об этом элементе. Вот почему у этого метода есть параметр offsets:

func delete(_ offsets: IndexSet) {
    allNotes.remove(atOffsets: offsets)
    saveNotes()
}

Наконец, напишите метод под названием move для перемещения List ячейки. Источником информации о ячейках в объекте List, который мы создадим, будет массив allNotes. Следовательно, когда мы меняем положение ячейки, нам нужно изменить смещения объекта Note этой ячейки:

func move(source: IndexSet, destination: Int) {
    allNotes.move(fromOffsets: source, toOffset: destination)
    saveNotes()
}

С этим методом мы закончили с классом DataProvider!

Подготовка пользовательского интерфейса

Чтобы получить доступ к свойствам и методам в классе DataProvider, давайте определим элемент, используя оболочку свойств @ObservedObject ниже в основном объекте View:

Таким образом, мы можем в основном создать наш основной пользовательский интерфейс:

Чтобы сделать код более надежным, создайте собственный объект с объектами, которые мы пишем в объекте ForEach:

Как видите, наш главный View объект намного удобнее:

В настоящее время мы не можем добавлять, редактировать или удалять новый Note объект в нашем приложении. Поэтому, когда вы запускаете проект прямо сейчас, в приложении нет функциональности. Приступим к созданию элементов пользовательского интерфейса для взаимодействия.

Чтобы создать элементы пользовательского интерфейса, нам нужно создать две переменные: alertShowing и editMode. alertShowing - это значение типа Bool, и если это true, объект Alert может появиться. editMode - это значение типа EditMode, и если оно установлено на .active, ячейки объекта List будут доступны для редактирования и удаления.

// MARK: - Properties
@ObservedObject var dataProvider: DataProvider
@State private var alertShowing = false
@State private var editMode: EditMode = .inactive

Наш новый объект, который добавляет и редактирует кнопки, будет находиться в объекте NavigationView. Мы используем модификатор navigationBarItems для размещения кнопок внутри NavigationView:

Неправильно иметь активную кнопку добавления при редактировании объекта List. Вот почему мы преобразуем кнопку добавления в пользовательский объект с именем AddButton, а когда элемент editMode равен .active, он превратится в объект EmptyView:

Таким образом, модификатор navigationBarItems также стал совсем маленьким:

.navigationBarItems(
    leading: EditButton(),
    trailing: AddButton(editMode: $editMode, alertShowing: $alertShowing)
)

Написав модификатор environment в конце объекта List, мы можем сделать объект List редактируемым при щелчке по EditButton:

.listStyle(InsetListStyle())
.environment(\.editMode, $editMode)

Наконец, нам нужно использовать модификаторы onDelete и onMove, чтобы ячейки в объекте списка можно было удалять и редактировать. Здесь мы используем методы move и delete, которые мы написали ранее в параметрах perform.

ForEach(dataProvider.allNotes) { note in
    NoteListCell(note: note)
}
.onDelete(perform: dataProvider.delete)
.onMove(perform: dataProvider.move)

В настоящее время мы можем редактировать и удалять List объектные ячейки, но мы не можем добавить новый Note объект. Вот почему наше приложение сейчас не имеет смысла, поскольку к нему не прикреплен Note объект. Если мы создадим элементы пользовательского интерфейса для добавления новых Note объектов, наше приложение станет тем, что мы хотим.

Мы будем использовать объект Alert, чтобы добавить новый объект Note. Вам это может показаться простым, но в SwiftUI по-прежнему нет Alert объекта с TextField объектами в нем. Следовательно, нам нужно создать собственный объект Alert с UIViewControllerRepresentable.

Первое, что мы сделаем, это создадим новый UIViewController, который аналогичен определению пользовательского UIAlertViewController в UIKit. Для этого вы можете создать файл Swift с именем TextFieldAlertViewController и импортировать фреймворки SwiftUI и Combine.

import SwiftUI
import Combine

После этого мы можем создать класс TextFieldAlertViewController. Код, который мы напишем ниже, поможет нам создать объект UIAlertController:

Создайте метод с именем presentAlertController и напишите этот метод в viewDidAppear:

В качестве нашего настраиваемого объекта Alert, который мы будем использовать в нашем основном объекте View, мы создаем настраиваемый объект пользовательского интерфейса с именем TextFieldAlert:

Мы используем протокол UIViewControllerRepresentable для использования пользовательских элементов UIKit в SwiftUI. Точно так же мы будем использовать его с объектом TextAlert:

Затем создайте элемент View с именем TextFieldWrapper, чтобы определить, что нужно сделать, когда объект TextAlert будет и не будет отображаться в нашем основном объекте View:

Как и в случае с модификатором alert, мы создаем модификатор с именем textAlert, чтобы использовать объект TextAlert как объект Alert в нашем приложении:

Теперь мы можем использовать наш объект TextAlert в нашем приложении с помощью модификатора textFieldAlert:

Наше приложение готово. Теперь мы можем это проверить! Как видите, SwiftUI позволяет нам выполнять множество действий автоматически:

Заключение

Хотя SwiftUI не поддерживает многие функции, благодаря таким инструментам, как UIViewControllerRepresentable, мы можем делать почти все с помощью SwiftUI. Вот почему я думаю, что каждый может разработать собственное инди-приложение с помощью только SwiftUI.

Надеюсь, вам понравилась эта статья. Если у вас есть вопросы, оставьте их в комментариях.

Другие мои статьи: