Программирование
Однонаправленный поток в Swift
Но как быть с type-safe системой управления состояниями? Как ее построить, используя возможности языка Swift?
На этой неделе я расскажу о подходе к управлению состояниями, который я использую в своих приложениях уже много лет. Мы рассмотрим построение предсказуемой, тестируемой, отлаживаемой и модульной системы управления состояниями в Swift.
Swift способствует созданию type-safe кода, используя набор возможностей языка, позволяющих кодировать правильное поведение в системе типов. Мы стремимся моделировать наши типы таким образом, чтобы логическая ошибка становилась ошибкой времени компиляции, а не времени выполнения. Для этого используются типы значений, перечисления, опции, протоколы, дженерики, фантомные типы и т.д.
Система управления состоянием
Но как быть с type-safe системой управления состояниями? Как ее построить, используя возможности языка Swift? Мы применим те же инструменты для построения функциональной и безопасной системы управления состоянием.
import Observation
@Observable final class Store<State, Action> {
private(set) var state: State
private let reduce: (State, Action) -> State
init(
initialState state: State,
reduce: @escaping (State, Action) -> State
) {
self.state = state
self.reduce = reduce
}
func send(_ action: Action) {
state = reduce(state, action)
}
}
Вот базовый пример системы управления состоянием, которую мы можем сделать в Swift. Мы выражаем тип Store, который определяет два общих типа: один для состояния, другой для действия. Он хранит текущее состояние в переменной, которую мы можем мутировать только внутри типа Store. Он также предоставляет нам функцию send, которая принимает в качестве параметра общее действие и мутирует текущее состояние. Весь тип помечен макросом Observable, что позволяет нам получать уведомления о каждом изменении состояния.
Предсказуемая
Первая цель при построении системы управления состоянием, которую мы хотим достичь, — сделать ее предсказуемой. Как видно из приведенного примера, единственным способом изменения состояния является отправка предопределенного действия. Все представления и контроллеры представления в вашем приложении берут экземпляр типа Store и могут читать только фактическое состояние. Не существует способа изменить что-либо в состоянии напрямую. Единственным способом обновления состояния является отправка действия.
xxxxxxxxxx
struct ShopState: Equatable {
var products: [String] = []
}
enum ShopAction: Equatable {
case add(String)
case remove(String)
}
let reduce: (ShopState, ShopAction) -> ShopState = { state, action in
var newState = state
switch action {
case let .add(product):
newState.products.append(product)
case let .remove(product):
newState.products.removeAll { $0 == product }
}
return newState
}
typealias ShopStore = Store<ShopState, ShopAction>
Функция reduce — это единственное место, содержащее логику обновления состояния. Это не означает, что нужно иметь одну функцию для всего приложения. Вы можете объединить несколько функций reduce в одну. Обычно у меня есть функция reduce для каждого функционального модуля моего приложения.
xxxxxxxxxx
import SwiftUI
struct ShopView: View {
@State private var store = ShopStore(
initialState: .init(products: []),
reduce: reduce
)
var body: some View {
List(store.state.products, id: \.self) { product in
Text(verbatim: product)
.swipeActions {
Button(role: .destructive) {
store.send(.remove(product))
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
}
Как видно из приведенного примера, мы определяем список, в котором отображаются товары. Мы также позволяем удалять товары из списка с помощью свайпов. Представление имеет доступ к состоянию только для чтения и может отображать его как есть. Единственный способ изменить состояние — использовать функцию send с одним из предопределенных в перечислении ShopAction случаев.
Такой подход называется однонаправленным потоком. Как следует из названия, движение осуществляется только в одном направлении. Представление посылает действия, магазин обновляет состояние, а представление принимает обновленное состояние. Применяя этот подход, мы делаем управление состоянием предсказуемым. Вы всегда знаете, где искать мутацию и где находится бизнес-логика вашего приложения.
Тестируемая
Как мы уже говорили, логика приложения находится в функции reduce. Это чистая функция, которая принимает в качестве параметров текущее состояние и действие, которое нужно применить, и возвращает новое состояние. И состояние, и действие являются типами значений. Обычно мы используем структуру для состояния и перечисление для действия. Это означает, что мы можем легко проверить любые функции reduce в модульных тестах.
xxxxxxxxxx
import XCTest
final class ShopReducerTests: XCTestCase {
func testRemove() {
let initialState = ShopState(products: ["p1"])
let newState = reduce(initialState, .remove("p1"))
XCTAssertTrue(newState.products.isEmpty)
}
}
Все, что нам нужно сделать, — это создать начальное состояние и вызвать функцию reduce с определенным действием. Затем мы можем убедиться, что новое состояние, возвращаемое функцией reduce, содержит все необходимые изменения.
С предварительным просмотром
Этот подход отлично работает с предварительным просмотром в Xcode. Можно создать несколько превью с различными начальными состояниями. Например, одно для пустого списка, другое — для списка товаров. При этом нет необходимости в мокировании протоколов, поскольку состояние представляет собой простую структуру, которую можно создать и поместить в хранилище для отображения в представлении.
xxxxxxxxxx
#Preview {
AnotherShopView(
store: .init(
initialState: .init(products: ["Product"]),
reduce: reduce
)
)
}
#Preview {
AnotherShopView(
store: .init(
initialState: .init(products: []),
reduce: reduce
)
)
}
Отлаживаемая
Отладка и логирование стали очень простыми. Если что-то идет не так, вы знаете, где искать. Функция reduce — единственное место, содержащее логику приложения, и это лучшее место для размещения сообщений логов, поскольку все действия проходят через функцию reduce. Это означает, что ваши журналы никогда не пропустят ни одного изменения состояния.
Храня состояние функции в одном месте, мы можем легко отслеживать историю изменения состояния. Это поможет нам понять, какая последовательность действий привела к ошибке. Мы также можем закодировать состояние в JSON и получить его для анализа или восстановить под отладчиком для проверки ошибки.
Модульная
Многие мои коллеги сравнивают однонаправленный поток с single-state контейнером, где все состояние приложения хранится в одном экземпляре определенной структуры AppState. Да, это возможно, но это не единственный путь.
Обычно я определяю хранилище для каждой функции. Таким образом, каждая независимая функция имеет свое собственное хранилище. Это позволяет оптимизировать производительность, поскольку в случае огромного приложения наличие одного хранилища может привести к снижению производительности, когда вся иерархия приложения обновляется при каждом небольшом изменении состояния.
Ссылки
После нескольких лет создания подобных приложений эти идеи вылились в Swift-пакет под названием Swift Unidirectional Flow. Он реализует все обсуждаемые нами идеи в готовом к производству коде, поддерживающем параллелизм и другие возможности, которые могут понадобиться при создании реального приложения.
Я не настаиваю на использовании этого пакета. В частности, не следует импортировать сторонние библиотеки или фреймворки для создания основных функций вашего приложения. Но вы можете использовать его для вдохновения, чтобы построить систему управления состояниями в своем приложении с учетом ваших потребностей.
Заключение
Надеюсь, вам понравилась эта статья. Не стесняйтесь следовать за мной в Twitter и задавать свои вопросы, связанные с этой статьей. Спасибо, что прочитали, и до встречи!
Еще про однонаправленные потоки
- ReSwift: однонаправленный поток данных в Swift
- Workflow: конечные автоматы для UI
- UDF: реализация шаблона Unidirectional Data Flow для iOS
- Реализация Unidirectional Data Flow в супераппе
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.11
-
Новости5 дней назад
Видео и подкасты о мобильной разработке 2025.14
-
Видео и подкасты для разработчиков3 недели назад
Javascript для бэкенда – отличная идея: Node.js, NPM, Typescript
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.12