Как стать автором
Обновить

Моделирование состояния приложения с использованием объектов Store в SwiftUI

Время на прочтение 4 мин
Количество просмотров 6.1K
Автор оригинала: Majid Jabrayilov
На этой неделе я хочу поговорить о моделировании слоя данных в SwiftUI. Я уже закончил работу над своим самым первым приложением, которое я создаю используя только SwiftUI. Теперь я могу поделиться способом создания слоя модели с использованием объектов Store, которые я использовал при разработке приложения NapBot.

Объект 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“.

todos-screenshots

Заключение


В данной статье обсуждался способ моделирования состояния приложения с использованием нескольких объектов store. Мне очень нравится простота этого подхода и то, как легко можно масштабировать свое приложение, добавляя больше объектов store. Я надеюсь, вам понравилась данная статья.
Теги:
Хабы:
+5
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн