Convert Figma logo to code with AI

ReactorKit logoReactorKit

A library for reactive and unidirectional Swift applications

2,731
274
2,731
36

Top Related Projects

24,441

Reactive Programming in Swift

Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

4,232

A Swift binding framework

7,565

Unidirectional Data Flow in Swift - Inspired by Redux

15,190

Network abstraction layer written in Swift.

Quick Overview

ReactorKit is a framework for building reactive and unidirectional application architecture in Swift. It provides a set of tools and patterns to help developers create scalable and maintainable iOS applications using the Flux architecture pattern.

Pros

  • Unidirectional Data Flow: ReactorKit enforces a unidirectional data flow, which helps to simplify the application's state management and improve overall code organization.
  • Testability: The framework's design promotes testability, making it easier to write unit tests for the application's business logic.
  • Scalability: ReactorKit's architecture scales well as the application grows in complexity, making it suitable for large-scale projects.
  • Reactive Programming: The framework integrates with RxSwift, allowing developers to leverage the power of reactive programming for building reactive user interfaces.

Cons

  • Learning Curve: Developers new to the Flux architecture pattern or reactive programming may face a steeper learning curve when adopting ReactorKit.
  • Boilerplate Code: The framework's architecture can introduce some boilerplate code, which may be a concern for developers who prefer more concise codebases.
  • Dependency on RxSwift: ReactorKit is tightly coupled with the RxSwift library, which may be a drawback for developers who prefer a different reactive programming library.
  • Limited Documentation: While the project has good documentation, some developers may find the documentation could be more comprehensive, especially for newcomers to the framework.

Code Examples

Example 1: Creating a Reactor

import ReactorKit

class CounterReactor: Reactor {
    enum Action {
        case increment
        case decrement
    }

    enum Mutation {
        case incrementCount
        case decrementCount
    }

    struct State {
        var count: Int = 0
    }

    let initialState = State()

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increment:
            return .just(.incrementCount)
        case .decrement:
            return .just(.decrementCount)
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .incrementCount:
            newState.count += 1
        case .decrementCount:
            newState.count -= 1
        }
        return newState
    }
}

This example demonstrates the basic structure of a ReactorKit Reactor, which encapsulates the application's business logic. The Reactor defines the possible Actions, Mutations, and State for a simple counter feature.

Example 2: Binding a Reactor to a View

import ReactorKit
import RxSwift
import RxCocoa

class CounterViewController: UIViewController, View {
    typealias Reactor = CounterReactor

    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var incrementButton: UIButton!
    @IBOutlet weak var decrementButton: UIButton!

