ReactorKit 공부하기 #6 transform
리액터킷에서 다른 리액터와 합성을 통해 이벤트를 전달받는 transform을 공부해보고자 함.
결과물 UI
ReacotrKit Global States
ReactorKit의 경우에는 앱 상태에 대한 global state를 정의하지 않음.
- Global State를 정의하기 위해 어떠한 방법이든 자유롭게 사용 가능
- Global State를 사용하기 위해 BehaviorSubject, PublishSubject 심지어는 Reactor 자체를 사용할 수 있음.
- ReactorKit은 앱에서 특정 기능을 위해 Global State를 강제하지 않음
- 장점: 이에 따라 구현의 자율성 증가
- 단점: 개발자의 숙련도에 따라 난해해질 수 있음.
- Action -> Mutation -> State 플로우를 보면 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))
}
ReacotrKit trasform()
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를 사용함.
//
// 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)
}
'apple > RxSwift, ReactorKit' 카테고리의 다른 글
ReactorKit + RxDataSources #1(SectionModelType) (0) | 2022.10.05 |
---|---|
[ReactorKit] ReactorKit 공부하기 #7 View (programming) (0) | 2022.10.01 |
[ReactorKit] ReactorKit 공부하기 #5 RxTodo 따라잡기 (3) (0) | 2022.09.12 |
[ReactorKit] ReactorKit 공부하기 #4 RxTodo 따라잡기 (2) (0) | 2022.09.08 |
[ReactorKit] ReactorKit 공부하기 #3 RxTodo 따라잡기 (1) (0) | 2022.09.07 |