Swiftpack.co - Package - ReactComponentKit/Redux

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.

ReactComponentKit/Redux

Manage SwiftUI's state with Redux and Combine :)

Redux

Manage SwiftUI's state with Redux and Combine :)

Counter Example

Define State

struct AppState: State {
    var count: Int = 0
    var error: (Error, Action)? = nil
}

Define Middlewares

enum MyError: Error {
    case tempError
}

enum MyError: Error {
    case tempError
}

func asyncJob(state: AppState, action: Action, sideEffect: @escaping SideEffect<AppState>) {
    let (dispatch, _) = sideEffect()
    Thread.sleep(forTimeInterval: 2)
    dispatch(IncrementAction(payload: 2))
}

func asyncJobWithError(state: AppState, action: Action, sideEffect: @escaping SideEffect<AppState>) throws {
    Thread.sleep(forTimeInterval: 20)
    throw MyError.tempError
}

Define Reducers

func counterReducer(state: AppState, action: Action) -> AppState {
    return state.copy { (mutation) in
        switch action {
        case let act as IncrementAction:
            mutation.count += act.payload
        case let act as DecrementAction:
            mutation.count -= act.payload
        default:
            break
        }
    }
}

Define Actions

struct AsyncIncrementAction: Action {
    static var job: ActionJob {
        Job<AppState>(middleware: [asyncJob])
    }
}

struct IncrementAction: Action {
    let payload: Int
    init(payload: Int = 1) {
        self.payload = payload
    }
    
    static var job: ActionJob {
        Job<AppState>(reducers: [counterReducer]) { state, newState in
            state.count = newState.count
        }
    }
}

struct DecrementAction: Action {
    let payload: Int
    init(payload: Int = 1) {
        self.payload = payload
    }
    
    static var job: ActionJob {
        Job(reducers: [counterReducer]) { state, newState in
            state.count = newState.count
        }
    }
}

struct TestAsyncErrorAction: Action {
    static var job: ActionJob {
        Job<AppState>(middleware: [asyncJobWithError])
    }
}

Define Store

class AppStore: Store<AppState> {
    override func beforeProcessingAction(state: AppState, action: Action) -> Action {
        // do whatever you need to
        return action
    }

    override func afterProcessingAction(state: AppState, action: Action) {
        // do whatever you need to
        print("[## \(action) ##]")
        print(state)
    }
}

Abstract Async State Value

struct AppState: State {
    var content: Async<String> = .uninitialized
    var error: (Error, Action)?
}

Define Middlewares

func fetchContent(state: AppState, action: Action, sideEffect: @escaping SideEffect<AppState>) {
    var (dispatch, context) = sideEffect()
    
    // if you need to access the store
    let store: AppStore = context.store()
    
    URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.com")!)
        .subscribe(on: DispatchQueue.global())
        .receive(on: DispatchQueue.global())
        .sink { (completion) in
            switch completion {
            case .finished:
                break
            case .failure(let error):
                dispatch(UpdateContentAction(content: .failed(error: error)))
            }
        } receiveValue: { (data, response) in
            let value = String(data: data, encoding: .utf8) ?? ""
            dispatch(UpdateContentAction(content: .success(value: value)))
        }
        .store(in: &context.cancellable)

}

Define Reducers

func updateContent(state: AppState, action: Action) -> AppState {
    guard let action = action as? UpdateContentAction else { return state }
    return state.copy { mutation in
        mutation.content = action.content
    }
}

Define Actions

struct RequestContentAction: Action {
    static var job: ActionJob {
        Job<AppState>(middleware: [fetchContent])
    }
}

struct UpdateContentAction: Action {
    let content: Async<String>
    
    static var job: ActionJob {
        Job<AppState>(reducers: [updateContent]) { (state, newState) in
            state.content = newState.content
        }
    }
}

Consume Async State

