Redux For SwiftUI

摘自《SwiftUI和Combine编程》---《SwiftUI架构》

Redux For SwiftUI 架构图

Action

创建动作,规定 View 不能直接操作 State,而只能通过发送 Action 的方式,间接改变存储在 Store 中的 State。

// 创建模块的相关动作
enum AppAction {
    case login(email: String, pwd: String)
    case loginDone(result: Result<User, AppError>)
    case logout
    case logoutDone
}

// 外部想改变 State 刷新界面,只能通过调用 action 来实现
Button(settings.accountBehavior.text) {
    self.store.dispatch(
        .login(email: self.settings.email, password: self.settings.password)
    )
}

Store

状态决定用户界面,将所有状态都保存在 Store 中

State

1、为模块创建一个状态机,状态决定用户界面

// 状态机 appState
struct AppState {
    var settings = Settings()
}

extension AppState {
    struct Settings {
        enum AccountBehavior: CaseIterable {
            case register, login
        }
        
        enum Sorting: CaseIterable {
            case id, name, color, favorite
        }
        
        var accountBehavior = AccountBehavior.login
        var email = ""
        var password = ""
        var verifyPassword = ""
        
        // 纯副作用 考虑使用 @propertyWrapped 和 didset
        var sorting: Sorting {
            // UserDefault 无法保存枚举值,只能保存对应的 rawValue,所以在外面包一层映射
            set { sortingRawValue = newValue.rawValue }
            get { Sorting(rawValue: sortingRawValue) ?? .id }
        }
        @UserDefaultStorage(Sorting.id.rawValue, key: "sorting")
        var sortingRawValue: Sorting.RawValue
        @UserDefaultStorage(true, key: "showEnglishName")
        var showEnglishName: Bool
        @UserDefaultStorage(false, key: "showFavoriteOnly")
        var showFavoriteOnly: Bool
        
        @FileStorage(directory: .documentDirectory, fileName: "user.json")
        var loginUser: User?
        var loginRequesting: Bool = false
        var loginError: AppError?
    }
}

2、状态机保存在一个 Store 对象中。

  • Store 声明为 @ObservableObject
  • State 声明为 @Published

这样,根据 SwiftUI 特性,每当 State 有变化,就会抛出通知触发相关界面更新。

class Store: ObservableObject {
    @Published var appState = AppState()
}

外部 View 使用属性:

// 使用需要绑定的值
var settingsBinding: Binding<AppState.Settings> {        
    $store.appState.settings
}
// 仅展示值:
var settings: AppState.Settings {
    store.appState.settings
}

Reduce

3、View 不能直接操作 State,而只能通过发送 Action 的方式,间接改变存储在 Store 中的 State,所以 Store 中需提供与外部交流的接口

func dispatch(_ action: AppAction)
Static func reduce(state: AppState, action: AppAction) -> (AppState, AppCommand?)

Reduce 方法作为操作的核心,接受原有的 State 和发送过来的 Action,生成新的 State 和 command。用新的 State 替换 Store 中原有的状态,并用新状态来驱动更新界面。

  • command 是状态的副作用,一般是由于异步操作带来的,需要在异步操作结束之后执行新的 action 驱动 State 进行再次改变。
  • 如果是同步操作,则 command 为 nil
  • 可以再提供一个 dispatch 方法,进而将 reduce 的职责单一化:只负责分拣出 Command 和新的 State
static func reduce(state: AppState,
            action: AppAction) -> (AppState, AppCommand?) {
    var appState = state
    var appCommand: AppCommand? // 声明可空的 command
        
    // 通过不同的操作,分拣出 command 和新的 state
    switch action {
    case .login(let email, let pwd):
        guard !appState.setting.loginRequesting else {
            break
        }
        appState.setting.loginRequesting = true
        appCommand = LoginCommand(email: email, pwd: pwd)
        
    case .loginDone(result: let result):
        appState.setting.loginRequesting = false
        switch result {
        case .success(let user):
            appState.setting.loginUser = user
        case .failure(let error):
            appState.setting.loginError = error
        }
    
    case .logout:
        guard !appState.setting.loginRequesting,
              let user = appState.setting.loginUser else {
            break
        }
        appState.setting.loginRequesting = true
        appCommand = LogoutCommand(user: user)
        
    case .logoutDone:
        appState.setting.loginRequesting = false
        appState.setting.loginUser = nil
    }
    return (appState, appCommand)
}

