iOS meets Redux
I started to lean Redux architecture recently. Redux is super simple and easy to understand.
I want to user Redux architecture in iOS development, and I found ReSwift.
ReSwift is the powerful framework to create Redux architecture iOS app. I created a sample app using ReSwift with CoreData.
Inside the sample app
There is only one entity named ManagedUser
.
The sample is a simple application to add and update users.
The key point of Redux is immutable state.
But NSManagedObject
is not value type, so direct manipulation of the instance mutates the state.
To avoid mutation, I created User
struct and mapped ManagedUser
to User
.
public class ManagedUser: NSManagedObject {
func toUser() -> User {
return User(objectID: self.objectID.uriRepresentation().absoluteString, name: self.name ?? "", age: Int(self.age))
}
...
}
struct User {
var objectID: String?
var name: String
var age: Int
init(name: String, age: Int) {
self.init(objectID: nil, name: name, age: age)
}
init(objectID: String?, name: String, age: Int) {
self.objectID = objectID
self.name = name
self.age = age
}
}
State, Action, Reducer, and Store
There are four key component in Redux. State, Action, Reducer, and Store.
To understand Redux, read ReSwift document or Redux document.
State
In this sample, I created one State named AppState
.
AppState
has all users.
struct AppState: StateType {
var users: [User] = []
}
Action
There are three actions, FetchUser
AddUser
, UpdateUserName
and UpdateUserAge
.
struct FetchUser: Action {
let users: [User]
}
struct AddUser: Action {
let user: User
}
struct UpdateUserName: Action {
let objectID: String
let name: String
}
struct UpdateUserAge: Action {
let objectID: String
let age: Int
}
Reducer
AppReducer
handles actions and create new state.
struct AppReducer: Reducer {
func handleAction(action: Action, state: AppState?) -> AppState {
var state = state ?? AppState()
switch action {
case let action as FetchUser:
state.users = action.users
case let action as AddUser:
state.users.append(action.user)
case let action as UpdateUserName:
let users = state.users.map({ user -> User in
if let objectID = user.objectID, objectID == action.objectID {
return User(objectID: objectID, name: action.name, age: user.age)
}
return user
})
state.users = users
case let action as UpdateUserAge:
let users = state.users.map({ user -> User in
if let objectID = user.objectID, objectID == action.objectID {
return User(objectID: objectID, name: user.name, age: action.age)
}
return user
})
state.users = users
default:
break
}
return state
}
}
Store
In AppDelegate
, the store is initialized.
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var mainStore: Store<AppState>!
var userRepository: UserRepository!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
userRepository = UserRepository(context: persistentContainer.viewContext)
self.mainStore = Store<AppState>(reducer: AppReducer(), state: AppState())
return true
}
...
ViewControllers
There are two view controllers in this app, TableViewController
and DetailViewController
.
TableViewController
shows all users.
DetailViewController
shows selected user info and we can edit user info.
Both view controllers subscribe to mainStore in viewWillAppear(_ animated: Bool)
, unsubscribe from mainStore in viewWillDisappear(_ animated: Bool)
, and conform to StoreSubscriber
protocol.
After reducers handle actions, newState(state: AppState)
function is called. So we can update UI here depend on the state.
TableViewController
TableViewController
has all users in users property.
In viewDidLoad
, TableViewController
fetches all users from db.
override func viewDidLoad() {
super.viewDidLoad()
self.userRepository = appDelegate.userRepository
self.userRepository.fetchUsers(completionHandler: { users, error in
if error == nil {
appDelegate.mainStore.dispatch(FetchUser(users: users!))
}
})
}
Inside newState(state: AppState)
, users update to the latest state, and tableView is reloaded.
TableViewController
has a right bar button item to add a new user.
When the button tapped, a new user is created and AddUser
action is dispatched.
@IBAction func addButtonDidTap(_ sender: AnyObject) {
let user = User(name: "Albert Einstein", age: 78)
self.userRepository.createUser(user: user, completionHandler: { user, error in
if error == nil {
//Dispatch Action
appDelegate.mainStore.dispatch(AddUser(user: user!))
}
})
}
When the row is selected, TableViewController
shows DetailViewController
.
In prepare(for segue: UIStoryboardSegue, sender: Any?)
, user is injected to DetailViewController
.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "Show" {
let vc = segue.destination as! DetailViewController
vc.user = self.users[self.tableView.indexPathForSelectedRow!.row]
vc.userRepository = self.userRepository
}
}
DetailViewController
DetailViewController
has two textfield, nameTextField
and ageTextField
.
When nameTextField
is edited, UpdateUserName
action is dispatched, and when ageTextField
is edited, UpdateUserAge
action is dispatched.
@IBAction func nameTextFieldEditingChanged(_ sender: UITextField) {
appDelegate.mainStore.dispatch(UpdateUserName(objectID: self.user.objectID!, name: sender.text ?? ""))
}
@IBAction func ageTextFieldEditingChanged(_ sender: UITextField) {
let age = Int(sender.text!) ?? -1
appDelegate.mainStore.dispatch(UpdateUserAge(objectID: self.user.objectID!, age: age))
}
If nameTextField
is empty or ageTextField
is incorrect, save button is disabled.
func newState(state: AppState) {
guard let currentUser = state.users.filter({
$0.objectID == self.user.objectID
}).first else {
return
}
self.user = currentUser
if self.user.name == "" || self.user.age < 0 {
self.saveButton.isEnabled = false
} else {
self.saveButton.isEnabled = true
}
self.nameTextField.text = self.user.name
self.ageTextField.text = self.user.age < 0 ? "" : "\(self.user.age)"
}
The Final Project
Please check my GitHub repository.
Next…
I want to integrate Redux with UndoManager.
エンジニアリング iOS Swift Xcode Redux ReSwift CoreData
関連記事
-
2025/3/28
Macのストレージに空き容量があるのに、実際には空き容量がない状態になっているとき
Mac Storage
-
2024/3/24
try! Swift Tokyo 2024
try! Swift Tokyo 2024の参加メモ
Swift
-
2023/1/31
SwiftUIでTabBarを非表示にする
SwiftUI TabBar
-
2023/1/14
Bootstrap5.3で追加されたダークモードに対応してみました
Bootstrap DarkMode
-
2021/2/17
brew upgradeで Your CLT does not support macOS 11というエラーがでた
homebrew macOS Big Sur
-
2020/12/1
Apple Silicon搭載 MacBook Pro M1で Homebrewとrbenvをインストールする
先日購入したApple Silicon搭載 MacBook Pro M1にこのブログをビルドするためにHomebrewとrbenvをインストールしたのでメモ。
Apple Silicon Homebrew rbenv
-
2020/11/27
Apple Silicon搭載のMacBook Pro M1が危うく文鎮化するところだったので記録
Apple Silicon搭載のMacBook Pro 13インチを仕事用に購入したのですが、クリーンインストールしたところ文鎮化しかけたので記録しておきます。
MacBook Pro Apple Apple Silicon M1
-
2020/6/23
Platform State of the Unionまとめ
Platform State of the Unioまとめ
WWDC2020 Apple アップル iOS macOS watchOS tvOS
-
2020/6/23
WWDC2020キーノートまとめ
WWDC2020 キーノートまとめ
WWDC2020 Apple アップル iOS macOS watchOS tvOS
-
2020/5/31
GitHub Pagesで無料ブログを作成する - Part4 自分のオリジナルのテーマを作る
Jekyllでは既存のテーマを使うこともできるのですが、さらに一歩踏み込んで、自分で既存のテーマをカスタマイズしたり、テーマを作ることもできます。
GitHub Pages ブログ