    var disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.reactor = CounterReactor()
    }

    func bind(reactor: CounterReactor) {
        // Action
        incrementButton.rx.tap
            .map { Reactor.Action.increment }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        decrementButton.rx.tap
            .map { Reactor.Action.decrement }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        // State
        reactor.state
            .map { $0.count }
            .bind(to: countLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

This example shows how to bind a Reactor to a View in ReactorKit. The bind(reactor:) method is responsible for connecting the user interactions (Actions) to the Reactor and updating the UI based on the Reactor's State.

Example 3: Testing a Reactor

import ReactorKit
import RxSwift
import RxTest
import XCTest

class Count

Competitor Comparisons

24,441

Reactive Programming in Swift

Pros of RxSwift

  • More comprehensive and feature-rich, offering a wide range of reactive programming tools
  • Larger community and ecosystem, with extensive documentation and third-party libraries
  • Direct integration with other RxSwift-based libraries and frameworks

Cons of RxSwift

  • Steeper learning curve due to its extensive API and concepts
  • Can lead to complex and hard-to-debug code if not used carefully
  • Heavier dependency compared to ReactorKit

Code Comparison

RxSwift:

Observable.from([1, 2, 3, 4, 5])
    .filter { $0 % 2 == 0 }
    .map { $0 * 2 }
    .subscribe(onNext: { print($0) })
    .disposed(by: disposeBag)

ReactorKit:

reactor.state.map { $0.numbers }
    .filter { $0 % 2 == 0 }
    .map { $0 * 2 }
    .bind(to: numberLabel.rx.text)
    .disposed(by: disposeBag)

ReactorKit builds on top of RxSwift, providing a more structured approach to building reactive apps. It offers a unidirectional data flow architecture, making it easier to manage state and separate concerns in larger applications. However, it may be overkill for smaller projects where RxSwift's flexibility might be sufficient.

Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

Pros of ReactiveCocoa

  • More mature and established project with a larger community
  • Supports both Objective-C and Swift
  • Offers a wider range of operators and utilities

Cons of ReactiveCocoa

  • Steeper learning curve due to its extensive API
  • Heavier framework with more overhead
  • Less opinionated architecture, which may lead to inconsistent implementations

Code Comparison

ReactorKit:

struct Reactor: ReactorKit.Reactor {
    enum Action {
        case increment
    }
    enum Mutation {
        case incrementCount
    }
    struct State {
        var count: Int = 0
    }
}

ReactiveCocoa:

class ViewModel {
    let (incrementSignal, incrementObserver) = Signal<Void, Never>.pipe()
    let count: Property<Int>
    
    init() {
        count = Property(initial: 0)
        incrementSignal.observeValues { [weak self] in
            self?.count.value += 1
        }
    }
}

ReactorKit provides a more structured approach with clear separation of concerns, while ReactiveCocoa offers more flexibility but requires more boilerplate code for similar functionality. ReactorKit's architecture is inspired by Redux and Flux, making it easier to reason about state changes and data flow in complex applications.

4,232

A Swift binding framework

Pros of Bond

  • More flexible and versatile, supporting a wider range of reactive programming scenarios
  • Offers a rich set of operators and combinators for data manipulation
  • Provides built-in support for UI binding, simplifying view updates

Cons of Bond

  • Steeper learning curve due to its more comprehensive feature set
  • Potentially higher memory footprint and performance overhead in simple use cases
  • Less opinionated architecture, which may lead to inconsistent code organization across projects

Code Comparison

Bond:

let text = Observable<String?>("")
let label = UILabel()
text.bind(to: label.reactive.text)

ReactorKit:

struct Reactor: ReactorKit.Reactor {
    enum Action { case updateText(String) }
    struct State { var text: String = "" }
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case let .updateText(newText):
            return .just(Mutation.setText(newText))
        }
    }
}

Summary

Bond offers more flexibility and built-in UI binding capabilities, making it suitable for a wide range of reactive programming scenarios. However, it comes with a steeper learning curve and potential performance overhead. ReactorKit, on the other hand, provides a more structured and opinionated approach to application architecture, which can lead to more consistent code organization across projects, but may be less flexible for certain use cases.

7,565

Unidirectional Data Flow in Swift - Inspired by Redux

Pros of ReSwift

  • Simpler architecture, easier to understand for beginners
  • More flexible, allowing for easier integration with existing projects
  • Larger community and ecosystem, with more resources and third-party libraries available

Cons of ReSwift

  • Less opinionated, which can lead to inconsistent implementations across projects
  • Lacks built-in side effect handling, requiring additional libraries or custom solutions
  • May result in more boilerplate code for complex applications

Code Comparison

ReSwift:

struct AppState {
    var counter: Int = 0
}

struct IncrementAction: Action {}

func counterReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()
    switch action {
    case _ as IncrementAction:
        state.counter += 1
    default:
        break
    }
    return state
}

ReactorKit:

final class CounterReactor: Reactor {
    enum Action {
        case increment
    }
    
    struct State {
        var counter: Int = 0
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increment:
            return Observable.just(Mutation.increaseCounter)
        }
    }
}

