Swiftpack.co - KazaiMazai/PureduxSwiftUI as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by KazaiMazai.
KazaiMazai/PureduxSwiftUI 1.1.0
SwiftUI bindings to connect UI to PureduxStore
⭐️ 12
🕓 52 weeks ago
iOS
.package(url: "https://github.com/KazaiMazai/PureduxSwiftUI.git", from: "1.1.0")

Sublime's custom image

SwiftUI bindings to connect UI to PureduxStore

Continuous Integration

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

  1. Import:
import PureduxSwiftUI

  1. Implement your FancyView

typealias Command = () -> Void

struct FancyView: View {
    let title: String
    let didAppear: Command
    
    var body: some View {
        Text(title)
            .onAppear { didAppear() }
    }
}
  1. Declare how view connects to store:

extension FancyView {

  init(state: AppState, dispatch: @escaping Dispatch<Action>) {
      self.init(
          title: state.title,
          didAppear: { dispatch(FancyViewDidAppearAction()) }
      )
   }
}
  1. 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.

Click for details

  1. Migrate to from RootStore to StoreFactory like 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() 

  1. Migrate from StoreProvidingView to ViewWithStoreFactory in case your implementation relied on injected RootEnvStore

Before:

 UIHostingController(
      rootView: StoreProvidingView(rootStore: rootEnvStore) {
        
        //content view
     }
 )

Now:

 UIHostingController(
    rootView: ViewWithStoreFactory(envStoreFactory) {
        
       //content view
    }
)
  1. Migrate from View.with(...) extension to ViewWithStore(...)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)
                    
  1. Migrate from View.withEnvStore(...) extension to ViewWithStore(...) in case your implementation relied on injected RootEnvStore

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 })

                    

Q&A

Click for details

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's lifecycle.
  • 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
    }
}

Props can be though of as a view model.

  1. Prepare Props .

extension FancyView.Props {
    static func makeProps(
          state: AppState, 
          dispatch: @escaping Dispatch<Action>) -> FancyView.Props {
        
        //prepare props for your fancy view
    }
}

  1. Connect View with store by providing props closure and content view from Props:

ViewWithStore(props: FancyView.Props.makeProps) { props in
    FancyView(
      props: props
    )
}

This allows to make Views dependent only on Props and reuse it in different ways.

Which DispatchQueue is used to prepare props?

  • By default, it works on a shared PresentationQueue. It is a global serial queue with user interactive quality of service. The purpose is to do as little as possible on the main thread queue.

Is it safe at all?

  • PureduxSwiftUI hops to the main dispatch queue in the end to update View. So yes, it's safe. Unless you try to do UI related things (you should not) during your Props preparation.

How to change presentation queue that is used to prepare props?

  • PureduxSwiftUI allows to use main queue or user-provided custom queue. The only requirement for the custom queue is to be serial one.
ViewWithStore {
   //Your content here
}
.usePresentationQueue(.main)
  

or standalone queue:

let queue = DispatchQueue(label: "some.queue", qos: .userInteractive)
  
ViewWithStore {
   //your content here
}
.usePresentationQueue(.serialQueue(queue))
  

Why we might need to prepare props on background queue?

  • Props evaluation maybe heavier than we would love to. We may deal with a large array of items, AttributedStrings, and any other slow things. Doing it on the main queue may eventually slow down our fancy app.

How to deduplicate state changes?

  • State deduplication is done by providing a way to compare two states on equality.
  • It allows to avoid props evaluation on every state update
  • It's done with the help of Equating<State> guy:
ViewWithStore {
   //Your content here
}
.removeStateDuplicates(.equal { $0.title })
  

Why we might need to deduplicate state changes?

  • Props evaluation maybe heavier than we would love to. The app state may be huuuuge and we might love to re-evaluate Props only when it's necessary.

Why we need Equating<State> guy?

  • Depending on context (or particular screen), we might be interested in different part of the state. Different properties of the same type.
  • And would like to deduplicate updates depending on it.
  • That's why single Equatable implementation won't work here.
VStack {  
    ViewWithStore { state, dispatch in
        FancyTitleView(state: state, dispatch: dispatch)
    )
    .removeStateDuplicates(.equal { $0.title })

    ViewWithStore { state, dispatch in
        FancySubtitleView(state: state, dispatch: dispatch)
    )
    .removeStateDuplicates(.equal { $0.subtitle })
}               

Any other Equating<State> details ?

  • Equating is a protocol witness for Equtable. It answers the question: "Are these states equal?"
  • With the help of it, deduplication happens.

Here is the definition:

  
    Equating<T> { (lhs: T, rhs: T) -> Bool
        //compare here
    }

It has handy extensions, like Equating.alwaysEqual or Equating.neverEqual as well as && operator:


ViewWithStore { state, dispatch in
        FancyView(state: state, dispatch: dispatch)
)
.removeStateDuplicates(
    .equal { $0.title } &&
    .equal { $0.subtitle }
)            

Licensing

PureduxSwiftUI is licensed under MIT license.

GitHub

link
Stars: 12
Last commit: 1 week ago
Advertisement: IndiePitcher.com - Cold Email Software for Startups

Release Notes

Release v1.1.0
52 weeks ago

This release contains

  • API update
  • StoreFactory support from PureduxStore 1.1.0
  • Child store support from PureduxStore 1.1.0
  • Old API is marked as deprecated, however still functioning
  • Docs update and migration guides provided

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