apple/UIKit & ReactiveX

[ReactorKit] ReactorKit 공부하기 #6 transform

lgvv 2022. 9. 22. 14:24

ReactorKit 공부하기 #6 transform

 

 

✅ 결과물 UI

결과물 GIF

 

(ReactorKit 공식문서)

https://github.com/ReactorKit/ReactorKit

 

GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

A library for reactive and unidirectional Swift applications - GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

github.com

 

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.

 

(해석 요약)

ReactorKit의 경우에는 앱 상태에 대한 global state를 정의하지 않음.

그렇다는 것은 즉, global state를 정의하기 위해 어떠한 방법이든 자유롭게 사용 가능함.

global state를 사용하기 위해 BehaviorSubject, PublishSubject 심지어는 reactor를 사용할 수 있음.

ReactorKit은 앱에서 특정 기능을 위해 global state를 강요하지 않음.

 

Action -> Mutation -> State flow에는 global state가 없음.

global mutation을 사용하기 위해서는 transform(mutation: )을 통해 전환해야 함.

예를들면, 인증받은 사용자의 정보를 저장하는 경우 global BehaviorSubject가 있고, 현재 유저가 변화할 때 Mutation.setUser(User?)를 방출하고자 하면 아래 코드처럼 사용할 수 있음.

 

var currentUser: BehaviorSubject<User> // global state

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

 

현재 유저가 변할 때, mutation을 방출한다!

 

transform!

 

trnasform의 경우에는 3가지 메소드가 있음.

transform(action:) 은 Action을 방출하기 때문에 mutate(action:) 이 불리고

transform(state:) 은 State를 방출하기 때문에 reactor.state를 구독하고 viewController쪽의 코드를 바로 호출하게 된다.

 

 

 

✅ 내 코드 ✅

폴더 구조

 

여기서 화면 전환할 때 코드 주석을 잘 보기!

진짜 핵심!

//
//  CounterViewController.swift
//  AppleCollectionView
//
//  Created by Hamlit Jason on 2022/09/21.
//

import RxSwift
import RxCocoa
import ReactorKit
import UIKit
import FlexLayout
import PinLayout

class CounterViewController: UIViewController, View {
    // MARK: - Properties
    var disposeBag = DisposeBag()
    var reactor: CounterViewReactor
    
    // MARK: - Views
    var increaseButton = UIButton()
    var valueLabel = UILabel()
    var decreaseButton = UIButton()
    var activityIndicatorView = UIActivityIndicatorView()
    var userGreetingLabel = UILabel()
    
    // MARK: - Initalize
    init(reactor: CounterViewReactor) {
        self.reactor = reactor
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupViews()
        bind(reactor: reactor)
    }
    
    func bind(reactor: CounterViewReactor) {
        // MARK: - Action
        increaseButton.rx.tap
            .map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap
            .map { Reactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // 🕊️ 1. 화면전환에 Reactor 주입 및 transform 함수 반영
        self.navigationItem.rightBarButtonItem?.rx.tap
            .withUnretained(self)
            .bind { owner, _ in
                // 🕊️ 2. CounterViewController의 UILabel의 text값 가져오기
                let userName = owner.userGreetingLabel.text ?? ""
                // 🕊️ 3. settingReactor 세팅
                var settingReactor = SettingViewReactor(
                    provider: ServiceProvider(),
                    userName: userName
                )
                // 🕊️ 4. 리액터 연결 - 👉 매우중요
                settingReactor = reactor.reactorForSetting()
                // 🕊️ 5. 화면전환 설정
                let settingViewController = UINavigationController(
                    rootViewController: SettingViewContrller(reactor: settingReactor)
                )
                // 🕊️ 6. 화면전환
                owner.present(settingViewController, animated: true)
            }
            .disposed(by: disposeBag)
        
        
        // MARK: - State
        reactor.state
            .map { $0.value }
            .distinctUntilChanged() // 값이 변화해야 내려감
            .map { "\($0)"}
            .bind(to: valueLabel.rx.text)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged()
            .bind(to: activityIndicatorView.rx.isAnimating)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.userName }
            .distinctUntilChanged()
            .map {
                $0.isEmpty
                ? $0 + "환영합니다. 🐈"
                : $0 + "님 안녕하세요. 😁"
            }
            .bind(to: userGreetingLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

extension CounterViewController {
    func setupViews() {
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "설정",
            style: .plain,
            target: self,
            action: nil
        )
        
        [increaseButton, valueLabel, decreaseButton, activityIndicatorView, userGreetingLabel].forEach { view.addSubview($0) }
        
        increaseButton.setImage(UIImage(systemName: "plus"), for: .normal)
        increaseButton.snp.makeConstraints {
            $0.centerX.equalToSuperview().offset(-100)
            $0.centerY.equalToSuperview()
        }
        
        valueLabel.text = "0"
        valueLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.centerY.equalToSuperview()
        }
        
        decreaseButton.setImage(UIImage(systemName: "minus"), for: .normal)
        decreaseButton.snp.makeConstraints {
            $0.centerX.equalToSuperview().offset(100)
            $0.centerY.equalToSuperview()
        }
        
        userGreetingLabel.text = "안녕하세요 ✅"
        userGreetingLabel.snp.makeConstraints {
            $0.top.equalToSuperview().offset(100)
            $0.centerX.equalToSuperview()
        }
        
        activityIndicatorView.snp.makeConstraints {
            $0.top.equalTo(userGreetingLabel.snp.bottom).offset(30)
            $0.centerX.equalToSuperview()
        }
    }
}

 

CounterReactor로 transform이 여기서 구현되어 있음

추가로 RxTodo의 ServiceProvider를 사용함.

 

고양이 주석 7번까지 잘 봐주기

//
//  CounterViewReactor.swift
//  AppleCollectionView
//
//  Created by Hamlit Jason on 2022/09/21.
//

import RxSwift
import RxCocoa
import ReactorKit
import UIKit

class CounterViewReactor: Reactor {
    enum Action {
        case increase
        case decrease
    }
    
