Skip to content

Latest commit

 

History

History
280 lines (222 loc) · 9.45 KB

README.md

File metadata and controls

280 lines (222 loc) · 9.45 KB
OneWay

OneWayKit ♻️

release license license license

OneWayKit is a reactive unidirectional architecture library built with Combine. It allows you to update the state based on user actions and reflect those changes in the view seamlessly. The library is designed to be simple and easy to integrate into any feature of your application.

Table of Contents

Basic Usage

1. State, Action, Updater, and Middleware are defined through ViewFeature.

ViewFeature

ViewFeature defines four key components:

  • State: Represents the state of the view.
  • Action: Captures user interactions.
  • Updater: Updates the state based on actions.
  • Middleware: Handles asynchronous operations in between.
struct TimerFeature: ViewFeature {
    
    struct State: ViewState {
        var currentTime: Float = 0
        var isStarted: Bool = false
        var interval: TimeInterval = 0.1
    }
    
    enum Action: ViewAction {
        case start
        case add
        case tapRightButton
        case toggleStart
    }
    
    static var updater: Updater = { state, action in
        var newState = state
        switch action {
            
        case .add:
            newState.currentTime += Float(state.interval)
            
        case .toggleStart:
            newState.isStarted.toggle()
            
        default: break
        }
        
        return newState
    }
    
    static var middlewares: [Middleware]? = [TimerMiddleware()]
    
}

Middleware

Middleware plays a key role in handling asynchronous tasks by processing actions from a specific feature and emitting desired actions. Using Combine, it can also emit periodic actions, and subscriptions can be canceled using the cancel(for:) method.

final class TimerMiddleware: Middleware {
    
    func send(_ action: ViewAction, currentState: any ViewState) -> AnyPublisher<ViewAction, Never> {
        guard let currentState = currentState as? TimerFeature.State else {
            return Empty().eraseToAnyPublisher()
        }
        
        switch action as? TimerFeature.Action {
            
        case .start:
            return Timer.publish(every: currentState.interval, on: .main, in: RunLoop.Mode.common)
                 .autoconnect()
                 .map { _ in
                     TimerFeature.Action.add
                 }
                 .eraseToAnyPublisher()
            
        case .tapRightButton:
            if currentState.isStarted {
                return Publishers.Merge(
                    Just(TimerFeature.Action.cancel(for: TimerFeature.Action.start)),
                    Just(TimerFeature.Action.toggleStart)
                )
                .eraseToAnyPublisher()
            } else {
                return Publishers.Merge(
                    Just(TimerFeature.Action.start),
                    Just(TimerFeature.Action.toggleStart)
                )
                .eraseToAnyPublisher()
            }
            
        default:
            return Empty().eraseToAnyPublisher()
        }
    }
}

2. Now, we use the previously defined ViewFeature to create a OneWay that represents the unidirectional flow of the view and set the initial state.

final class TimerViewController: UIViewController {
    
    private let oneway = OneWay<TimerFeature>(initialState: .init())
    ...

3. Subscribe to the state of OneWay to bind and update the UI accordingly when changes occur.

    private func setupOneWay() {
        oneway.statePublisher
            .map { String(format: "%.1f", $0.currentTime) }
            .assign(to: \.text, on: timeLabel)
            .store(in: &cancellables)
        
        oneway.statePublisher
            .map { $0.isStarted }
            .removeDuplicates()
            .sink { [weak self] isStarted in
                self?.navigationItem.rightBarButtonItem?.title = isStarted ? "Stop" : "Start"
            }
            .store(in: &cancellables)
    }

4. Send actions corresponding to user interactions.

    @objc private func tapLeftButton() {
        oneway.send(.left)
    }
    
    @objc private func tapRightButton() {
        oneway.send(.right)
    }
    
    @objc private func tapUpButton() {
        oneway.send(.up)
    }
    
    @objc private func tapDownButton() {
        oneway.send(.down)
    }

Key Features

  1. Tracer

When creating onewaykit, set the context first.

    private lazy var oneway = OneWay<TracerFeature>(
        initialState:
            .init(
                position: .init(x: view.center.x, y: view.center.y),
                size: .init(width: 100, height: 100)
            ),
        context: TracerViewController.self
    )

If you want to track actions and state changes, you can use shouldTrace to trace them when sending actions, as shown below:

    @objc private func tapLeftButton() {
        oneway.send(.left, shouldTrace: true)
    }

By setting shouldTrace to true, you can observe events that capture the context, action, and how the state changes as a result, as shown below:

스크린샷 2024-12-24 오후 11 31 19
  1. Global

First, register the ViewFeature to be used globally and set the initial state.

   GlobalOneWay.registerState(feature: GlobalFeature.self, initialState: .init())

Then, you can subscribe to and use the state of the desired global feature.

        GlobalOneWay.state(feature: GlobalFeature.self)?
            .map { $0.backgroundColor }
            .sink { [weak self] in
                self?.titleLabel.backgroundColor = $0
            }
            .store(in: &cancellables)

You can also send actions to the global feature, of course.

GlobalOneWay.send(feature: GlobalFeature.self, .setBackgroundColor(.white))

Examples

This project allows you to add a To-Do List and learn how to detect child actions and update the parent view accordingly.

This project allows you to learn how to implement a timer asynchronously using Middleware and handle cancellation of subscribed events.

This project allows you to learn how to detect triggered actions and track state changes accordingly.

This project allows you to create a global feature, subscribe to it from multiple views, and dispatch actions accordingly.

Installation

You can install OneWayKit via Swift Package Manager by adding the following line to your Package.swift:

import PackageDescription

let package = Package(
    [...]
    dependencies: [
        .package(url: "https: github.com/fomagran/OneWayKit", from: "1.2.0"),
    ]
)

References

The following projects have greatly inspired the creation of OneWayKit.

Author

Fomagran, [email protected]

License

This library is released under the MIT license. See LICENSE for details.