VStack {
    Button(action: { store.dispatch(action: RequestContentAction()) }) {
        Text("Fetch Content")
            .bold()
            .multilineTextAlignment(.center)
    }
    ScrollView(.vertical) {
        Text(store.state.content.value ?? store.state.content.error?.localizedDescription ?? "")
    }
    .frame(width: UIApplication.shared.windows.first?.frame.width)
}

Testing

CounterStore Example for Testing

import Foundation

struct CounterState: State {
    var count: Int = 0
    var error: (Error, Action)?
}

class CounterStore: Store<CounterState> {
}

struct IncrementAction: Action {
    let payload: Int
    static var job: ActionJob {
        Job<CounterState>(reducers: [counterReducer]) { state, newState in
            state.count = newState.count
        }
    }
}

struct DecrementAction: Action {
    let payload: Int
    static var job: ActionJob {
        Job<CounterState>(reducers: [counterReducer]) { state, newState in
            state.count = newState.count
        }
    }
}


func counterReducer(state: CounterState, action: Action) -> CounterState {
    return state.copy { mutation in
        switch action {
        case let act as IncrementAction:
            mutation.count += act.payload
        case let act as DecrementAction:
            mutation.count -= act.payload
        default:
            break
        }
    }
}

UnitTest

import XCTest
@testable import Redux

final class CounterStoreTests: XCTestCase {
    
    private var store: CounterStore? = nil
    
    override func setUp() {
        super.setUp()
        store = CounterStore()
    }
    
    override func tearDown() {
        super.tearDown()
        store = nil
    }
    
    func testInitialState() {
        XCTAssertEqual(0, store!.state.count)
        XCTAssertNil(store!.state.error)
    }
    
    func testIncrementAction() {
        let test = Test<CounterState>()
        
        test
            .dispatch(action: IncrementAction(payload: 1))
            .to(store)
        
        wait(for: test) { state in
            XCTAssertEqual(1, state.count)
        }
    }
    
    func testDecrementAction() {
        let test = Test<CounterState>()

        test
            .dispatch(action: DecrementAction(payload: 1))
            .to(store)

        wait(for: test) { state in
            XCTAssertEqual(-1, state.count)
        }
    }

    func testMultipleIncrementActions() {
        let test = Test<CounterState>()

        test
            .dispatch(action: IncrementAction(payload: 1))
            .dispatch(action: IncrementAction(payload: 1))
            .test { state in
                XCTAssertEqual(2, state.count)
            }
            .dispatch(action: IncrementAction(payload: 1))
            .dispatch(action: IncrementAction(payload: 1))
            .dispatch(action: IncrementAction(payload: 1))
            .to(store)

        wait(for: test) { (state) in
            XCTAssertEqual(5, state.count)
        }
    }

    func testMultipleDecrementActions() {
        let test = Test<CounterState>()

        test
            .dispatch(action: DecrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 1))
            .to(store)

        wait(for: test) { (state) in
            XCTAssertEqual(-5, state.count)
        }
    }

    func testMultipleIncDecActions() {
        let test = Test<CounterState>()
        let counterState = CounterState(count: 10, error: nil)
        test.reset(store: store!, state: counterState)

        XCTAssertEqual(10, store!.state.count)
        XCTAssertNil(store!.state.error)

        test.dispatch(action: IncrementAction(payload: 1))
            .dispatch(action: IncrementAction(payload: 2))
            .dispatch(action: IncrementAction(payload: 3))
            .test { state in
                XCTAssertEqual(16, state.count)
            }
            .dispatch(action: DecrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 2))
            .dispatch(action: DecrementAction(payload: 3))
            .test { state in
                XCTAssertEqual(10, state.count)
            }
            .dispatch(action: IncrementAction(payload: 1))
            .dispatch(action: DecrementAction(payload: 2))
            .to(store)

        wait(for: test) { (state) in
            XCTAssertEqual(9, state.count)
        }
    }
    

    static var allTests = [
        ("testInitialState", testInitialState),
    ]
}