ProductPromotion
Logo

Swift

made by https://0x3d.site

GitHub - Ryu0118/swiftui-simplex-architecture: A Library of simple architecture that decouples state changes from SwiftUI's View
A Library of simple architecture that decouples state changes from SwiftUI's View - Ryu0118/swiftui-simplex-architecture
Visit Site

GitHub - Ryu0118/swiftui-simplex-architecture: A Library of simple architecture that decouples state changes from SwiftUI's View

GitHub - Ryu0118/swiftui-simplex-architecture: A Library of simple architecture that decouples state changes from SwiftUI's View

Simplex Architecture

A Library of simple architectures that decouples state changes from SwiftUI's View.

Language:Swift License:MIT Latest Release Twitter

This library is inspired by TCA (swift-composable-architecture), which allows you to decouple the state change logic from the SwiftUI's View and ObservableObject and confine it within the Reducer.

In TCA, integrating child domains into parent domains resulted in higher computational costs, especially at the leaf nodes of the app. Our library addresses this by avoiding the integration of child domains into parent domains, eliminating unnecessary computational overhead. To share values or logic with deeply nested views, we leverage SwiftUI's EnvironmentObject property wrapper. This allows you to seamlessly write logic or state that can be accessed throughout the app. Moreover, our library simplifies the app-building process. You no longer need to remember various TCA modifiers or custom views like ForEachStore, IfLetStore, SwitchStore, sheet(store:), and so on.

Examples

We've provided example implementations within this library. Currently, we only feature a simple GitHub repository search app, but we plan to expand with more examples in the future.

Documentation

The documentation for main are available here:

Installation

let package = Package(
    name: "YourProject",
    ...
    dependencies: [
        .package(url: "https://github.com/Ryu0118/swiftui-simplex-architecture", exact: "0.9.0")
    ],
    targets: [
        .target(
            name: "YourTarget",
            dependencies: [
                .product(name: "SimplexArchitecture", package: "swiftui-simplex-architecture"),
            ]
        )
    ]
)

Basic Usage

The usage is almost the same as in TCA. State definitions use property wrappers used in SwiftUI, such as @State, @Binding, @FocusState.

@Reducer
struct MyReducer {
    enum ViewAction {
        case increment
        case decrement
    }
    func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
        switch action {
        case .increment:
            state.counter += 1
            return .none
        case .decrement:
            state.counter -= 1
            return .none
        }
    }
}

@ViewState
struct MyView: View {
    @State var counter = 0
    
    let store: Store<MyReducer> = Store(reducer: MyReducer())

    var body: some View {
        VStack {
            Text("\(counter)")
            Button("+") {
                send(.increment)
            }
            Button("-") {
                send(.decrement)
            }
        }
    }
}

Events from the View are defined using ViewAction. Actions that should be kept private or used only in the Reducer should use ReducerAction.

ReducerAction

If there are Actions that you do not want to expose to View, ReducerAction is effective. This is the sample code:

@Reducer
struct MyReducer {
    enum ViewAction {
        case login
    }

    enum ReducerAction {
        case loginResponse(TaskResult<Response>)
    }

    @Dependency(\.authClient) var authClient

    func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
        switch action {
        case .login:
            return .run { [email = state.email, password = state.password] send in
                await send(
                    .loginResponse(
                        TaskResult { try await authClient.login(email, password) }
                    )
                )
            }
        case let .loginResponse(result):
            ...
            return .none
        }
    }
}

@ViewState
struct MyView: View {
    @State var email: String = ""
    @State var password: String = ""

    let store: Store<MyReducer>
    ...
}

ReducerState

Use ReducerState if you want to keep the state only in the Reducer. ReducerState is also effective to improve performance because the View is not updated even if the value is changed.

This is the example code

@Reducer
struct MyReducer {
    enum ViewAction {
        case increment
        case decrement
    }

    struct ReducerState {
        var totalCalledCount = 0
    }

    func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
        state.reducerState.totalCalledCount += 1
        switch action {
        case .increment:
            if state.reducerState.totalCalledCount < 10 {
                state.counter += 1
            }
            return .none
        case .decrement:
            state.counter -= 1
            return .none
        }
    }
}

@ViewState
struct MyView: View {
    ...
    init() {
        store = Store(reducer: MyReducer(), initialReducerState: MyReducer.ReducerState())
    }
    ...
}

Pullback Action

If you want to send the Action of the child Reducer to the parent Reducer, use pullback. This is the sample code.

@ViewState
struct ParentView: View {
    let store: Store<ParentReducer> = Store(reducer: ParentReducer())

    var body: some View {
        ChildView()
            .pullback(to: /ParentReducer.Action.child, parent: self)
    }
}

@Reducer
struct ParentReducer {
    enum ViewAction {
    }
    enum ReducerAction {
        case child(ChildReducer.Action)
    }

    func reduce(into state: StateContainer<ParentView>, action: Action) -> SideEffect<Self> {
        switch action {
        case .child(.onButtonTapped):
            // do something
            return .none
        }
    }
}