Both ReSwift and ReactorKit provide unidirectional data flow architectures for Swift applications, but they differ in their approach and complexity. ReSwift offers a more traditional Redux-like implementation, while ReactorKit combines concepts from Redux and ReactiveX for a more reactive approach.

15,190

Network abstraction layer written in Swift.

Pros of Moya

  • Simplifies network requests and API integration
  • Provides type-safe networking and automatic response mapping
  • Offers extensive plugin system for customization

Cons of Moya

  • Limited to networking tasks, not a complete app architecture
  • May introduce additional complexity for simple API calls
  • Steeper learning curve for developers new to reactive programming

Code Comparison

Moya example:

let provider = MoyaProvider<MyAPI>()
provider.request(.userProfile) { result in
    switch result {
    case let .success(response):
        let data = response.data
        // Handle the response data
    case let .failure(error):
        // Handle the error
    }
}

ReactorKit example:

class MyReactor: Reactor {
    enum Action {
        case fetchProfile
    }
    enum Mutation {
        case setProfile(User)
    }
    struct State {
        var user: User?
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .fetchProfile:
            return userService.fetchProfile()
                .map { Mutation.setProfile($0) }
        }
    }
}

While Moya focuses on simplifying network requests, ReactorKit provides a more comprehensive architecture for managing app state and UI interactions. Moya is ideal for projects with complex API integrations, while ReactorKit suits applications requiring a structured approach to state management and UI updates.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

ReactorKit

Swift CocoaPods Platform CI Codecov

ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit and describes how to build an application using ReactorKit.

You may want to see the Examples section first if you'd like to see the actual code. For an overview of ReactorKit's features and the reasoning behind its creation, you may also check the slides from this introductory presentation over at SlideShare.

Table of Contents

Basic Concept

ReactorKit is a combination of Flux and Reactive Programming. The user actions and the view states are delivered to each layer via observable streams. These streams are unidirectional: the view can only emit actions and the reactor can only emit states.

flow

Design Goal

  • Testability: The first purpose of ReactorKit is to separate the business logic from a view. This can make the code testable. A reactor doesn't have any dependency to a view. Just test reactors and test view bindings. See Testing section for details.
  • Start Small: ReactorKit doesn't require the whole application to follow a single architecture. ReactorKit can be adopted partially, for one or more specific views. You don't need to rewrite everything to use ReactorKit on your existing project.
  • Less Typing: ReactorKit focuses on avoiding complicated code for a simple thing. ReactorKit requires less code compared to other architectures. Start simple and scale up.

View

A View displays data. A view controller and a cell are treated as a view. The view binds user inputs to the action stream and binds the view states to each UI component. There's no business logic in a view layer. A view just defines how to map the action stream and the state stream.

To define a view, just have an existing class conform a protocol named View. Then your class will have a property named reactor automatically. This property is typically set outside of the view.

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor

When the reactor property has changed, bind(reactor:) gets called. Implement this method to define the bindings of an action stream and a state stream.

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

Storyboard Support

Use StoryboardView protocol if you're using a storyboard to initialize view controllers. Everything is same but the only difference is that the StoryboardView performs a binding after the view is loaded.

let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately

class MyViewController: UIViewController, StoryboardView {
  func bind(reactor: MyViewReactor) {
    // this is called after the view is loaded (viewDidLoad)
  }
}

Reactor

A Reactor is an UI-independent layer which manages the state of a view. The foremost role of a reactor is to separate control flow from a view. Every view has its corresponding reactor and delegates all logic to its reactor. A reactor has no dependency to a view, so it can be easily tested.

Conform to the Reactor protocol to define a reactor. This protocol requires three types to be defined: Action, Mutation and State. It also requires a property named initialState.