    enum Mutation {
        case increaseValue
        case decreaseValue
        case setLoading(Bool)
        case updateUserName(String)
    }
    
    struct State {
        var value: Int = 0
        var isLoading: Bool = false
        var userName: String = ""
    }
    
    let initialState: State
    let provider: ServiceProviderProtocol
    
    init(provider: ServiceProviderProtocol) {
        self.initialState = State()
        self.provider = provider
    }
    
    // MARK: - transform 핵심
    // 🐈 1. transform Mutation으로 선언
    func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
        // 🐈 2. provider를 통해서 userService의 값 가져오기 // 🐬 2. 여기서 이벤트 꺼내오기
        let eventMutation = provider.userService.event.flatMap { event -> Observable<Mutation> in
            // 🐈 3. UserEvent를 enum으로 정의하여 해당하는 케이스 가져오기
            switch event {
            case .updateUserName(let name):
                // 🐈 4. 값 방출!
                return .just(.updateUserName(name)) // -> 이거 연산 프로퍼티라 eventMutation의 값을 설정함.
            }
        }
        
        // 🐈 5. 함수 리턴! merge를 통해 기존 mutaion에 새러운 mutation을 합쳐줌
        return Observable.merge(mutation, eventMutation)
        
    }
    
    func mutate(action: CounterViewReactor.Action) -> Observable<CounterViewReactor.Mutation> {
        switch action {
        case .increase:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.increaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])
        case .decrease:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])
        }
    }
    
    func reduce(state: CounterViewReactor.State, mutation: CounterViewReactor.Mutation) -> CounterViewReactor.State {
        var newState = state
        switch mutation {
        case .increaseValue:
            newState.value += 1
        case .decreaseValue:
            newState.value -= 1
        case .setLoading(let isLoading):
            newState.isLoading = isLoading
        // 🐈 6. reduce에서 값 변화를 처리할 수 있음.
        case .updateUserName(let name):
            newState.userName = name
        }
        return newState
    }
    
    // 🐈 7. global state를 처리하기 위한 매우 중요한 작업!
    // CounterViewReactor에서 SettingViewReactor를 return해 주어야 값 처리가 가능함.
    func reactorForSetting() -> SettingViewReactor {
        return SettingViewReactor(
            provider: provider,
            userName: currentState.userName
        )
    }
}

 

세팅의 경우에는 약간 이슈가 있었는데, 

distinctUntilChanged를 통해 값이 변화할 때, dismiss하려고 했는데, 화면이 present되자마자 dismiss되는 현상이 있었음.

로그를 찍어보니까 init될 때 subscribe되고 초기값이 방출되고 있었음.

그래서 그냥 skip(1)으로 처리했는데, 약간 아쉬움이 있음.

 

//
//  SettingViewContrller.swift
//  AppleCollectionView
//
//  Created by Hamlit Jason on 2022/09/22.
//

import UIKit
import ReactorKit
import RxSwift

class SettingViewContrller: UIViewController, View {
    
    // MARK: - Properties
    var disposeBag = DisposeBag()
    var reactor: SettingViewReactor
    
    // MARK: - Views
    var cancelButton = UIBarButtonItem(title: "취소", style: .plain, target: self, action: nil)
    var saveButton = UIBarButtonItem(title: "성공", style: .plain, target: self, action: nil)
    var userNameLabel = UILabel()
    var userNameTextField = UITextField()
    
    // MARK: - Initialize
    init(reactor: SettingViewReactor) {
        self.reactor = reactor
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupViews()
        bind(reactor: reactor)
    }
    
