В последнее время я больше занимался фронтенд разработкой, чем мобильной, и я столкнулся с некоторыми очень интересными паттернами проектирования, которые я уже знал, но на самом деле не углублялся в них… до сих пор.
Но теперь все это имеет смысл, после использования из в разработки на React в течение нескольких недель, я теперь не могу вернуться к своим старым способам разработки под iOS. Я не буду переходить на javascript (AKA React Native) для разработки мобильных приложений, но вот кое-что, чему я научился.
Вернувшись к разработке под iOS, я создал новый проект и начал исследовать ReSwift, это реализация паттерна Flux и Redux в Swift. И это довольно просто работает, я несколько раз клонировал архитектуру JavaScript приложении, теперь у меня есть глобальное состояние, и мои контроллеры просто слушают это состояние. Сами контроллеры состоят из различных компонентов представления, которые инкапсулируют очень специфическое поведение.
Все изменения state делаются в одном месте, в reducer. Один на подсостояние. Вы можете увидеть все actions в одном месте. Нет больше сетевого кода или вызова контроллеров, также больше нет мутаций объектов в представлениях. Нет больше спагетти кода. Имеется только одно state, и это правда, тогда ваши различные компоненты представления (и я настаиваю на этом) подписываются на различные части state и реагируют соответственно. Это просто лучшая архитектура для приложения с сильным уровнем модели.
Для примера. Раньше контроллеры представления Login были наполнены множеством строк кода, различным состоянием управления, обработкой ошибок и т. д… Теперь это выглядит так: (В качестве примера)
import UIKit
import Base
import ReSwift
class LoginViewController: UIViewController {
@IBOutlet var usernameField: UITextField!
@IBOutlet var passwordField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
store.subscribe(self) {state in
state.usersState
}
}
@IBAction func onLoginButton(_ sender: Any) {
store.dispatch(AuthenticatePassword(username: usernameField.text!, password: passwordField.text!))
}
@IBAction func onTwitterButton(_ sender: Any) {
store.dispatch(AuthenticateTwitter())
}
@IBAction func onFacebookButton(_ sender: Any) {
store.dispatch(AuthenticateFacebook(from: self))
}
}
// MARK: - State management
extension LoginViewController: StoreSubscriber {
func newState(state: UsersState) {
if let error = state.authState.error {
presentError(error: error.type, viewController: self, completion: nil)
}
if let _ = state.getCurrentUser {
self.dismiss(animated: true, completion: nil)
}
}
}
Контроллеры и представления dispatch действия в глобальный state, эти действия фактически выполняют работу с сетью или запускают различные части, которые вашему приложению необходимо будет преобразовать в новое состояние/state.
Action может вызвать другой action, это то, как происходит для сетевого запроса, например, у вас есть одно действие FetchUser(id: String) и одно действие, которое вы перехватите в reducer, которое выглядит подобно SetUser(user: User). В reducer вы несете ответственность за merge/слияние нового объекта с вашим текущим состоянием.
Сначала нужно state, мой пример будет сосредоточен вокруг объекта User, поэтому state может выглядеть примерно так:
struct UsersState {
var users: [String: User] = [:]
}
Необходимо иметь файл, который инкапсулирует все сетевые действия для объекта пользователя
struct FetchUser: Action {
init(user: String) {
GETRequest(path: "users/(user)").run { (response: APIResponse<UserJSON>) in
store.dispatch(SetUser(user: response.object))
}
}
}
Как только запрос выполнен, он вызывает другой action, это действие на самом деле пустое, на него следует ссылаться, например, в UsersActions. Это действие описывает результат, на который должен полагаться reducer, чтобы изменить state.
struct SetUser: Action {
let user: UserJSON?
}
И, наконец, самая важная работа выполняются в UsersReducer, необходимо поймать action и выполнить некоторую работу в соответствии с его содержанием:
func usersReducer(state: UsersState?, action: Action) -> UsersState {
var state = state ?? initialUsersState()
switch action {
case let action as SetUser:
if let user = action.user {
state.users[user.id] = User(json: user)
}
default:
break
}
return state
}
Теперь все, что необходимо, это suscribe/подписаться на состояние в контроллерах или представлениях, а когда оно изменится, извлечь нужную информацию и получить новые значения!
class UserViewController: UIViewController {
var userId: String? {
didSet {
if let id = userId {
store.dispatch(FetchUser(user: id))
}
}
}
var user: User? {
didSet {
if let user = user {
setupViewUser(user: user)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
store.subscribe(self) {state in
state.usersState
}
}
func setupViewUser(user: User) {
//Do uour UI stuff.
}
}
extension UserViewController: StoreSubscriber {
func newState(state: UsersState) {
self.user = state.users[userId!]
}
}
Но теперь следует взглянуть на примеры ReSwift для более глубокого пони мания, я планирую опубликовать приложение с открытым исходным кодом (на самом деле игра ) с использованием этого паттерна проектирования. Но пока код отображает очень грубое представление о том, как это все вместе работает.
Это еще очень ранняя архитектура в Glose books, но мы не можем дождаться, когда приложение будет запущено в производство с использованием данной архитектуры.
Я чувствую, что разработка приложений с использованием этого паттерна сэкономит много времени и силы. Это займет немного больше работы, чем тупо простой REST-клиент, потому что внутри состояния клиента будет немного больше логики, но в итоге это сэкономит вам бесценное время для отладки. Вы сможете изменять многие элементы локально, и больше не будет каскадных изменений между контроллерами и представлениями. Воспроизведите состояние в резервном порядке, заархивируйте его, создайте промежуточное программное обеспечение и т. д. Поток данных приложения понятен, централизован и прост.
Паттерн Redux добавляет немного структуры в приложение. Я очень давно занимаюсь чистым MVC, уверен, что вы можете создать чистую кодовую базу, но вы склонны развивать привычки, которые часто приносят больше вреда, чем пользы. Вы даже можете сделать еще один шаг и полностью реализовать Redux, управляя своим пользовательским интерфейсом (таким как контроллеры представления, представление предупреждений, контроллеры маршрутизации) в отдельном состоянии, но я еще не достиг всего этого).
И тесты… Unit тестирование теперь легко реализуемо, потому что все, что нужно протестировать, — это сравнить данные которые вы вводите с данными которые содержаться в глобальном state, поэтому тесты могут отправлять mock actions, а затем проверять, соответствует ли состояние тому, что вы хотите.
Серьезно, это будущее. Будущее за Redux :)
Автор: yarmolchuk