/CombineReactor

A wrapper for Combine, inspired by ReactorKit

Primary LanguageSwiftMIT LicenseMIT

CombineReactor

Version License Platform

CombineReactor is a ReactorKit inspired wrapper for Swift Combine. Includes a "Binder" to add bindable capabilities to default implementation by conforming to ReactiveCompatible and extending Reactive. See the example below.

Contributions and feedback are welcome and appreciated.

Example Usage

Reactor

import Foundation
import Combine

import CombineReactor

class CounterReactor: Reactor {
    enum Action {
        case decrease
        case increase
    }

    enum Mutation {
        case decreaseValue
        case increaseValue
        case setIsLoading(Bool)
    }

    struct State {
        var value: Int = 0
        var isLoading: Bool = false
    }

    let initialState = State()

    func mutate(action: Action) -> AnyPublisher<Mutation, Never> {
        switch action {
        case .decrease:
            let step: AnyPublisher<Mutation, Never> = Empty()
                .append(.decreaseValue)
                .delay(for: .seconds(1), scheduler: RunLoop.current)
                .eraseToAnyPublisher()
            
            return Empty()
                .append(.setIsLoading(true))
                .append(step)
                .append(.setIsLoading(false))
                .eraseToAnyPublisher()
            
        case .increase:
            let step: AnyPublisher<Mutation, Never> = Empty()
                .append(.increaseValue)
                .delay(for: .seconds(1), scheduler: RunLoop.current)
                .eraseToAnyPublisher()
        
            return Empty()
                .append(.setIsLoading(true))
                .append(step)
                .append(.setIsLoading(false))
                .eraseToAnyPublisher()
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        
        switch mutation {
        case .decreaseValue:
            newState.value -= 1
            
        case .increaseValue:
            newState.value += 1
            
        case .setIsLoading(let isLoading):
            newState.isLoading = isLoading
        }
        
        return newState
    }
}

ReactiveCompatible & Reactive

extension UILabel: ReactiveCompatible {}

extension Reactive where Base: UILabel {
   var text: Binder<String?> {
        Binder(base) { (label, text) in
            label.text = text
        }
    }
}

extension UIActivityIndicatorView: ReactiveCompatible {}

extension Reactive where Base: UIActivityIndicatorView {
   var isAnimating: Binder<Bool> {
        Binder(base) { (activityIndicator, isAnimating) in
            isAnimating ? activityIndicator.startAnimating() : activityIndicator.stopAnimating()
        }
    }
}

ViewController & View

extension ViewController: View {
    func bind(reactor: CounterReactor) {
        buttonActionPassthrough
            .eraseToAnyPublisher()
            .sink(receiveValue: reactor.action.send)
            .store(in: &cancellables)
            
        reactor.state
            .map { $0.value }
            .map { String(format: "%i", $0) }
            .bind(to: valueLabel.r.text )
            .store(in: &cancellables)

        reactor.state
            .map { $0.isLoading }
            .removeDuplicates()
            .bind(to: activityIndicator.r.isAnimating)
            .store(in: &cancellables)
    }
}

Installation

Cocoapods

pod 'CombineReactor', git: 'https://github.com/ilendemli/CombineReactor.git'

SPM

.package(url: "https://github.com/ilendemli/CombineReactor.git", .upToNextMajor(from: "0.1"))

Other Managers

Feel free to create a pull request

Requirements

  • Swift 5.1
  • iOS 13
  • watchOS 6
  • tvOS 13
  • macOS 10.15

License

CombineReactor is available under the MIT license. See the LICENSE file for more info.