Macro

There are two macros in this library:

  • @Reducer
  • @ViewState

@Reducer

@Reducer is a macro that integrates ViewAction and ReducerAction to generate Action.

@Reducer
struct MyReducer {
    enum ViewAction {
        case loginButtonTapped
    }
    enum ReducerAction {
        case loginResponse(TaskResult<User>)
    }
    // expand to ↓
    enum Action {
        case loginButtonTapped
        case loginResponse(TaskResult<User>)
        
        init(viewAction: ViewAction) {
            switch viewAction {
            case .loginButtonTapped:
                self = .loginButtonTapped
            }
        }
        
        init(reducerAction: ReducerAction) {
            switch reducerAction {
            case .loginResponse(let arg1):
                self = .loginResponse(arg1)
            }
        }
    }
    ...
}

Reducer.reduce(into:action:) no longer needs to be prepared for two actions, ViewAction and ReducerAction, but can be integrated into Action.

@ViewState

@ViewState creates a ViewState structure and conforms it to the ActionSendable protocol.

@ViewState
struct MyView: View {
    @State var counter = 0

    let store: Store<MyReducer> = Store(reducer: MyReducer())

    var body: some View {
        VStack {
            Text("\(counter)")
            Button("+") {
                send(.increment)
            }
            Button("-") {
                send(.decrement)
            }
        }
    }
    // expand to ↓
    struct ViewState: ViewStateProtocol {
        var counter = 0
        static let keyPathMap: [PartialKeyPath<ViewState>: PartialKeyPath<MyView>] = [\.counter: \.counter]
    }
}

The ViewState structure serves two main purposes:

  • To make properties such as store and body of View inaccessible to Reducer.
  • To make it testable.

Also, By conforming to the ActionSendable protocol, you can send Actions to the Store.

Testing

For testing, we use TestStore. This requires an instance of the ViewState struct, which is generated by the @ViewState macro. Additionally, we'll conduct further operations to assert how its behavior evolves when an action is dispatched.

@MainActor
func testReducer() async {
    let store = MyView().testStore(viewState: .init())
}

Each step of the way we need to prove that state changed how we expect. For example, we can simulate the user flow of tapping on the increment and decrement buttons:

@MainActor
func testReducer() async {
    let store = MyView().testStore(viewState: .init())
    await store.send(.increment) {
        $0.count = 1
    }
    await store.send(.decrement) {
        $0.count = 0
    }
}

Furthermore, when effects are executed by steps and data is fed back into the store, it's necessary to assert on those effects.

@MainActor
func testReducer() async {
    let store = MyView().testStore(viewState: .init())
    await store.send(.fetchData)
    await store.receive(\.fetchDataResponse.success) {
        $0.data = ...
    }
}

If you're using swift-dependencies, you can perform dependency injection as follows:

let store = MyView().testStore(viewState: .init()) {
    $0.apiClient.fetchData = { _ in ... }
}

More Resources
to explore the angular.

mail [email protected] to add your project or resources here 🔥.

Related Articles
to learn about angular.

FAQ's
to learn more about Angular JS.

mail [email protected] to add more queries here 🔍.

More Sites
to check out once you're finished browsing here.

0x3d
https://www.0x3d.site/
0x3d is designed for aggregating information.
NodeJS
https://nodejs.0x3d.site/
NodeJS Online Directory
Cross Platform
https://cross-platform.0x3d.site/
Cross Platform Online Directory
Open Source
https://open-source.0x3d.site/
Open Source Online Directory
Analytics
https://analytics.0x3d.site/
Analytics Online Directory
JavaScript
https://javascript.0x3d.site/
JavaScript Online Directory
GoLang
https://golang.0x3d.site/
GoLang Online Directory
Python
https://python.0x3d.site/
Python Online Directory
Swift
https://swift.0x3d.site/
Swift Online Directory
Rust
https://rust.0x3d.site/
Rust Online Directory
Scala
https://scala.0x3d.site/
Scala Online Directory
Ruby
https://ruby.0x3d.site/
Ruby Online Directory
Clojure
https://clojure.0x3d.site/
Clojure Online Directory
Elixir
https://elixir.0x3d.site/
Elixir Online Directory
Elm
https://elm.0x3d.site/
Elm Online Directory
Lua
https://lua.0x3d.site/
Lua Online Directory
C Programming
https://c-programming.0x3d.site/
C Programming Online Directory
C++ Programming
https://cpp-programming.0x3d.site/
C++ Programming Online Directory
R Programming
https://r-programming.0x3d.site/
R Programming Online Directory
Perl
https://perl.0x3d.site/
Perl Online Directory
Java
https://java.0x3d.site/
Java Online Directory
Kotlin
https://kotlin.0x3d.site/
Kotlin Online Directory
PHP
https://php.0x3d.site/
PHP Online Directory
React JS
https://react.0x3d.site/
React JS Online Directory
Angular
https://angular.0x3d.site/
Angular JS Online Directory