Swiftpack.co -  afiorito/Reflow as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
afiorito/Reflow
A Redux-like implementation of the unidirectional data flow architecture in Swift using Combine.
.package(url: "https://github.com/afiorito/Reflow.git", from: "v1.3.0")

icon Reflow

Reflow is a Redux-like implementation of the unidirectional data flow architecture in Swift using Combine.

About Reflow

Reflow relies on a few concepts:

  • A Store contains the entire state of your app in a single data structure. The state is modified by dispatching actions to the store. Whenever the state changes, all selectors are updated via a Combine publisher.

  • A Reducer is a pure function that when given action, uses the current state to create a new state.

  • An Action is a way of describing a change in state. Actions don't have logic, they are dispatched to the store and processed by reducers.

Installation

Add Reflow to your project using Swift Package Manager. In your Xcode project, select File > Swift Packages > Add Package Dependency and enter the repository URL.

Documentation

Basic Usage

  1. Create a state representation.
struct CounterState {
  var count: Int
  var name: String
}
  1. Create dispatchable actions.
enum CounterAction: Action {
  case increment
  case loadedName(String)
}
  1. Create a reducer.
struct CounterState: Equatable {
  ...

  static func reducer(state: Self, action: Action) -> Self {
    switch action {
      case CounterAction.increment:
        return Self(count: state.count + 1, name: state.name)
      case CounterAction.loadedName(let name):
        return Self(count: state.count, name: name)
      default:
          return state
    }
  }
}
  1. Create a store.
let store = Store(reducer: CounterState.reducer, initialState: CounterState(count: 0, name: ""))
  1. Dispatch an action.
store.dispatch(CounterAction.increment)
  1. Subscribe to the store.
let cancellable = store
  .select { state in state.count }
  .sink { count in
    // prints "Received count: 0" before any actions are dispatched
    // prints "Received count: 1" after action is dispatched
    print("Received count: \(count)")
  }

Effects

Normal (synchronous) actions don't allow for side effects like making a network call or accessing the disk. Effects allow you to perform asynchronous operations with side effects. Effects are not passed through the middleware pipeline and never reach the reducer but can dispatch to other actions.

func loadCounterName(url: URL) -> AnyPublisher<String?, Never> {
  return URLSession(configuration: .default)
    .dataTaskPublisher(for: url)
    .map { String(data: $0.data, encoding: .utf8) }
    .replaceError(with: nil)
    .eraseToAnyPublisher()
}

enum CounterAction: Action {
  ...

  static let loadName = Effect<CounterState> { dispatch, getState -> AnyCancellable in
    return loadCounterName(url: URL(string: "https://api.counter.com/name")!)
      .sink { name in
        dispatch(CounterAction.loadedName(name ?? ""))
      }
  }
}

// dispatch the effect
let cancellable = store.dispatch(CounterAction.loadName)

Affects can also be synchronous by returning Void.

store.dispatch(Effect<CounterState> { dispatch, getState -> Void in
  // effect logic
})

Middleware

Middleware transform actions and can create more actions, while having access to the current state at any point. These actions are passed from middleware to middleware until they reach the reducer.

Middleware are functions that can be registered on store creation:

let logger: Middleware<CounterState> = { dispatch, getState in
  return { next in
    return { action in
      print("Received action: \(action), with current state: \(getState())")
      next(action)  // pass the action to the next middleware
    }
  }
}

// register the middleware
let store = Store(reducer: CounterState.reducer, initialState: CounterState(count: 0, name: ""), middleware: [logger])

Combine Reducers

As your application state becomes more complex and you create more actions, it may be useful to split your state into multiple reducers. Each reducer handles a single key of your state.

struct AuthState {
  var token: String

  static func reducer(state: Self, action: Action) -> Self {
    return state
  }
}

struct AppState {
  var counter = CounterState(count: 0, name: "")
  var auth = AuthState(token: "")
}

let reducers: Reducer<AppState> = combineReducers(
  withKey(\.counter, use: CounterState.reducer),
  withKey(\.auth, use: AuthState.reducer)
)

// pass the combined reducers to the store
let store = Store(reducer: reducers, initialState: AppState())

License

Reflow is available under the MIT license. See the LICENSE file for more info.

GitHub

link
Stars: 1
Last commit: 3 days ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Related Packages

Release Notes

Reflow v1.3.0
22 weeks ago

Easier overriding for Store

Changed certain access specifiers for Store to make it easier to override.

The updated state property:

open var state: State

The updated select method:

open func select<Prop: Equatable>(_ selector: @escaping (State) -> (Prop)) -> AnyPublisher<Prop, Never>

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API