class ProfileViewReactor: Reactor {
  // represent user actions
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

An Action represents a user interaction and State represents a view state. Mutation is a bridge between Action and State. A reactor converts the action stream to the state stream in two steps: mutate() and reduce().

flow-reactor

mutate()

mutate() receives an Action and generates an Observable<Mutation>.

func mutate(action: Action) -> Observable<Mutation>

Every side effect, such as an async operation or API call, is performed in this method.

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

reduce()

reduce() generates a new State from a previous State and a Mutation.

func reduce(state: State, mutation: Mutation) -> State

This method is a pure function. It should just return a new State synchronously. Don't perform any side effects in this function.

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of the old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return the new state
  }
}

transform()

transform() transforms each stream. There are three transform() functions:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

Implement these methods to transform and combine with other observable streams. For example, transform(mutation:) is the best place for combining a global event stream to a mutation stream. See the Global States section for details.

These methods can be also used for debugging purposes:

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

Advanced

Global States

Unlike Redux, ReactorKit doesn't define a global app state. It means that you can use anything to manage a global state. You can use a BehaviorSubject, a PublishSubject or even a reactor. ReactorKit doesn't force to have a global state so you can use ReactorKit in a specific feature in your application.

There is no global state in the Action → Mutation → State flow. You should use transform(mutation:) to transform the global state to a mutation. Let's assume that we have a global BehaviorSubject which stores the current authenticated user. If you'd like to emit a Mutation.setUser(User?) when the currentUser is changed, you can do as following:

var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

Then the mutation will be emitted each time the view sends an action to a reactor and the currentUser is changed.

View Communication

You must be familiar with callback closures or delegate patterns for communicating between multiple views. ReactorKit recommends you to use reactive extensions for it. The most common example of ControlEvent is UIButton.rx.tap. The key concept is to treat your custom views as UIButton or UILabel.

view-view

Let's assume that we have a ChatViewController which displays messages. The ChatViewController owns a MessageInputView. When an user taps the send button on the MessageInputView, the text will be sent to the ChatViewController and ChatViewController will bind in to the reactor's action. This is an example MessageInputView's reactive extension:

extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}

You can use that extension in the ChatViewController. For example:

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)

Testing

ReactorKit has a built-in functionality for a testing. You'll be able to easily test both a view and a reactor with a following instruction.

What to test

First of all, you have to decide what to test. There are two things to test: a view and a reactor.

  • View
    • Action: is a proper action sent to a reactor with a given user interaction?
    • State: is a view property set properly with a following state?
  • Reactor
    • State: is a state changed properly with an action?

View testing

A view can be tested with a stub reactor. A reactor has a property stub which can log actions and force change states. If a reactor's stub is enabled, both mutate() and reduce() are not executed. A stub has these properties:

var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions

Here are some example test cases:

func testAction_refresh() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. send an user interaction programmatically
  view.refreshControl.sendActions(for: .valueChanged)

  // 4. assert actions
  XCTAssertEqual(reactor.stub.actions.last, .refresh)
}

func testState_isLoading() {
  // 1. prepare a stub reactor
  let reactor = MyReactor()
  reactor.isStubEnabled = true

  // 2. prepare a view with a stub reactor
  let view = MyView()
  view.reactor = reactor

  // 3. set a stub state
  reactor.stub.state.value = MyReactor.State(isLoading: true)

  // 4. assert view properties
  XCTAssertEqual(view.activityIndicator.isAnimating, true)
}

Reactor testing

A reactor can be tested independently.

func testIsBookmarked() {
  let reactor = MyReactor()
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, true)
  reactor.action.onNext(.toggleBookmarked)
  XCTAssertEqual(reactor.currentState.isBookmarked, false)
}

Sometimes a state is changed more than one time for a single action. For example, a .refresh action sets state.isLoading to true at first and sets to false after the refreshing. In this case it's difficult to test state.isLoading with currentState so you might need to use RxTest or RxExpect. Here is an example test case using RxSwift:

