A super simple Swift implementation of the redux pattern making use of Swift 5.5's new async await API's.
- iOS 15.0+
- macOS 12.0+
In Xcode:
- Click
Project
. - Click
Package Dependencies
. - Click
+
. - Enter package URL:
https://github.com/reddavis/Redux
. - Add
RedUx
to your app target.
Documentation can be found here.
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() }
}
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
)
}
}
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)
]
)
}
}
- 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.