    // NOTE: -
    // Action에 대한 함수명을 dispatch로 사용하시는 분도 계심
    // State에 대한 함수명을 render로 사용하시는 분도 계심
    func bind(reactor: SettingViewReactor) {
        // MARK: - Action
        saveButton.rx.tap
            .withUnretained(self)
            .map { owner, _ in owner.userNameTextField.text ?? "" }
            .map { Reactor.Action.saveUserName($0) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        cancelButton.rx.tap
            .map { Reactor.Action.cancel }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // MARK: - State
        reactor.state
            .skip(1) // 처음에 불리는거 막기
            .map { $0.shouldDismissed }
            .withUnretained(self)
            .bind { owner, _ in
                owner.dismiss(animated: true, completion: nil)
            }
            .disposed(by: disposeBag)
    }
}

extension SettingViewContrller {
    func setupViews() {
        view.backgroundColor = .white
        self.navigationItem.leftBarButtonItem = cancelButton
        self.navigationItem.rightBarButtonItem = saveButton
        
        [userNameLabel, userNameTextField].forEach { view.addSubview($0) }
        
        userNameLabel.text = "사용자 이름"
        userNameLabel.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
        
        userNameTextField.borderStyle = .roundedRect
        userNameTextField.snp.makeConstraints {
            $0.top.equalTo(userNameLabel.snp.bottom).offset(10)
            $0.leading.trailing.equalToSuperview().inset(50)
        }
    }
}

 

세팅 리액터

여기는 돌고래 주석 잘 보기

여기서 함수를 불러주면 그 함수는 event를 호출해서 CounterReactor에서 결과저긍로 사용하게 된다.

//
//  SettingViewReactor.swift
//  AppleCollectionView
//
//  Created by Hamlit Jason on 2022/09/22.
//

import RxSwift
import ReactorKit

class SettingViewReactor: Reactor {
    enum Action {
        case saveUserName(String)
        case cancel
    }
    
    enum Mutation {
        case saveUserName(String)
        case dismiss
    }

    struct State {
        var userName: String
        var shouldDismissed: Bool

        // NOTE: - 초기 스테이트를 지정해주기 위함
        init(userName: String) {
            self.userName = userName
            self.shouldDismissed = false
        }
    }
    
    let initialState: State
    let provider: ServiceProviderProtocol
    
    init(provider: ServiceProviderProtocol, userName: String) {
        self.initialState = State(userName: userName)
        self.provider = provider
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case let .saveUserName(name):
            
            // 🐬 1. 여기서 함수를 호출하기!
            return provider.userService.updateUserName(to: name)
                .map { _ in .dismiss } // 저장하고나서 dismiss해야 하니까 dissmiss로 내보내!
        case .cancel:
            return .just(.dismiss)
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case let .saveUserName(name):
            newState.userName = name
        case .dismiss:
            print("reduce: dismiss called")
            newState.shouldDismissed = true
        }
        return newState
    }
}

 

Service 코드 모음

//  BaseService.swift

class BaseService {
  unowned let provider: ServiceProviderProtocol

  init(provider: ServiceProviderProtocol) {
    self.provider = provider
  }
}

//  ServiceProviderProtocol.swift

import Foundation

protocol ServiceProviderProtocol: class {
    var userService: UserServiceProtocol { get }
}

final class ServiceProvider: ServiceProviderProtocol {
    lazy var userService: UserServiceProtocol = UserService(provider: self)
}

//  UserService.swift

import Foundation
import RxSwift

// 🦥 1. 이벤트 정의
enum UserEvent {
    case updateUserName(String)
}

// 🦥 2. 프로토콜 정의
protocol UserServiceProtocol {
    var event: PublishSubject<UserEvent> { get }
    func updateUserName(to name: String) -> Observable<String>
}

// 🦥 3. 상속받아서 구현
class UserService: BaseService, UserServiceProtocol {
    let event = PublishSubject<UserEvent>()
    
    func updateUserName(to name: String) -> Observable<String> {
        // UserDefaults에 저장하는 등의 작업을 진행할 수 있을 것.
        event.onNext(.updateUserName(name))
        return .just(name)
    }
}

 

transform 사용 다르게 해보기.

아래 블로그를 참고

func transform(action: Observable<Action>) -> Observable<Action> {
    let eventAction = service.event.flatMap { event -> Observable<Action> in
        switch event {
        case .updateUserName:
            return .just(.increase)
        }
    }
    return Observable.merge(action, eventAction)
}

func transform(state: Observable<State>) -> Observable<State> {
    let eventState = service.event.flatMap { event -> Observable<State> in
        switch event {
        case .updateUserName(let name):
            return .just(State(value: 1, isLoading: false, userName: name))
        }
    }
    return Observable.merge(state, eventState)
}

 

 

(참고)

https://eunjin3786.tistory.com/147

 

[ReactorKit] ReactorKit의 Transform 함수에 대해 알아보자

ReactorKit문서 를 보면 transform 함수에 대한 설명이 있습니다. transform은 mutate나 reduce 함수처럼 Reactor에 반드시 구현해야하는 함수가 아닙니다. 그래서 저는 최근에 알았는데, 까먹지 않게 정리해

eunjin3786.tistory.com