На этой неделе я хочу поговорить о моделировании слоя данных в SwiftUI. Я уже закончил работу над своим самым первым приложением, которое я создаю используя только SwiftUI. Теперь я могу поделиться способом создания слоя модели с использованием объектов Store, которые я использовал при разработке приложения NapBot.
Объекты Store, отвечают за сохранение состояния и предоставлении действии по его изменению. У вас может быть столько объектов Store, сколько вам необходимо, желательно чтобы они были простыми и отвечали за небольшую часть состояния вашего приложения. Например, у вас может быть SettingsStore для сохранения состояния пользовательских настроек и TodoStore для сохранения пользовательских задач.
Чтобы создать объект Store, необходимо создать класс, который соответствует протоколу ObservableObject. Протокол ObservableObject позволяет SwiftUI наблюдать и реагировать на изменения данных. Чтобы узнать больше о ObservableObject, взгляните на статью "Управление потоком данных в SwiftUI". Давайте посмотрим на простой пример объекта SettingsStore.
В приведенном выше примере кода у нас есть класс SettingsStore, который предоставляет доступ к пользовательским настройкам. Мы также используем didChangeNotification, чтобы уведомлять SwiftUI всякий раз, когда пользователь изменяет настройки по умолчанию.
Давайте рассмотрим еще одно использование объекта store, создав простое приложение Todo. Нам необходимо создать объект store, который хранит список задач и предоставляет действия для их изменения, например их удаление и фильтрацию.
Здесь имеется класс TodosStore, который соответствует протоколу ObservableObject. TodosStore предоставляет несколько действий для изменения своего состояния, мы можем использовать эти методы из наших views. По умолчанию SwiftUI обновляет view при каждом изменении поля @Published. Вот почему массив элементов Todo обозначен, как @Published. Как только мы добавим или удалим элементы из этого массива, SwiftUI обновит view, подписанные на TodosStore.
Теперь можно создать view, которое отобразит список задач и такие действия, как пометка задачи как выполненной, удаление и изменение порядка отображения задач. Давайте начнем с создания view, которое отображает заголовок задачи и переключатель, чтобы отметить задачу как выполненная.
В приведенном выше примере был использован Binding для предоставления ссылки, например доступ к типу значения. Другими словами, предоставим доступ к записи для элемента todo. TodoItemView не владеет экземпляром структуры Todo, но у него есть доступ к записи в TodoStore посредством Binding.
Теперь у нас имеется TodosView — элемент, который использует компонент List для отображения задач. Компонент List также обеспечивает изменение порядка отображения и удаления. Еще одна интересная вещь — это функция indexed(). Эта функция возвращает коллекцию элементов с ее индексами. Мы используем ее для доступа к элементам в store посредством Binding. Вот полный источник этого расширения.
Environment (Окружающая среда) является идеальным кандидатом для хранения объектов store. Environment может разделить их между несколькими представлениями без явного внедрения посредством метода init. Чтобы узнать больше о преимуществах Environment в SwiftUI, взгляните на статью “Возможности Environment в SwiftUI“.
В данной статье обсуждался способ моделирования состояния приложения с использованием нескольких объектов store. Мне очень нравится простота этого подхода и то, как легко можно масштабировать свое приложение, добавляя больше объектов store. Я надеюсь, вам понравилась данная статья.
Объект Store
Объекты Store, отвечают за сохранение состояния и предоставлении действии по его изменению. У вас может быть столько объектов Store, сколько вам необходимо, желательно чтобы они были простыми и отвечали за небольшую часть состояния вашего приложения. Например, у вас может быть SettingsStore для сохранения состояния пользовательских настроек и TodoStore для сохранения пользовательских задач.
Чтобы создать объект Store, необходимо создать класс, который соответствует протоколу ObservableObject. Протокол ObservableObject позволяет SwiftUI наблюдать и реагировать на изменения данных. Чтобы узнать больше о ObservableObject, взгляните на статью "Управление потоком данных в SwiftUI". Давайте посмотрим на простой пример объекта SettingsStore.
import Foundation
import Combine
final class SettingsStore: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
@UserDefault(Constants.UserDefaults.sleepGoal, defaultValue: 8.0)
var sleepGoal: Double
@UserDefault(Constants.UserDefaults.notifications, defaultValue: true)
var isNotificationsEnabled: Bool
private var didChangeCancellable: AnyCancellable?
override init() {
super.init()
didChangeCancellable = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () }
.receive(on: DispatchQueue.main)
.subscribe(objectWillChange)
}
}
В приведенном выше примере кода у нас есть класс SettingsStore, который предоставляет доступ к пользовательским настройкам. Мы также используем didChangeNotification, чтобы уведомлять SwiftUI всякий раз, когда пользователь изменяет настройки по умолчанию.
Расширенное использование
Давайте рассмотрим еще одно использование объекта store, создав простое приложение Todo. Нам необходимо создать объект store, который хранит список задач и предоставляет действия для их изменения, например их удаление и фильтрацию.
import Foundation
import Combine
struct Todo: Identifiable, Hashable {
let id = UUID()
var title: String
var date: Date
var isDone: Bool
var priority: Int
}
final class TodosStore: ObservableObject {
@Published var todos: [Todo] = []
func orderByDate() {
todos.sort { $0.date < $1.date }
}
func orderByPriority() {
todos.sort { $0.priority > $1.priority }
}
func removeCompleted() {
todos.removeAll { $0.isDone }
}
}
Здесь имеется класс TodosStore, который соответствует протоколу ObservableObject. TodosStore предоставляет несколько действий для изменения своего состояния, мы можем использовать эти методы из наших views. По умолчанию SwiftUI обновляет view при каждом изменении поля @Published. Вот почему массив элементов Todo обозначен, как @Published. Как только мы добавим или удалим элементы из этого массива, SwiftUI обновит view, подписанные на TodosStore.
Теперь можно создать view, которое отобразит список задач и такие действия, как пометка задачи как выполненной, удаление и изменение порядка отображения задач. Давайте начнем с создания view, которое отображает заголовок задачи и переключатель, чтобы отметить задачу как выполненная.
import SwiftUI
struct TodoItemView: View {
let todo: Binding<Todo>
var body: some View {
HStack {
Toggle(isOn: todo.isDone) {
Text(todo.title.wrappedValue)
.strikethrough(todo.isDone.wrappedValue)
}
}
}
}
В приведенном выше примере был использован Binding для предоставления ссылки, например доступ к типу значения. Другими словами, предоставим доступ к записи для элемента todo. TodoItemView не владеет экземпляром структуры Todo, но у него есть доступ к записи в TodoStore посредством Binding.
import SwiftUI
struct TodosView: View {
@EnvironmentObject var store: TodosStore
@State private var draft: String = ""
var body: some View {
NavigationView {
List {
TextField("Type something...", text: $draft, onCommit: addTodo)
ForEach(store.todos.indexed(), id: \.1.id) { index, _ in
TodoItemView(todo: self.$store.todos[index])
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle("Todos")
}
}
private func delete(_ indexes: IndexSet) {
store.todos.remove(atOffsets: indexes)
}
private func move(_ indexes: IndexSet, to offset: Int) {
store.todos.move(fromOffsets: indexes, toOffset: offset)
}
private func addTodo() {
let newTodo = Todo(title: draft, date: Date(), isDone: false, priority: 0)
store.todos.insert(newTodo, at: 0)
draft = ""
}
}
Теперь у нас имеется TodosView — элемент, который использует компонент List для отображения задач. Компонент List также обеспечивает изменение порядка отображения и удаления. Еще одна интересная вещь — это функция indexed(). Эта функция возвращает коллекцию элементов с ее индексами. Мы используем ее для доступа к элементам в store посредством Binding. Вот полный источник этого расширения.
import Foundation
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Environment (Окружающая среда) является идеальным кандидатом для хранения объектов store. Environment может разделить их между несколькими представлениями без явного внедрения посредством метода init. Чтобы узнать больше о преимуществах Environment в SwiftUI, взгляните на статью “Возможности Environment в SwiftUI“.
Заключение
В данной статье обсуждался способ моделирования состояния приложения с использованием нескольких объектов store. Мне очень нравится простота этого подхода и то, как легко можно масштабировать свое приложение, добавляя больше объектов store. Я надеюсь, вам понравилась данная статья.