ReactorKit : https://github.com/ReactorKit/ReactorKit/tree/master
ReactorKit 은 단방향 흐름을 가진, 반응형 앱을 위한 프레임워크. 카카오, 토스를 포함한 많은 기업들이 사용하고 있다.
내부적으로 RxSwift를 활용하고 있으며, Reactor 라는 객체가 MVVM 의 ViewModel 과 비슷한 역할을 수행한다.
위와 같은 단방향성 흐름을 추구한다. 예를들어, 날씨 앱이라고 했을 때, 유저가 날씨를 요청하는 버튼을 클릭하고 서버에서 날씨를 받아오는 경우.
MVVM 패턴은 개발자들이 애용하는 디자인 패턴이지만, 회사에따라, 구현하는 개발자에 따라 템플릿이 조금씩 다르다. ViewModel 에 Input Output 을 선언하는 경우도 있고, 그렇지 않은 경우도 있다.
ReactorKit 은 Reactor 객체에 enum Action, Mutation, State 를 규격화 해놓았기 때문에 조금 더 개인의 차이를 줄이고 일관된 코드를 작성할 수 있다.
Soma Weather App 에서, 도시의 이름을 영어로 검색하면, 그 도시의 날씨를 보여주는 페이지를 ViewModel 에서 Reactor 로 리팩토링 해봤다.
https://github.com/socar-abel/Soma-WeatherApp
SearchViewModel
//
// SearchViewModel.swift
// Search
//
// Created by 김상우 on 2023/04/03.
// Copyright © 2023 soma. All rights reserved.
//
import Domain
import CommonUI
import RxSwift
import RxCocoa
protocol SearhViewModelProtocol {
func requestCityWeather(city: String?)
}
public class SearchViewModel: SearhViewModelProtocol {
let disposeBag = DisposeBag()
let weatherUseCase: WeatherUseCase
public weak var searchCoordinator: SearchCoordinator?
/// 도시 날씨 옵저빙
let weatherRelay = PublishRelay<WeatherVO>()
/// 에러 옵저빙
let errorRelay = PublishRelay<Bool>()
public init(weatherUseCase: WeatherUseCase) {
self.weatherUseCase = weatherUseCase
}
func requestCityWeather(city: String?) {
guard let city = city else { return }
weatherUseCase.getCityWeather(city: city)
.subscribe(onSuccess: { [weak self] response in
guard let response = response else { return }
self?.weatherRelay.accept(response)
}).disposed(by: disposeBag)
}
}
SearchViewReactor
//
// SearchViewReactor.swift
// Search
//
// Created by abel on 2023/05/29.
// Copyright © 2023 soma. All rights reserved.
//
import ReactorKit
import RxSwift
import Domain
public final class SearchViewReactor: Reactor {
private let disposeBag = DisposeBag()
private let weatherUseCase: WeatherUseCase
public var initialState: State
public enum Action {
/// 도시의 날씨 검색
case searchCityWeather(String)
}
public enum Mutation {
/// 도시 날씨 정보 세팅
case setCityWeather(WeatherVO)
/// 로딩 중 세팅
case setLoading(Bool)
}
public struct State {
/// 검색한 도시의 날씨
var weather: WeatherVO?
/// 로딩 중
var isLoading: Bool
}
public init(weatherUseCase: WeatherUseCase) {
self.weatherUseCase = weatherUseCase
initialState = State(
weather: nil,
isLoading: false
)
}
// Action -> Mutation
public func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .searchCityWeather(cityName):
return performSearchCityWeather(cityName: cityName)
}
}
// Mutation -> State
public func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let .setCityWeather(weather):
state.weather = weather
case let .setLoading(isLoading):
state.isLoading = isLoading
}
return state
}
/// 도시의 날씨 가져오기
func performSearchCityWeather(cityName: String) -> Observable<Mutation> {
if currentState.isLoading { return .empty() }
return .concat(
.just(.setLoading(true)),
weatherUseCase.getCityWeather(city: cityName)
.asObservable()
.catchAndReturn(nil)
.compactMap { $0 }
.map { .setCityWeather($0) },
.just(.setLoading(false))
)
}
}