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.

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