使用 Swift 5.5 中引入的 async/await 实现 Redux 变得非常简单。 从 Xcode 13.2 开始,Swift 5.5 的新并发特性支持 iOS 13。 因此,现有的 Redux 包基于 async/await 进行了重新实现。
Redux 仅支持 Swift Package Manager。
dependencies: [
.package(url: "https://github.com/ReactComponentKit/Redux.git", from: "1.2.1"),
]
上图显示了 Redux 的流程。 内容很多,但实际上非常简洁。 Store 处理了大部分流程。 开发者所要做的就是定义 State 和 Store,并定义执行 Action 和 Mutation 的函数。 此外,可以定义类似中间件的任务,以便在 Mutation 发生之前或之后执行必要的任务。
State 可以定义如下。
struct Counter: State {
var count = 0
}
请注意,State 应该符合 Equatable。
定义 Store 时,需要一个 State。 您可以如下定义 Store。
struct Counter: State {
var count = 0
}
class CounterStore: Store<Counter> {
init() {
super.init(state: Counter())
}
}
Store 提供以下方法。
创建自定义 Store 时,主要使用的方法是 commit(mutation:, payload:)。 dispatch(action:, payload:) 很少使用。
Mutation 定义为 Store 方法。 Mutation 方法是一个同步方法。
// mutation
private func increment(counter: inout Counter, payload: Int) {
counter.count += payload
}
private func decrement(counter: inout Counter, payload: Int) {
counter.count -= payload
}
Action 也由 Store 的方法定义。 无需再为 Action 创建单独的自定义数据类型。 由于 Action 定义为 Store 方法,因此实际使用 Store 的 dispatch 方法的情况很少。
Action 可以通过将其划分为同步 Action 或异步 Action 来定义。 由于状态更改发生在 commit 中,因此无需为异步处理定义额外的中间件。 您可以在异步 action 中完成异步处理,然后提交更改。
// actions
func incrementAction(payload: Int) {
self.commit(mutation: increment, payload: payload)
}
func decrementAction(payload: Int) {
self.commit(mutation: decrement, payload: payload)
}
func asyncIncrementAction(payload: Int) async {
await Task.sleep(1 * 1_000_000_000)
self.commit(mutation: increment, payload: payload)
}
func asyncDecrementAction(payload: Int) async {
await Task.sleep(1 * 1_000_000_000)
self.commit(mutation: decrement, payload: payload)
}
此外,您可以使用简化的 commit 方法来定义 action 或 mutate 状态。
func asyncIncrementAction(payload: Int) async {
await Task.sleep(1 * 1_000_000_000)
self.commit { mutableState in
mutableState.count += 1
}
}
Store 的 commit
方法是公共的,因此您可以在 UI 层使用它。
Button(action: { store.counter.commit { $0.count += 1 }) {
Text(" + ")
.font(.title)
.bold()
}
或者使用 Store 的 action 方法。
Button(action: { store.counter.incrementAction(payload: 1) }) {
Text(" + ")
.font(.title)
.bold()
}
定义要连接到 View 的属性。 Store 不发布 State。 因此,为了发布 State 的特定属性,可以在 computed 步骤中将值注入到该属性中。
class CounterStore: Store<Counter> {
init() {
super.init(state: Counter())
}
// computed
@Published
var count = 0
override func computed(new: Counter, old: Counter) {
self.count = new.count
}
...
}
到目前为止定义的 CounterStore 的完整代码如下。
import Foundation
import Redux
struct Counter: State {
var count = 0
}
class CounterStore: Store<Counter> {
init() {
super.init(state: Counter())
}
// computed
@Published
var count = 0
override func computed(new: Counter, old: Counter) {
self.count = new.count
}
// mutation
private func increment(counter: inout Counter, payload: Int) {
counter.count += payload
}
private func decrement(counter: inout Counter, payload: Int) {
counter.count -= payload
}
// actions
func incrementAction(payload: Int) {
self.commit(mutation: increment, payload: payload)
}
func decrementAction(payload: Int) {
self.commit(mutation: decrement, payload: payload)
}
func asyncIncrementAction(payload: Int) async {
await Task.sleep(1 * 1_000_000_000)
self.commit(mutation: increment, payload: payload)
}
func asyncDecrementAction(payload: Int) async {
await Task.sleep(1 * 1_000_000_000)
self.commit(mutation: decrement, payload: payload)
}
}
您可以选择添加中间件。 中间件是在所有 Mutation 之前或之后调用的一组函数。
您可以选择添加中间件。中间件是在提交所有 mutations 之前和之后调用的一组同步函数。例如,您可以定义中间件来打印日志以调试状态更改。
class WorksBeforeCommitStore: Store<ReduxState> {
init() {
super.init(state: ReduxState())
}
override func worksBeforeCommit() -> [(ReduxState) -> Void] {
return [
{ (state) in
print(state.count)
}
]
}
}
class WorksAfterCommitStore: Store<ReduxState> {
init() {
super.init(state: ReduxState())
}
override func worksAfterCommit() -> [(ReduxState) -> Void] {
return [
{ (state) in
print(state.count)
}
]
}
}
测试上面定义的 CounterStore 非常容易。
import XCTest
@testable import Redux
final class CounterStoreTests: XCTestCase {
private var store: CounterStore!
override func setUp() {
super.setUp()
store = CounterStore()
}
override func tearDown() {
super.tearDown()
store = nil
}
func testInitialState() {
XCTAssertEqual(0, store.state.count)
}
func testIncrementAction() {
store.incrementAction(payload: 1)
XCTAssertEqual(1, store.state.count)
store.incrementAction(payload: 10)
XCTAssertEqual(11, store.state.count)
}
func testPublisherValue() {
XCTAssertEqual(0, store.count)
store.incrementAction(payload: 1)
XCTAssertEqual(1, store.count)
store.incrementAction(payload: 10)
XCTAssertEqual(11, store.count)
store.decrementAction(payload: 10)
XCTAssertEqual(1, store.count)
store.decrementAction(payload: 1)
XCTAssertEqual(0, store.count)
}
func testAsyncIncrementAction() async {
await store.asyncIncrementAction(payload: 1)
XCTAssertEqual(1, store.state.count)
XCTAssertEqual(1, store.count)
await store.asyncIncrementAction(payload: 10)
XCTAssertEqual(11, store.state.count)
XCTAssertEqual(11, store.count)
}
func testAsyncDecrementAction() async {
await store.asyncDecrementAction(payload: 1)
XCTAssertEqual(-1, store.state.count)
XCTAssertEqual(-1, store.count)
await store.asyncDecrementAction(payload: 10)
XCTAssertEqual(-11, store.state.count)
XCTAssertEqual(-11, store.count)
}
}
让我们定义一个使用 API 的 Store (https://jsonplaceholder.typicode.com)。
import Foundation
import Redux
struct User: Equatable, Codable {
let id: Int
var name: String
}
struct UserState: State {
var users: [User] = []
}
class UserStore: Store<UserState> {
init() {
super.init(state: UserState())
}
// mutations
private func SET_USERS(userState: inout UserState, payload: [User]) {
userState.users = payload
}
private func SET_USER(userState: inout UserState, payload: User) {
let index = userState.users.firstIndex { it in
it.id == payload.id
}
if let index = index {
userState.users[index] = payload
}
}
// actions
func loadUsers() async {
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://jsonplaceholder.typicode.com/users/")!)
let users = try JSONDecoder().decode([User].self, from: data)
commit(mutation: SET_USERS, payload: users)
} catch {
print(#function, error)
commit(mutation: SET_USERS, payload: [])
}
}
func update(user: User) async throws {
let params = try JSONEncoder().encode(user)
var request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/users/\(user.id)")!)
request.httpMethod = "PUT"
request.httpBody = params
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let (data, _) = try await URLSession.shared.data(for: request)
let user = try JSONDecoder().decode(User.self, from: data)
commit(mutation: SET_USER, payload: user)
}
}
您可以如下测试上面的 UserStore。
import XCTest
@testable import Redux
final class UserStoreTests: XCTestCase {
private var store: UserStore!
override func setUp() {
super.setUp()
store = UserStore()
}
override func tearDown() {
super.tearDown()
store = nil
}
func testInitialState() {
XCTAssertEqual([], store.state.users)
}
func testLoadUsers() async {
await store.loadUsers()
XCTAssertEqual(10, store.state.users.count)
for user in store.state.users {
XCTAssertGreaterThan(user.id, 0)
XCTAssertNotEqual(user.name, "")
}
}
func testUpdateUser() async {
do {
await store.loadUsers()
XCTAssertEqual(10, store.state.users.count)
var mutableUser = store.state.users[0]
mutableUser.name = "Sungcheol Kim"
try await store.update(user: mutableUser)
XCTAssertEqual(10, store.state.users.count)
let user = store.state.users[0]
XCTAssertEqual("Sungcheol Kim", user.name)
} catch {
XCTFail("Failed update user")
}
}
}
有必要用单一事实来源在一个地方管理应用程序状态。 在这种情况下,在一个状态中定义应用程序的所有状态是危险的。 因此,建议将状态划分为模块,并创建和管理管理每个状态的 Store。 您可以如下定义 App Store。
import Foundation
import Redux
struct AppState: State {
}
class AppStore: Store<AppState> {
// composition store
let counter = CounterStore()
let users = UserStore()
init() {
super.init(state: AppState())
}
}
您可以如下使用上面的 AppStore。
import XCTest
@testable import Redux
// Single Source of Truth
final class SSOTTests: XCTestCase {
private var store: AppStore!
override func setUp() {
super.setUp()
store = AppStore()
}
override func tearDown() {
super.tearDown()
store = nil
}
func testLoadUsers() async {
await store.users.loadUsers()
XCTAssertEqual(10, store.users.state.users.count)
for user in store.users.state.users {
XCTAssertGreaterThan(user.id, 0)
XCTAssertNotEqual(user.name, "")
}
}
func testUpdateUser() async {
do {
await store.users.loadUsers()
XCTAssertEqual(10, store.users.state.users.count)
var mutableUser = store.users.state.users[0]
mutableUser.name = "Sungcheol Kim"
try await store.users.update(user: mutableUser)
XCTAssertEqual(10, store.users.state.users.count)
let user = store.users.state.users[0]
XCTAssertEqual("Sungcheol Kim", user.name)
} catch {
XCTFail("Failed update user")
}
}
func testIncrementAction() {
store.counter.incrementAction(payload: 1)
XCTAssertEqual(1, store.counter.state.count)
store.counter.incrementAction(payload: 10)
XCTAssertEqual(11, store.counter.state.count)
}
func testPublisherValue() {
XCTAssertEqual(0, store.counter.count)
store.counter.incrementAction(payload: 1)
XCTAssertEqual(1, store.counter.count)
store.counter.incrementAction(payload: 10)
XCTAssertEqual(11, store.counter.count)
store.counter.decrementAction(payload: 10)
XCTAssertEqual(1, store.counter.count)
store.counter.decrementAction(payload: 1)
XCTAssertEqual(0, store.counter.count)
}
func testAsyncIncrementAction() async {
await store.counter.asyncIncrementAction(payload: 1)
XCTAssertEqual(1, store.counter.state.count)
XCTAssertEqual(1, store.counter.count)
await store.counter.asyncIncrementAction(payload: 10)
XCTAssertEqual(11, store.counter.state.count)
XCTAssertEqual(11, store.counter.count)
}
func testAsyncDecrementAction() async {
await store.counter.asyncDecrementAction(payload: 1)
XCTAssertEqual(-1, store.counter.state.count)
XCTAssertEqual(-1, store.counter.count)
await store.counter.asyncDecrementAction(payload: 10)
XCTAssertEqual(-11, store.counter.state.count)
XCTAssertEqual(-11, store.counter.count)
}
}
我们可以像下面这样定义 AppStore,但这不是一个好的设计。 如果您向 AppState 添加更多状态,AppStore 将变成更大的 Store。
struct AppState: State {
var count: Int = 0
var content: String? = nil
var error: String? = nil
}
class AppStore: Store<AppState> {
init() {
super.init(state: AppState())
}
@Published
var count: Int = 0
@Published
var content: String? = nil
@Published
var error: String? = nil
override func computed(new: AppState, old: AppState) {
if (self.count != new.count) {
self.count = new.count
}
if (self.content != new.content) {
self.content = new.content
}
if (self.error != new.error) {
self.error = new.error
}
}
override func worksAfterCommit() -> [(AppState) -> Void] {
return [ { state in
print(state.count)
}]
}
private func INCREMENT(state: inout AppState, payload: Int) {
state.count += payload
}
private func DECREMENT(state: inout AppState, payload: Int) {
state.count -= payload
}
private func SET_CONTENT(state: inout AppState, payload: String) {
state.content = payload
}
private func SET_ERROR(state: inout AppState, payload: String?) {
state.error = payload
}
func incrementAction(payload: Int) {
commit(mutation: INCREMENT, payload: payload)
}
func decrementAction(payload: Int) {
commit(mutation: DECREMENT, payload: payload)
}
func fetchContent() async {
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://#")!)
let value = String(data: data, encoding: .utf8) ?? ""
commit(mutation: SET_ERROR, payload: nil)
commit(mutation: SET_CONTENT, payload: value)
} catch {
commit(mutation: SET_ERROR, payload: error.localizedDescription)
}
}
}
一个好的实践是将状态分解成小块,然后将它们组成一个 store。
/**
* CounterStore.swift
*/
struct Counter: State {
var count: Int = 0
}
class CounterStore: Store<Counter> {
@Published
var count: Int = 0
override func computed(new: Counter, old: Counter) {
if (self.count != new.count) {
self.count = new.count
}
}
init() {
super.init(state: Counter())
}
override func worksAfterCommit() -> [(Counter) -> Void] {
return [ { state in
print(state.count)
}]
}
private func INCREMENT(state: inout Counter, payload: Int) {
state.count += payload
}
private func DECREMENT(state: inout Counter, payload: Int) {
state.count -= payload
}
func incrementAction(payload: Int) {
commit(mutation: INCREMENT, payload: payload)
}
func decrementAction(payload: Int) {
commit(mutation: DECREMENT, payload: payload)
}
}
/**
* ContentStore.swift
*/
struct Content: State {
var value: String? = nil
var error: String? = nil
}
class ContentStore: Store<Content> {
@Published
var value: String? = nil
@Published
var error: String? = nil
override func computed(new: Content, old: Content) {
if (self.value != new.value) {
self.value = new.value
}
if (self.error != new.error) {
self.error = new.error
}
}
init() {
super.init(state: Content())
}
override func worksAfterCommit() -> [(Content) -> Void] {
return [
{ state in
print(state.value ?? "없음")
}
]
}
private func SET_CONTENT_VALUE(state: inout Content, payload: String) {
state.value = payload
}
private func SET_ERROR(state: inout Content, payload: String?) {
state.error = payload
}
func fetchContentValue() async {
do {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://#")!)
let value = String(data: data, encoding: .utf8) ?? ""
commit(mutation: SET_ERROR, payload: nil)
commit(mutation: SET_CONTENT_VALUE, payload: value)
} catch {
commit(mutation: SET_ERROR, payload: error.localizedDescription)
}
}
}
/**
* ComposeAppStore.swift
*/
struct ComposeAppState: State {
// A state that depends on the state of another store.
var allLength: String = ""
}
class ComposeAppStore: Store<ComposeAppState> {
let counter = CounterStore();
let content = ContentStore();
// Set it to private to access counter.count with the counter namespace in the UI layer.
@Published
private var count = 0;
@Published
private var contentValue: String? = nil;
@Published
private var error: String? = nil;
@Published
var allLength: String? = nil;
override func computed(new: ComposeAppState, old: ComposeAppState) {
if (new.allLength != old.allLength) {
self.allLength = new.allLength
}
}
init() {
super.init(state: ComposeAppState())
// @Published chaining is required.
counter.$count.assign(to: &self.$count)
content.$value.assign(to: &self.$contentValue)
content.$error.assign(to: &self.$error)
}
//Examples of actions and state mutations that depend on the state and actions of other stores are
private func SET_ALL_LENGTH(state: inout ComposeAppState, payload: String) {
state.allLength = payload
}
func someComposeAction() async {
await content.fetchContentValue()
commit(mutation: SET_ALL_LENGTH, payload: "counter: \(counter.state.count), content: \(content.state.value?.count ?? 0)")
}
}
/**
* ContentView.swift
*/
import SwiftUI
struct ContentView: View {
@EnvironmentObject
private var store: ComposeAppStore
var body: some View {
VStack {
Text("\(store.counter.count)")
.font(.title)
.bold()
.padding()
if let error = store.content.error {
Text("Error! \(error)")
}
HStack {
Spacer()
Button(action: { store.counter.decrementAction(payload: 1) }) {
Text(" - ")
.font(.title)
.bold()
}
Spacer()
Button(action: { store.counter.incrementAction(payload: 1) }) {
Text(" + ")
.font(.title)
.bold()
}
Spacer()
}
VStack {
Button(action: {
Task {
await store.someComposeAction()
}
}) {
Text("All Length")
.bold()
.multilineTextAlignment(.center)
}
Text(store.allLength ?? "")
.foregroundColor(.red)
.font(.system(size: 12))
.lineLimit(5)
Button(action: {
Task {
await store.content.fetchContentValue()
}
}) {
Text("Fetch Content")
.bold()
.multilineTextAlignment(.center)
}
Text(store.content.value ?? "")
.foregroundColor(.red)
.font(.system(size: 12))
.lineLimit(5)
}
}
.padding(.horizontal, 100)
}
}
版权所有 (c) 2021 Redux, ReactComponentKit
特此授予任何人免费获得本软件和相关文档文件(“软件”)副本的许可,以不受限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售本软件副本的权利,并允许向为此提供软件的人员提供以下条件
上述版权声明和本许可声明应包含在本软件的所有副本或重要部分中。
本软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、适用于特定目的和不侵权的保证。 在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论是在合同、侵权或其他方面,由软件或使用或其他与软件的交易引起的或与之相关的。