前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用Async/Await重建SwiftU的Redux-like状态容器

用Async/Await重建SwiftU的Redux-like状态容器

作者头像
东坡肘子
发布2022-07-28 12:52:40
1.8K0
发布2022-07-28 12:52:40
举报
文章被收录于专栏:肘子的Swift记事本

用Async/Await重建SwiftU的Redux-like状态容器

本文介绍了如何使用Swift 5.5版本的Async/Await功能重构SwiftUI的状态容器代码。

经过两年多的时间,SwiftUI发展到当前的3.0版本,无论SwiftUI的功能还是Swift语言本身在这段时间里都有了巨大的提升。是时候使用Async/Await来重构我的的状态容器代码了。

SwiftUI的状态容器

我是从王巍的SwiftUI与Combine编程[1]一书中,第一次接触到Single souce of truth式的编程思想。整体来说,同Redux的逻辑基本一致:

•将App当做状态机,UI是App状态(State)的具体呈现。•State(值类型)被保存在一个Store对象当中,为了在视图中注入方便,Store需符合ObservableObject协议,且为State设置@Published属性包装,保证State的任何变化都将被及时响应。•View不能直接修改State,只能通过发送Action间接改变Store中的State内容•Store中的Reducer负责处理收到的Action,并按照Action的要求变更State

Redux1

通常,对State、Store和Action的定义如下:

代码语言:javascript
复制
struct AppState {    var name: String = ""    var age:Int = 10}enum AppAction {    case setName(name:String)    case setAge(age:Int)}final class Store: ObservableObject {    @Published private(set) var state: AppState      func dispatch(action:Action) {        reducer(action)    }      func reducer(action) }

Reducer在处理Action时,经常会面对带有副作用(side effect)的情况,比如:

•需从网络查询获得数据后,根据数据修改State•修改State后,需要向磁盘或数据库写入数据等

我们无法控制副作用的执行时间(有长有短),并且副作用还可能会通过Action继续来改变State。

对状态(State)的修改必须在主线程上进行,否则视图不会正常刷新。

我们构建的状态容器(Store)需要满足处理上述情况的能力。

1.0版本

在编写健康笔记1.0[2]时,我采用了SwiftUI与Combine编程[3]一书中提出的解决方式。

对于副作用采用从Reducer中返回Command的方式来处理。Command采用异步操作,将返回结果通过Combine回传给Store。

