Swiftpack.co - Package - StevenLambion/SwiftDux

SwiftDux

Predictable state management for SwiftUI applications.

Swift Version Platform Versions License

This is still a work in progress.

Introduction

This is yet another redux inspired state management solution for swift. It's built on top of the Combine framework with hooks for SwiftUI. This library helps build applications around an elm-like archectiture using a single, centralized state container. For more information about the architecture and this library, take a look at the getting started guide.

Why another library?

As someone expierienced with Rx, React and Redux, I was excited to see the introduction of SwiftUI and the Combine framework. After a couple of days, I noticed a lot of people asking questions about how best to architect their SwiftUI applications. I had already begun work on my own pet application, so I've ripped out the "redux" portion and added it here as its own library in the hopes that it might help others.

There's more established libraries like ReSwift which may provide more functionality. Due to previous ABI instabilities and how easy it is to implement in Swift, I've always rolled my own.

Why dux?

Ducks is an established and common way to organize your code into modules, so I thought it goes hand-in-hand with a library built around architecting an application. If you're new to tools like Redux and wonder how best to organize your files, ducks is a great option to begin with.

Documentation

Visit the documentation website.

Installation

Xcode 11

Use the new swift package manager integration to include the libary.

Swift Package Manager

Include the library as a dependencies as shown below:

import PackageDescription

let package = Package(
  dependencies: [
    .Package(url: "https://github.com/StevenLambion/SwiftDux.git", majorVersion: 0, minor: 6)
  ]
)

SwiftUI Examples

Adding the SwiftDux store to the SwiftUI environment:

struct RootView : View {
  var store: Store<AppState>

  var body: some View {
    BookListView()
      .mapState(updateOn: BookAction.self) { (state: AppState) in state.books }
      .provideStore(store)
  }

}

Use property wrappers to inject mapped states and the store dispatcher

struct BookListView : View {
  @MappedState var books: OrderedState<Book>
  @Dispatcher var send: SendAction

  var body: some View {
    List {
      ForEach(books) { item in
        BookRow(item: item)
      }
      .onMove { send(BookAction.moveBooks(from: $0, to: $1)) }
      .onDelete  { send(BookAction.removeBooks(at: $0)) }
    }
  }
}

Modify actions sent from child views

struct AuthorView : View {
  @MappedState author: AuthorState

  var body: some View {
    BookListView()
      .mapState(updateOn: BookAction.self) { (state: AuthorState) in state.books }
      .modifyActions(self.modifyBookActions)
  }

  func modifyBookActions(action: Action) -> Action? {
    if let action = action as? BookAction {
      return AuthorAction.routeBookAction(for: author.id, action)
    }
    return action
  }

}

Known Issues

onAppear() doesn't update the view when dispatching actions

The built-in onAppear method does not trigger a view update. Use the provided onAppearAsync() instead.

@MappedState fails to find its state

Make sure the mapState() method is called in the correct environment scope. For example, a NavigationButton's destination is not in the same scope as the current content of the NavigationView even though the Button is declared inside it. To fix this, call the mapState() method directly on the NavigationView.

When using modifyActions() some views don't update properly.

When using modifyActions() both the original action and the new action are broadcasted. The new action updates the UI first. If the update affects any mapState() methods, a new environment object will be created for them. When the second action is fired the environment object of those mapped states haven't subscribed to the didChange publisher yet, so the action is ignored. This shouldn't be a problem, but SwiftUI also doesn't appear to refresh views when setting a new environment object to replace a previous one.

The current workaround is to use the optional "exceptWhen" parameter of the mapState() method on the parent view to ignore the modified action. This stops the recreation of the child mapState() methods, and reduces unneeded rerendering.

Github

link
Stars: 8
Help us keep the lights on

Dependencies

Used By

Total: 0

Releases

v0.6.0 - Jun 22, 2019

Additions

  • Added onAppearAsync() and onDisappearAsync() to View to help trigger view updates when dispatching actions.

Changes

  • OrderedState<_> is now a RandomAccessCollection, allowing it to be used directly with SwiftUI's list elements.
  • OrderedState<_> now allows subscripting by index or id.

Fixes

  • The state mapping API now assumes SwiftUI Views may still exist shortly after the state itself was removed.

v0.5.0 - Jun 20, 2019

Changes

  • Made major refinements to the property wrapper API.
  • New state mapping API to map the application to a substate, views can focus on just their slice.
  • Store publishes changes on main thread.

v0.4.0 - Jun 20, 2019

Changes

  • Changed action plans from a closure type to structs.
  • ActionDispatcher is simplified to have one method that can received all types of actions (including action plans)
  • Converted API to use the new property wrapper API of Swift when mapping state to a view.

v0.3.0 - Jun 16, 2019

Changes

  • Added Store<>.connect(:, wrapper:) to update views using a mapping of the application's state as an alternative to updating off of an action type.
  • Hooked up dispatcher proxying for connect functions.

v0.2.0 - Jun 15, 2019

Changes

  • Added a new StoreActionDispatcher to separate the store from action dispatching. This allows the ability to proxy, modify, and monitor actions sent upstream.
  • Added View.provideStore(_:) modifier to inject an initial store and dispatcher object into the view environment.
  • Cleaned up and refined APIs
  • Added and cleaned up documentation
  • Reducer.reduceAny(state:action:) will always dispatch the action to Reducer.reduceNext(state:action:)