Home

Awesome

Logo

GoodReactor

Check out the Documentation

iOS Version Swift Version Supported devices Contains Test Dependency Manager

GoodReactor is an adaptation of the Reactor framework that is Redux inspired. The view model communicates with the view controller via the State and with the Coordinator via the navigation function. You communicate to the viewModel via Actions Viewmodel changes state in the Reduce function Viewmodel interactes with dependencies outside of the Reduce function not to create side-effects

Link to the original reactor kit: https://github.com/ReactorKit/ReactorKit

Installation

Swift Package Manager

Create a Package.swift file and add the package dependency into the dependencies list. Or to integrate without package.swift add it through the Xcode add package interface.

import PackageDescription

let package = Package(
    name: "SampleProject",
    dependencies: [
        .package(url: "https://github.com/GoodRequest/GoodReactor" .upToNextMajor("2.0.0"))
    ]
)

Usage

GoodReactor

ViewModel

In your ViewModel define Actions, Mutations, Destinations and the State

@Observable final class ViewModel: Reactor {
    enum Action {
        case login(username: String, password: String)
    }

    enum Mutation {
        case didReceiveAuthResponse(Credentials)
    }

    enum Destination {
        case homeScreen
        case errorAlert
    }

    @Observable final class State {
        var username: String
        var password: String
    }
}

You can provide the initial state of the view in the makeInitialState function.

func makeInitialState() -> State {
    return State()
}

Finally in the reduce function you define how state changes, according to certain events:

typealias Event = NewReactor.Event<Action, Mutation, Destination>

func reduce(state: inout State, event: Event) {
    switch event.kind {
    case .action(.login(...)):
        // ...

    case .mutation:
        // ...

    case .destination:
        // ...
    }
}

You can run asynchronous tasks by using run and returning the result in form of a Mutation.

func reduce(state: inout State, event: Event) {
    switch event.kind {
    case .action(.login(let username, let password)):
        run(event) {
            let credentials = await networking.login(username, password)
            return Mutation.didReceiveAuthResponse(credentials)
        }

    // ...

    case .mutation(.didReceiveAuthResponse(let credentials)):
        // proceed with login
    }
}

You can listen to external changes by subscribe-ing to event Publisher-s. You start the subscriptions by calling the start() function.

// in ViewModel:
func transform() {
    subscribe {
        await ExternalTimer.shared.timePublisher
    } map: {
        Mutation.didChangeTime(seconds: $0)
    }
}

// in View (SwiftUI):
var body: some View {
    MyContentView()
        .task { viewModel.start() }
}

View (SwiftUI)

You add the ViewModel as a property wrapper to your view:

@ViewModel private var model = MyViewModel()

To access the current State you use:

// read-only access
Text(model.username)

// binding (refactored to a variable for better readability)
let binding = model.bind(\.username, action: { .setUsername($0) })
TextField("Username",  text: binding)

To send an event to the ViewModel you call:

model.send(action: .login(username, password))
model.send(destination: .errorAlert)

UIViewController (UIKit/Combine)

From UIViewController (in UIKit, or any other frameworks) you can send actions to ViewModel via Combine:

myButton.publisher(for: .touchUpInside).mapĀ { _ in .login(username, password) }
    .map { .action($0) }
    .subscribe(model.eventStream)
    .store(in: &cancellables)

Then use Combine to subscribe to state changes, so every time the state is changed, ViewController can be updated as well:

reactor.stateStream
    .map { String($0.username) }
    .assign(to: \.text, on: usernameLabel, ownership: .weak)
    .store(in: &cancellables)

License

GoodReactor repository is released under the MIT license. See LICENSE for details.