代码语言:javascript
复制
struct LoginAppCommand: AppCommand {  //...  func execute(in store: Store) {    //...    .sink(      receiveCompletion: { complete in        if case .failure(let error) = complete {          store.dispatch(            .accountBehaviorDone(result: .failure(error))          )        }      },      receiveValue: { user in        store.dispatch(          .accountBehaviorDone(result: .success(user))        )      }    )  }}
代码语言:javascript
复制
func reduce(  state: AppState,   action: AppAction) -> (AppState, AppCommand?) {  // ...  case .accountBehaviorDone(let result):    // 1    appState.settings.loginRequesting = false    switch result {    case .success(let user):      // 2      appState.settings.loginUser = user    case .failure(let error):      // 3      print("Error: \(error)")    }  }    return (appState, appCommand)}

采用了如下的方式保证了State只能在主线程上进行修改:

代码语言:javascript
复制
    func dispatch(_ action: AppAction) {        let result = reduce(state: appState, action: action)        if Thread.isMainThread {            state = result.0        } else {            DispatchQueue.main.async { [weak self] in                self?.state = result.0            }        }        if let command = result.1 {            command.execute(in: self)        }    }

作者自己在书中也说上述代码属于试验性质,因此尽管完全胜任Store的工作,但是从逻辑组织上还是比较复杂,尤其对于每个Command的处理十分的繁琐。

2.0版本

通过阅读、学习Majid的文章Redux-like state container in SwiftUI[4],在健康笔记[5]2.0中,我重构了Store的代码。

Majid的实现方式最大的提升在于,大大简化了副作用代码的复杂度,将原本需要在副作用中处理的Publisher生命周期管理集中到了Store中。并且使用Combine提供的线程调度,保证了只在主线程上修改State。

代码语言:javascript
复制
    func dispatch(_ action: AppAction) {        let effect = reduce(&state, action, environment)        var didComplete = false        let uuid = UUID()        let cancellable = effect            .receive(on: DispatchQueue.main)            .sink(                receiveCompletion: { [weak self] _ in                    didComplete = true                    self?.effectCancellables[uuid] = nil                },                receiveValue: { [weak self] in self?.send($0) }            )        if !didComplete {            effectCancellables[uuid] = cancellable        }    }

Reducer

代码语言:javascript
复制
    private let reduce: Reducer<AppState, AppAction, AppEnvironment> = Reducer { state, action, environment in        switch action {        case .editMemo(let memo, let newMemoViewModel):            return environment.dataHandler.editMemo(memo: memo, newMemoViewModel: newMemoViewModel)        case .setSelection(let selection):            state.selection = selection        }     return Empty(completeImmediately: true)            .eraseToAnyPublisher()            }                                                                          

副作用代码

代码语言:javascript
复制
func editNote(note: Note, newNoteViewModel: NoteViewModel) -> AnyPublisher<AppAction, Never> {        _ = _updateNote(note, newNoteViewModel)        if !_coreDataSave() {            logDebug("更新Note出现错误")        }        return Just(AppAction.none).eraseToAnyPublisher()    }

3.0版本

无论1.0版本还是2.0版本,都可以很好的完成我们对状态容器功能的要求。

两个版本都严重依赖Combine,都是采用Combine来进行异步代码的生命周期管理,并且在2.0中又是通过Combine提供的.receive(on: DispatchQueue.main)来进行的线程调度。

幸好,Combine很好的完成了这个本来并非它最擅长(管理生命周期,线程调度)的工作。

今年,Swift 5.5推出了大家期待已久的Async/Await功能,在对新功能有了一定的了解后,我便有了用Async/Await来实现新的状态容器的想法。

•使用@MainActore保证State只能在主线程被修改•dispatch创建即发即弃的Task完成副作用生命周期管理•同2.0版本类似,在副作用方法中返回Task<AppAction,Error>,简化副作用代码

具体的实现:

代码语言:javascript
复制
@MainActorfinal class Store: ObservableObject {    @Published private(set) var state = AppState()    private let environment = Environment()    @discardableResult    func dispatch(_ action: AppAction) -> Task<Void, Never>? {        Task {            if let task = reduc(state: &state, action: action, environment: environment) {                do {                    let action = try await task.value                    send(action)                } catch {                    print(error)                }            }        }    }}

Reducer:

代码语言:javascript
复制
extension Store {    func reduc(state: inout AppState, action: AppAction, environment: Environment) -> Task<AppAction, Error>? {        switch action {        case .empty:            break        case .setAge(let age):            state.age = age            return Task {                await environment.setAge(age: 100)            }        case .setName(let name):            state.name = name            return Task {                await environment.setName(name: name)            }        }        return nil    }}

副作用:

代码语言:javascript
复制
final class Environment {    func setAge(age: Int) async -> AppAction {        print("set age")        return .empty    }    func setName(name: String) async -> AppAction {        print("set Name")        await Task.sleep(2 * 1000000000)        return AppAction.setAge(age: Int.random(in: 0...100))    }}

由于Store声明为@MainActor,我们在代码中须通过如下两种方式之一来引用:

代码语言:javascript
复制
@mainstruct NewReduxTest3AppApp: App {    @StateObject var store = Store()    var body: some Scene {        WindowGroup {            ContentView()                .environmentObject(store)        }    }}

或者

代码语言:javascript
复制
@main@MainActorstruct NewReduxTest3AppApp: App {    let store = Store()    var body: some Scene {        WindowGroup {            ContentView()                .environmentObject(store)        }    }}

新版本的代码不仅易读性更好,而且可以充分享受到Swift5.5带来的安全、高效的线程调度能力。

总结

通过此次重建状态容器,让我对Swift的Async/Await有了更多的了解,也认识到它在现代编程中的重要性。

希望本文对你有所帮助。

引用链接

[1] SwiftUI与Combine编程: https://objccn.io/products/ [2] 健康笔记1.0: https://www.fatbobman.com/healthnotes/ [3] SwiftUI与Combine编程: https://objccn.io/products/ [4] Redux-like state container in SwiftUI: https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/ [5] 健康笔记: https://www.fatbobman.com/healthnotes/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 用Async/Await重建SwiftU的Redux-like状态容器
    • SwiftUI的状态容器
      • 1.0版本
        • 2.0版本
          • 3.0版本
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档