Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.
/ RedUx Public archive

A super simple Swift implementation of the redux pattern making use of Swift 5.5's new async await API's.

License

Notifications You must be signed in to change notification settings

reddavis/RedUx

Repository files navigation

RedUx

A super simple Swift implementation of the redux pattern making use of Swift 5.5's new async await API's.

Requirements

  • iOS 15.0+
  • macOS 12.0+

Installation

Swift Package Manager

In Xcode:

  1. Click Project.
  2. Click Package Dependencies.
  3. Click +.
  4. Enter package URL: https://github.com/reddavis/Redux.
  5. Add RedUx to your app target.
  6. If you want the test utilities, add RedUxTestUtilities to your test target.

Example screenshot of Xcode

Documentation

Documentation can be found here.

Usage

App store

import RedUx


typealias AppStore = RedUx.Store<AppState, AppEvent, AppEnvironment>


extension AppStore {
    static func make() -> AppStore {
        Store(
            state: .init(),
            reducer: reducer,
            environment: .init()
        )
    }
    
    static func mock(
        state: AppState
    ) -> AppStore {
        Store(
            state: state,
            reducer: .empty,
            environment: .init()
        )
    }
}



// MARK: Reducer

fileprivate let reducer: Reducer<AppState, AppEvent, AppEnvironment> = Reducer { state, event, environment in
    switch event {
    case .increment:
        state.count += 1
        return .none
    case .decrement:
        state.count -= 1
        return .none
    case .incrementWithDelay:
        return AsyncStream { continuation in
            // Really taxing shiz
            try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
            continuation.yield(.increment)
            continuation.finish()
        }.eraseToAnyAsyncSequenceable()
    case .toggleIsPresentingSheet:
        state.isPresentingSheet.toggle()
        return .none
    case .details:
        return .none
    }
}
<>
detailsReducer.pull(
    state: \.details,
    localEvent: {
        guard case AppEvent.details(let localEvent) = $0 else { return nil }
        return localEvent
    },
    appEvent: AppEvent.details,
    environment: { $0 }
)



// MARK: State

struct AppState: Equatable {
    var count = 0
    var isPresentingSheet = false
    var details: DetailsState = .init()
}



// MARK: Event

enum AppEvent {
    case increment
    case decrement
    case incrementWithDelay
    case toggleIsPresentingSheet
    case details(DetailsEvent)
}



// MARK: Environment

struct AppEnvironment {
    static var mock: AppEnvironment { .init() }
}

Root screen

import SwiftUI
import RedUx


struct RootScreen: View, RedUxable {
    typealias LocalState = AppState
    typealias LocalEvent = AppEvent
    typealias LocalEnvironment = AppEnvironment
    
    let store: LocalStore
    @StateObject var viewModel: LocalViewModel
    
    // MARK: Initialization
    
    init(store: LocalStore, viewModel: LocalViewModel) {
        self.store = store
        self._viewModel = .init(wrappedValue: viewModel)
    }
    
    // MARK: Body
    
    var body: some View {
        VStack(alignment: .center) {
            Text(verbatim: .init(self.viewModel.count))
                .font(.largeTitle)
            
            HStack {
                Button("Decrement") {
                    self.viewModel.send(.decrement)
                }
                .buttonStyle(.bordered)
                
                Button("Increment") {
                    self.viewModel.send(.increment)
                }
                .buttonStyle(.bordered)
                
                Button("Delayed increment") {
                    self.viewModel.send(.incrementWithDelay)
                }
                .buttonStyle(.bordered)
            }
            
            Button("Present sheet") {
                self.viewModel.send(.toggleIsPresentingSheet)
            }
            .buttonStyle(.bordered)
        }
        .sheet(
            isPresented: self.viewModel.binding(
                value: \.isPresentingSheet,
                event: .toggleIsPresentingSheet
            ),
            onDismiss: nil,
            content: {
                DetailsScreen.make(
                    store: self.store.scope(
                        state: \.details,
                        event: AppEvent.details,
                        environment: { $0 }
                    )
                )
            }
        )
    }
}



// MARK: Preview

struct RootScreen_Previews: PreviewProvider {
    static var previews: some View {
        RootScreen.mock(
            state: .init(
                count: 0
            ),
            environment: .mock
        )
    }
}

Tests

import XCTest
import RedUxTestUtilities
@testable import Example


class RootScreenTests: XCTestCase {
    func testStateChange() async {
        let store = RootScreen.LocalStore.make()
        
        await XCTAssertStateChange(
            store: store,
            events: [
                .increment,
                .decrement,
                .incrementWithDelay
            ],
            matches: [
                .init(),
                .init(count: 1),
                .init(count: 0),
                .init(count: 1)
            ]
        )
    }
}

Other libraries

  • Papyrus - Papyrus aims to hit the sweet spot between saving raw API responses to the file system and a fully fledged database like Realm.
  • Asynchrone - Extensions and additions to AsyncSequence, AsyncStream and AsyncThrowingStream.
  • Validate - A property wrapper that can validate the property it wraps.
  • Kyu - A persistent queue system in Swift.
  • FloatingLabelTextFieldStyle - A floating label style for SwiftUI's TextField.
  • Panel - A panel component similar to the iOS Airpod battery panel.

About

A super simple Swift implementation of the redux pattern making use of Swift 5.5's new async await API's.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Languages