Cloe
Cloe是针对SwiftUI的Redux在Combine上实现的解决方案,具有良好的设计风格。
设置您的store
struct AppState {
var appName = "Demo App"
var age = 6
var names = ["hank", "cloe", "spike", "joffrey", "fido", "kahlil", "malik"]
static let initialValue = AppState()
}
enum AppAction: Action {
case growup
}
typealias AppStore = Store<AppReducer>
设置您的reducer
func appReducer(state: inout AppState, action: Action) {
guard let action = action as? AppAction else { return }
switch action {
case .growup:
state.age += 1
}
}
实例化您的Store
// Create a store with the publisher middleware
// this middleware allows us to use `PublisherAction`
// later to dispatch an async action.
let store = AppStore(
reducer: appReducer,
state: .initialValue,
middlewares: [createPublisherMiddleware()])
// Inject the store with `.environmentObject()`.
// Alternatively we could inject it with `.environment()`
let contentView = ContentView().environmentObject(store)
// later...
window.rootViewController = UIHostingController(rootView: contentView)
(可选) 向store中添加一些便利扩展
这些扩展可以提高与store工作的便捷性。使用内建的 dispatch
函数,我们通常会使用 store.dispatch(AppAction.growup)
来发送。有了这个 dispatch
扩展,我们可以使用 store.dispatch(.growup)
来替代。
subscript
扩展允许我们在SwiftUI视图中避免使用闭包。例如,按钮可以使用以下方式实现:Button("Grow up", action: store[.growup])
。
extension Store {
func dispatch(_ action: AppAction) {
dispatch(action as Action)
}
subscript(_ action: AppAction) -> (() -> Void) {
{ [weak self] in self?.dispatch(action as Action) }
}
}
将您的 SwiftUI 视图连接到您的 store
这是一个使用状态选择器注入状态的示例。我们在 View 中定义了状态选择器,但也可以在任何地方定义。
struct MyView: View {
var index: Int
// Define your derived state
struct MyDerivedState: Equatable {
var age: Int
var name: String
}
// Inject your store
@EnvironmentObject var store: AppStore
// Connect to the store
var body: some View {
Connect(store: store, selector: selector, content: body)
}
// Render something using the selected state
private func body(_ state: MyDerivedState) -> some View {
Text("Hello \(state.name)!")
}
// Setup a state selector
private func selector(_ state: AppState) -> MyDerivedState {
.init(age: state.age, name: state.names[index])
}
}
如果您想连接到 store 的状态而不定义选择器,则使用 ConnectStore
。请注意,目前 ConnectStore
不会像 Connect
那样跳过重复的状态。
派发一个简单的动作
下面是如何派发一个简单动作的示例
Button("Grow up") { self.store.dispatch(AppAction.growup) }
// ... or ...
Button("Grow up", action: store[AppAction.growup])
或者使用上面提到的可选的 Store
扩展
Button("Grow up") { self.store.dispatch(.growup) }
// ...or...
Button("Grow up", action: store[.growup])
使用 publisher 中间件派发异步动作
下面是一个简单示例,更多关于 publisher 中间件的信息请参阅 此处。
Button("Grow up") { self.store.dispatch(self.delayedGrowup) }
//...
private let delayedGrowup = PublisherAction<AppState> { dispatch, getState, cancellables in
Just(())
.delay(for: 2, scheduler: RunLoop.main)
.sink { _ in
dispatch(AppAction.growup)
}
.store(in: &cancellables)
}
使用 publisher-dispatcher 跟踪异步任务进度
与 ReSwift 有何不同?
- ReSwift 经受战火考验。
- ReSwift 正在实际的生产应用中使用。
- Cloe 使用 Combine Publishers 而不是 定制的 StoreSubscriber
- Cloe 的 Middleware 比的 ReSwift Middleware 简单,但实现了相同级别的灵活性。
- Cloe的
combineMiddleware
函数更简洁、更容易阅读。 - Cloe提供了一种流畅的方式来连接您的SwiftUI视图。
- Cloe未为主Store状态提供跳过重复项的选项,但当您将其连接到SwiftUI组件时,它总是会跳过重复的状态(可能有所变化)。
Store
对象要遵守ObservableObject
协议?
为什么您可能已经注意到Cloe的Store
类遵守了ObservableObject
协议。然而,Store
并不包含任何@Published
属性。添加这种遵守只是为了方便使用.environmentObject()
向您的store注入。然而,由于我们不暴露任何@Published
变量,请不要期望当store发生变化时,带有
@ObservedObject var store: AppStore
的视图会自动重新渲染。这种设计是故意的,这样您就可以使用订阅通过Connect
的更细粒度的更新。
示例
要运行示例项目,请克隆此存储库,然后从iOS Example目录打开iOS Example.xcworkspace。
要求
- iOS 13
- macOS 10.15
- watchOS 6
- tvOS 13
安装
使用Swift包管理器将此添加到您的项目中。在Xcode中,这很简单:File > Swift Packages > Add Package Dependency... 就完成了。以下是针对遗留项目的其他安装选项。
CocoaPods
如果你已经在使用 CocoaPods,只需将 'Cloe' 添加到你的 Podfile
中,然后运行 pod install
。
Carthage
如果你已经在使用 Carthage,只需将其添加到你的 Cartfile
github "gilbox/Cloe" ~> 0.3.0
然后运行 carthage update
来构建框架,并将构建的 Cloe.framework
拖入你的 Xcode 项目中。
许可证
Cloe 采用 MIT 许可证。更多详细信息请参阅 LICENSE 文件。