func testIsLoading() {
  // given
  let scheduler = TestScheduler(initialClock: 0)
  let reactor = MyReactor()
  let disposeBag = DisposeBag()

  // when
  scheduler
    .createHotObservable([
      .next(100, .refresh) // send .refresh at 100 scheduler time
    ])
    .subscribe(reactor.action)
    .disposed(by: disposeBag)

  // then
  let response = scheduler.start(created: 0, subscribed: 0, disposed: 1000) {
    reactor.state.map(\.isLoading)
  }
  XCTAssertEqual(response.events.map(\.value.element), [
    false, // initial state
    true,  // just after .refresh
    false  // after refreshing
  ])
}

Pulse

Pulse has diff only when mutated To explain in code, the results are as follows.

var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

let oldMessagePulse: Pulse<String?> = messagePulse
messagePulse.value = "Hello tokijh" // add valueUpdatedCount +1

oldMessagePulse.valueUpdatedCount != messagePulse.valueUpdatedCount // true
oldMessagePulse.value == messagePulse.value // true

Use when you want to receive an event only if the new value is assigned, even if it is the same value. like alertMessage (See follows or PulseTests.swift)

// Reactor
private final class MyReactor: Reactor {
  struct State {
    @Pulse var alertMessage: String?
  }

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .alert(message):
      return Observable.just(Mutation.setAlertMessage(message))
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    var newState = state

    switch mutation {
    case let .setAlertMessage(alertMessage):
      newState.alertMessage = alertMessage
    }

    return newState
  }
}

// View
reactor.pulse(\.$alertMessage)
  .compactMap { $0 } // filter nil
  .subscribe(onNext: { [weak self] (message: String) in
    self?.showAlert(message)
  })
  .disposed(by: disposeBag)

// Cases
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called
reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction)    // showAlert() is not called

Examples

  • Counter: The most simple and basic example of ReactorKit
  • GitHub Search: A simple application which provides a GitHub repository search
  • RxTodo: iOS Todo Application using ReactorKit
  • Cleverbot: iOS Messaging Application using Cleverbot and ReactorKit
  • Drrrible: Dribbble for iOS using ReactorKit (App Store)
  • Passcode: Passcode for iOS RxSwift, ReactorKit and IGListKit example
  • Flickr Search: A simple application which provides a Flickr Photo search with RxSwift and ReactorKit
  • ReactorKitExample
  • reactorkit-keyboard-example: iOS Application example for develop keyboard-extensions using ReactorKit Architecture.
  • TinyHub: Use ReactorKit develop the Github client

Dependencies

Requirements

  • Swift 5
  • iOS 8
  • macOS 10.11
  • tvOS 9.0
  • watchOS 2.0

Installation

Podfile

pod 'ReactorKit'

Package.swift

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(url: "https://github.com/ReactorKit/ReactorKit.git", .upToNextMajor(from: "3.0.0"))
  ],
  targets: [
    .target(name: "MyTarget", dependencies: ["ReactorKit"])
  ]
)

ReactorKit does not officially support Carthage.

Cartfile

github "ReactorKit/ReactorKit"

Most Carthage installation issues can be resolved with the following:

carthage update 2>/dev/null
(cd Carthage/Checkouts/ReactorKit && swift package generate-xcodeproj)
carthage build

Contribution

Any discussions and pull requests are welcomed 💖

  • To development:

    TEST=1 swift package generate-xcodeproj
    
  • To test:

    swift test
    

Community

Join

Community Projects

Who's using ReactorKit


StyleShare Kakao Wantedly

DocTalk Constant Contact KT

Hyperconnect Toss LINE Pay

LINE Pay Kurly

Are you using ReactorKit? Please let me know!

Changelog

  • 2017-04-18
    • Change the repository name to ReactorKit.
  • 2017-03-17
    • Change the architecture name from RxMVVM to The Reactive Architecture.
    • Every ViewModels are renamed to ViewReactors.

License

ReactorKit is under MIT license. See the LICENSE for more info.