Cloe is Redux on Combine for SwiftUI with excellent feng shui.
MIT License
Cloe is Redux on Combine for SwiftUI with excellent feng shui.
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>
func appReducer(state: inout AppState, action: Action) {
guard let action = action as? AppAction else { return }
switch action {
case .growup:
state.age += 1
}
}
// 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)
These extensions improve the ergonomics of working with the store. With the built-in
dispatch
function we would normally dispatch with store.dispatch(AppAction.growup)
.
With this dispatch
extension we can do store.dispatch(.growup)
instead.
The subscript
extension allows us to avoid using a closure with SwiftUI views.
For example, a button can be implemented with: 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) }
}
}
This is an example of injecting state using a state selector. Here were define the state selector inside of the View, but it can be defined anywhere.
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])
}
}
If you want to connect to the state of the store without defining a selector,
use ConnectStore
instead. Note that ConnectStore
does not currently skip
duplicate states the way that Connect
does.
Here's how you can dispatch a simple action:
Button("Grow up") { self.store.dispatch(AppAction.growup) }
// ... or ...
Button("Grow up", action: store[AppAction.growup])
Or with the optional Store
extension mentioned above:
Button("Grow up") { self.store.dispatch(.growup) }
// ...or...
Button("Grow up", action: store[.growup])
Below is a simple example, read more about publisher middleware here.
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 documentation.
Connect
it to a SwiftUI component it always skips repeated states (subject to change).Store
object conform to ObservableObject
?You may have noticed that Cloe's Store
class conforms to ObservableObject
.
However, the Store
does not contain any @Published
properties. This conformance
is only added to make it easy to inject your store with .environmentObject()
.
However, since we don't expose any @Published
vars don't expect a view with
@ObservedObject var store: AppStore
to automatically re-render when the store changes. This design is intentional so you can
subscribe to more granular updates with Connect
.
To run the example project, clone this repo, and open iOS Example.xcworkspace from the iOS Example directory.
Add this to your project using Swift Package Manager. In Xcode that is simply: File > Swift Packages > Add Package Dependency... and you're done. Alternative installations options are shown below for legacy projects.
If you are already using CocoaPods, just add 'Cloe' to your Podfile
then run pod install
.
If you are already using Carthage, just add to your Cartfile
:
github "gilbox/Cloe" ~> 0.3.0
Then run carthage update
to build the framework and drag the built Cloe
.framework into your Xcode project.
Cloe is available under the MIT license. See the LICENSE file for more information.