PureduxSwiftUI
SwiftUI bindings to connect UI to PureduxStore
Install / Use
/learn @KazaiMazai/PureduxSwiftUIREADME
SwiftUI bindings to connect UI to PureduxStore
<p align="left"> <a href="https://github.com/KazaiMazai/PureduxSwiftUI/actions"> <img src="https://github.com/KazaiMazai/PureduxSwiftUI/workflows/Tests/badge.svg" alt="Continuous Integration"> </a> </p>Features
- Сlean and reusable SwiftUI's Views without dependencies
- Presentation data model aka. 'Props' can be prepared on Main or Background queue
- State updates deduplication to avoid unnecessary UI refresh
Important Notice
This repo has been moved to Puredux monorepo. Follow the installation guide there.
If you're looking to contribute or raise an issue, head over to the main repository where it's being developed now.
Installation
Swift Package Manager.
PureduxSwiftUI is available as a part of Puredux via Swift Package Manager. To install it, in Xcode 11.0 or later select File > Swift Packages > Add Package Dependency... and add Puredux repositoies URLs for the modules requried:
https://github.com/KazaiMazai/Puredux
Quick Start Guide
- Import:
import PureduxSwiftUI
- Implement your FancyView
typealias Command = () -> Void
struct FancyView: View {
let title: String
let didAppear: Command
var body: some View {
Text(title)
.onAppear { didAppear() }
}
}
- Declare how view connects to store:
extension FancyView {
init(state: AppState, dispatch: @escaping Dispatch<Action>) {
self.init(
title: state.title,
didAppear: { dispatch(FancyViewDidAppearAction()) }
)
}
}
- Connect your fancy view to the store, providing how it should be connected:
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(
initialState: state,
reducer: { state, action in state.reduce(action) }
)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
UIHostingController(
rootView: ViewWithStoreFactory(envStoreFactory) {
ViewWithStore { state, dispatch in
FancyView(
state: state,
dispatch: dispatch
)
}
}
)
How to migrate from v1.0.x to v1.1.x
Old API will be deprecated in the next major update. Good time to migrate to new API, especially if you plan to use new features like child stores.
<details><summary>Click for details</summary> <p>- Migrate to from
RootStoretoStoreFactorylike mentioned in PureduxStore docs
Before:
let appState = AppState()
let rootStore = RootStore<AppState, Action>(initialState: appState, reducer: reducer)
let rootEnvStore = RootEnvStore(rootStore: rootStore)
let fancyFeatureStore = rootEnvStore.store().proxy { $0.yourFancyFeatureSubstate }
let presenter = FancyViewPresenter()
Now:
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(initialState: state, reducer: reducer)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
let fancyFeatureStore = envStoreFactory.scopeStore { $0.yourFancyFeatureSubstate }
let presenter = FancyViewPresenter()
- Migrate from
StoreProvidingViewtoViewWithStoreFactoryin case your implementation relied on injectedRootEnvStore
Before:
UIHostingController(
rootView: StoreProvidingView(rootStore: rootEnvStore) {
//content view
}
)
Now:
UIHostingController(
rootView: ViewWithStoreFactory(envStoreFactory) {
//content view
}
)
- Migrate from
View.with(...)extension toViewWithStore(...)in case your implementation relied on explicit store
Before:
FancyView.with(
store: fancyFeatureStore,
removeStateDuplicates: .equal {
$0.title
},
props: presenter.makeProps,
queue: .main,
content: { FancyView(props: $0) }
)
Now:
ViewWithStore(props: presenter.makeProps) {
FancyView(props: $0)
}
.usePresentationQueue(.main)
.removeStateDuplicates(.equal { $0.title })
.store(fancyFeatureStore)
- Migrate from
View.withEnvStore(...)extension toViewWithStore(...)in case your implementation relied on injectedRootEnvStore
Before:
FancyView.withEnvStore(
removeStateDuplicates: .equal {
$0.title
},
props: presenter.makeProps,
queue: .main,
content: { FancyView(props: $0) }
)
Now:
ViewWithStore(props: presenter.makeProps) {
FancyView(props: $0)
}
.usePresentationQueue(.main)
.removeStateDuplicates(.equal { $0.title })
</p>
</details>
Q&A
<details><summary>Click for details</summary> <p>What is PureduxStore?
It's minilistic UDF architecture store implementation. More details can be found here
How to connect view to store?
PureduxSwiftUI allows to connect view to the following kinds of stores:
- Explicitly provided store
- Root store - app's central single store
- Scope store - scoped proxy to app's central single store
- Child store - a composition of independent store with app's root store
How to connect view to the explicitly provided store:
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(initialState: state, reducer: reducer)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
let featureStore = envStoreFactory.scopeStore { $0.yourFancyFeatureSubstate }
UIHostingController(
rootView: ViewWithStore { state, dispatch in
FancyView(
state: state,
dispatch: dispatch
)
}
.store(featureStore)
)
How to connect view to the EnvStoreFactory's root store:
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(initialState: state, reducer: reducer)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
UIHostingController(
rootView: ViewWithStoreFactory(envStoreFactory) {
ViewWithStore { appState, dispatch in
FancyView(
state: state,
dispatch: dispatch
)
}
}
)
How to connect view to the EnvStoreFactory's root scope store
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(initialState: state, reducer: reducer)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
UIHostingController(
rootView: ViewWithStoreFactory(envStoreFactory) {
ViewWithStore { featureSubstate, dispatch in
FancyView(
state: substate,
dispatch: dispatch
)
}
.scopeStore({ $0.yourFancyFeatureSubstate })
}
)
How to connect view to the EnvStoreFactory's root child store
Child store is special. More details in PureduxStore docs
- ChildStore is a composition of root store and newly created local store.
- ChildStore's state is a mapping of the local child state and root store's state
- Child store has its own reducer.
- ChildStore's lifecycle along with its LocalState is determined by
ViewWithStore'slifecycle. - Child state would be destroyed when ViewWithStore disappears from the hierarchy.
When child store is used, view recieves composition of root and child state.
This allows View to use both a local child state as well as global app's root state.
Child actions dispatching also works in a special way.
Child actions dispatching and state delivery works in the following way:
- Actions go down from child stores to root store
- Actions never go from one child stores to another child store
- States go up from root store to child stores and views
When action is dispatched to RootStore:
- action is delivered to root store's reducer
- action is not delivered to child store's reducer
- root state update triggers root store's subscribers
- root state update triggers child stores' subscribers
- Interceptor dispatches additional actions to RootStore
When action is dispatched to ChildStore:
- action is delivered to root store's reducer
- action is delivered to child store's reducer
- root state update triggers root store's subscribers.
- root state update triggers child store's subscribers.
- local state update triggers child stores' subscribers.
- Interceptor dispatches additional actions to ChildStore
let appState = AppState()
let storeFactory = StoreFactory<AppState, Action>(initialState: state, reducer: reducer)
let envStoreFactory = EnvStoreFactory(storeFactory: storeFactory)
UIHostingController(
rootView: ViewWithStoreFactory(envStoreFactory) {
ViewWithStore { stateComposition, dispatch in
FancyView(
state: substate,
dispatch: dispatch
)
}
.childStore(
initialState: ChildState(),
stateMapping: { appState, childState in
StateComposition(appState, childState)
},
reducer: { childState, action in childState.reduce(action) }
)
}
)
How to split view from state with props?
PureduxSwiftUI allows to add an extra presentation layer between view and state. It can be done for view reusability purposes. It also allows to improve performance by moving props preparation to background queue.
We can add Props:
struct FancyView: View {
let props: Props
var body: some View {
Text(props.title)
.onAppear { props.didAppear() }
}
}
extension FancyView {
struct Props {
let title: String
let didAppear: Command
