项目作者: palle-k

项目描述 :
Redux-Like unidirectional data flow for SwiftUI with a Redux-Saga-like side side effect model
高级语言: Swift
项目地址: git://github.com/palle-k/SwiftState.git
创建时间: 2019-06-19T14:37:19Z
项目社区:https://github.com/palle-k/SwiftState

开源协议:MIT License

下载


SwiftState

Redux + Saga unidirectional data flow built for SwiftUI and Combine

Quick Start

Install

Requires Swift 5.1 and iOS/iPadOS/tvOS 13, macOS 10.15 or watchOS 6

Swift Package Manager

Add the package as a dependency to the Package.swift file:

  1. .package(url: "https://github.com/palle-k/SwiftState.git", branch: "master")

Overview

The state of the app is managed by a single Store<State> object.
To modify the app state, an action must be dispatched against the store.
This triggers a root reducer of the app, which takes the current state and the action to produce a new state.

It is possible to read the app state from the store using the readonly store.state property.
Alternatively, the state can be subscribed to using the store.didChange publisher.

Reducers

The store calls the root reducer with the current state and a dispatched action.
The reducer then produces a new app state using only information from the current state and the action.

  1. struct AppState {
  2. var username: String?
  3. var count: Int
  4. }
  5. enum AppAction {
  6. case setUsername(String?)
  7. case incrementCount
  8. }
  9. func rootReducer(state: AppState, action: Action) -> AppState {
  10. var state = state // create a mutable copy of the app state
  11. switch Action {
  12. case AppAction.setUsername(let newUsername)
  13. state.username = newUsername
  14. case AppAction.incrementCount:
  15. state.count += 1
  16. }
  17. return state
  18. }

Middlewares

Middlewares can be used to dispatch additional actions following an initial action.
Examples for this can be network calls that are triggered by an action and then asynchronously dispatch a completion or error.

  1. enum RegisterAction {
  2. case register(username: String, password: String)
  3. case usernameTaken
  4. case passwordTooShort
  5. case success(LoginToken)
  6. }
  7. func registerMiddleware(getState: @escaping () -> AppState, dispatch: @escaping (Action) -> ()) {
  8. guard case RegisterAction.register(username: let username, password: let password) else {
  9. return
  10. }
  11. guard password.length >= 8 else {
  12. dispatch(RegisterAction.passwordTooShort)
  13. return
  14. }
  15. checkUsernameAvailability(username) { isAvailable in
  16. guard isAvailable else {
  17. dispatch(RegisterAction.usernameTaken)
  18. return
  19. }
  20. registerUser(name: username, password: password) { loginToken in
  21. dispatch(RegisterAction.success(loginToken))
  22. }
  23. }
  24. }
  25. func loginMiddleware(getState: @escaping () -> AppState, dispatch: @escaping (Action) -> ()) {
  26. // ...
  27. }
  28. let store = Store<AppState>(
  29. initialState: initialState,
  30. rootReducer: rootReducer,
  31. middleware: Middlewares.combine(registerMiddleware, loginMiddleware)
  32. )

Sagas

Sagas run asynchronous middleware in regular code through coroutines without the need to nest completion handlers.

  1. store.runSaga { yield in
  2. yield(Effects.TakeEvery(RegisterAction.self) { action, yield in
  3. let state = yield(Effects.Select(AppState.self))
  4. let response = yield(Effects.Call { completion in
  5. performRegisterAPICall(state, action, completion: completion)
  6. })
  7. if let token = response.token {
  8. yield(Effects.Put(RegisterAction.success(token)))
  9. } else {
  10. yield(Effects.Put(RegisterAction.usernameTaken))
  11. }
  12. }
  13. }

Each saga is a generator function that yields effects.
As sagas are implemented using continuations (setjmp and longjmp), they can run on arbitrary threads without blocking them.
This mechanism allows long running sagas on the main thread (if desired) without the UI being frozen.

Effects

The following effects are available through the Effects namespace:

  • Select: Retrieves the current state
  • Put: Dispatches an action
  • Call: Performs a method call to a function with a completion handler.
  • Sleep: Waits for a given time interval (does not block the current thread).
  • Fork: Runs a saga in parallel to the current saga.
  • Take: Waits until an action of a given type is dispatched.
  • TakeLeading: Forks and takes every action of the given type that is dispatched. If another instance of the provided saga is already running, the call is ignored.
  • TakeEvery: Forks and takes every action of the given type that is dispatched and runs the provided saga with the action as an argument.
  • TakeLatest: Forks and takes every action of the given type that is dispatched. If another instance of the saga is already running, it is cancelled.
  • Debounce: Forks and takes every action of the given type that is dispatched. After the action is dispatched, a sleep is performed for the provided interval. If no other instance of the action has been dispatched in the meantime, the provided saga is executed.
  • Throttle: Forks and takes every action of the given type that is dispatched. If the last dispatch of the action type occurred later than the given time interval ago, the action is ignored.
  • All: Executes all provided effects in parallel and waits for completion of all of the effects.

SwiftUI integration

The store can be integrated into a SwiftUI view hierarchy using the @EnvironmentObject property wrapper in the scene function of the SceneDelegate:

  1. ContentView().environmentObject(store)

In every SwiftUI View that is placed in the hierarchy of the content view, it is then possible to access the store as an environment object.

  1. struct YourView: View {
  2. @EnvironmentObject let store: Store<AppState> // automatically set by SwiftUI
  3. var body: some View {
  4. VStack {
  5. Text(store.state.username ?? "not logged in")
  6. Button(
  7. action: {self.store.dispatch(AppAction.setUsername("John Appleseed"))},
  8. label: {Text("Set Username")}
  9. )
  10. }
  11. }
  12. }