Writing iOS Applications Using the Redux Pattern

image






Recently, I’ve been more involved in front-end development than in mobile, and I came across some very interesting design patterns that I already knew, but didn’t really go into them ... until now.



But now all this makes sense, after using from development in React for several weeks, I can’t now return to my old iOS development methods. I will not switch to javascript (AKA React Native) for developing mobile applications, but here are some things I learned.



image






Returning to iOS development, I created a new project and started exploring ReSwift , this is an implementation of the Flux and Redux pattern in Swift. And it works quite simply, I cloned the architecture of the JavaScript application several times, now I have a global state, and my controllers just listen to this state. The controllers themselves are made up of various presentation components that encapsulate very specific behavior.



image



All state changes are made in one place, in reducer . One for the substate. You can see all the actions in one place. No more network code or calling controllers, no more mutations of objects in views. No more spaghetti code. There is only one state , and it is true, then your various presentation components (and I insist on it) subscribe to different parts of state and react accordingly. This is simply the best architecture for a strong model application.



For example. Previously, login view controllers were filled with a lot of lines of code, various control states, error handling, etc. ... Now it looks like this: (As an example)



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) } } }
      
      





Controllers and representations of dispatch actions in the global state, these actions actually work with the network or launch various parts that your application will need to convert to a new state.



An action can trigger another action, this is how it happens for a network request, for example, you have one FetchUser action (id: String) and one action that you intercept in a reducer that looks like SetUser (user: User). In reducer, you are responsible for merging / merging a new object with your current state.



First you need state , my example will focus around the User object, so state might look something like this:



 struct UsersState { var users: [String: User] = [:] }
      
      





You must have a file that encapsulates all network activities for the user object.



 struct FetchUser: Action { init(user: String) { GETRequest(path: "users/\(user)").run { (response: APIResponse<UserJSON>) in store.dispatch(SetUser(user: response.object)) } } }
      
      





Once the request is completed, it calls another action , this action is actually empty, it should be referenced, for example, in UsersActions. This action describes the result that reducer must rely on to change state.



 struct SetUser: Action { let user: UserJSON? }
      
      





And finally, the most important work is done in UsersReducer , you need to catch the action and do some work in accordance with its contents:



 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 }
      
      





Now all that is needed is suscribe / subscribe to the state in controllers or views, and when it changes, extract the necessary information and get new values!



 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!] } }
      
      





But now you should take a look at the ReSwift examples for a deeper understanding, I plan to publish an open source application (actually a game) using this design pattern. But for now, the code displays a very crude idea of ​​how this all works together.



This is still a very early architecture in Glose books, but we cannot wait for the application to be put into production using this architecture.



I feel that developing applications using this pattern will save a lot of time and effort. It will take a little more work than a stupidly simple REST client , because there will be a bit more logic inside the client state, but in the end it will save you invaluable time for debugging. You will be able to modify many elements locally, and there will no longer be cascading changes between controllers and views. Reproduce the state in backup order, archive it, create middleware, etc. The application data stream is clear, centralized, and simple.



The Redux pattern adds a bit of structure to the application. I have been doing pure MVC for a very long time, I am sure that you can create a clean code base, but you tend to develop habits that often do more harm than good. You can even take one more step and fully implement Redux by controlling your user interface (such as view controllers, alert viewers, routing controllers) in a separate state, but I have not yet achieved all this).



And the tests ... Unit testing is now easy to implement, because all you need to test is to compare the data that you enter with the data that is contained in the global state, so the tests can send mock actions, and then check whether the state matches what you want to.



Seriously, this is the future. The future is for Redux :)



All Articles