dispatch 作为与 View 联系的接口,仅需要接收 action,内部调用reduce,将 OldState 和 action 传入即可。

func dispatch(_ action: AppAction) {
    let result = Store.reduce(state: appState, action: action)
    self.appState = result.0
    if let command = result.1 {
        command.execute(in: self)
    }
}

Command

我们希望 Reducer 具有纯函数特性,但是在实际开发中,我们会遇到非常多带有副作用 (side effect) 的情况:比如在改变状态的同时,需要向磁盘写入文件,或者需要进行网络请求。选择在 Reducer 处理当前 State 和 Action 后,除了返回新的 State 以外,再额外返回一个 Command 值,并让 Command 来执行所需的副作用。

写法:
提供 Command 协议,提供 execute 方法,因为回调结束之后要调用 store 接口改变 state,所以需要与 store 相关联,给 execute 附加一个参数 store

protocol AppCommand {
    func execute(in store: Store)
}
struct LoginCommand: AppCommand {
    let email: String
    let pwd: String
    
    func execute(in store: Store) {
        let token = SubscriptionToken()
        LoginRequest(email: self.email, pwd: self.pwd).publisher
            .sink(receiveCompletion: { complete in
                if case .failure(let error) = complete {
                    // 回调完成后 调用store接口改变状态
                    store.dispatch(.loginDone(result: .failure(error)))
                }
                // 是为了持有token至block结束,避免提前取消
                token.unseal()
            }, receiveValue: { user in
                // 回调完成后 调用store接口改变状态
                store.dispatch(.loginDone(result: .success(user)))
                // WriteUserAppCommand(user: user).execute(in: store)
            })
            .seal(in: token)  // 持有anyCancellable,否则会提前取消
    }
}


struct LogoutCommand: AppCommand {
    let user: User
    
    func execute(in store: Store) {
        let token = SubscriptionToken()
        LogoutRequest(user: user).publisher
            .sink(receiveCompletion: { _ in
                token.unseal()
            }, receiveValue: { _ in
                store.dispatch(.logoutDone)
            })
            .seal(in: token)
    }
}

// 最标准的副作用 Command 写法
// 但是此处使用 loginUser的 @propertyWrapper更为简单
// 纯副作用考虑使用 didset 或者 @propertyWrapper
//struct WriteUserAppCommand: AppCommand {
//    let user: User
//
//    func execute(in store: Store) {
//        try? FileHelper.writeJSON(user,
//                                  to: .documentDirectory,
//                                  fileName: "user.json")
//    }
//}

class SubscriptionToken { // 为了避免提前取消
    var cancellable: AnyCancellable?
    func unseal() {
        cancellable = nil
    }
}

extension AnyCancellable {
    func seal(in token: SubscriptionToken) {
        token.cancellable = self
    }
}

// request
struct LoginRequest {
    let email: String
    let password: String
    
    var publisher: AnyPublisher<User, AppError> {
        Future { promise in
            DispatchQueue.global().asyncAfter(deadline: .now()+1.5) {
                if self.password == "password" {
                    let user = User(email: self.email, favoritePokemonIDs: [])
                    promise(.success(user))
                } else {
                    promise(.failure(.passwordWrong))
                }
            }
        }
        .receive(on: DispatchQueue.main)  // 之后执行的操作放在主线程
        .eraseToAnyPublisher()
    }
}


struct LogoutRequest {
    let user: User
    
    var publisher: AnyPublisher<Void, Never> {
        Future { promise in
            DispatchQueue.global().asyncAfter(deadline: .now()+1) {
                promise(.success(()))
            }
        }
        .receive(on: DispatchQueue.main) // 之后执行的操作放在主线程
        .eraseToAnyPublisher()
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 的创作理念: Web 应用是一...
    yozosann阅读 4,026评论 0 0
  • 学习必备要点: 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理 弄清楚Redux是...
    贺贺v5阅读 12,887评论 10 58
  • 之前利用知乎日报的api写了react-native的一个入门项目,传送文章地址React Native 项目入门...
    wutongke阅读 14,188评论 2 26
  • Redux笔记 参考理解 Redux 中文文档Redux 阮一峰 严格的单向数据流是Rduex设计核心。 Redu...
    oNexiaoyao阅读 3,471评论 0 0
  • 一.引入: react本身是一个非常轻量级的视图层框架,因为组件传值太麻烦了。在react基础上配套一个数据层的框...
    Zlaojie阅读 1,696评论 0 0