[ReactorKit#2] Swift ReactorKit Example Counter 카피, 분석하기
https://github.com/ReactorKit/ReactorKit/tree/master/Examples/Counter
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
ReactorKit 활용을 위해 몇가지 예제를 실습해보고 있다. 그 첫번째 예제이다.
정말 간단한 예제이지만, Action, State, Mutate 등 ReactorKit 활용 방법을 잘 설명해주고 있다.
같이 예제를 완성해보자
1. 사전 준비
1.1 Project 생성
1.2 불필요한 file, code 제거
1.2.1 SceneDelegate.swift 삭제
1.2.2 AppDelegate.swift 코드 제거
1.2.3 Info.plist 요소 제거
Info.plist -> Information Property List -> Application Scene Manifest -> "Scene Configuration" 제거
1.2.4 필요 라이브러리 다운
cocoapod을 통해 "ReactorKit", "RxSwift", "RxCocoa" install
* 참고
- https://zeddios.tistory.com/25
왕 초보를 위한 CocoaPods(코코아팟) 사용법 (Xcode와 연동)
안녕하세요! 오늘은 CocoaPod사용법에 대해 알려드릴려고해요 :) 저는 CocoaPod 처음에 시작할 때 뭐가 뭔지 몰라서 정말 하나도 몰라서 진짜 어려운거구나...라고 생각했었어요. 하지만 한번 배워 놓
zeddios.tistory.com
2. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window:UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let viewController = UIApplication.shared.windows.first?.rootViewController as! ViewController
viewController.reactor = ViewReactor()
return true
}
}
- UIWindow 객체를 통해 "rootViewController"인 ViewController의 객체(viewController) 생성
- 해당 객체에 reactor property 추가 // ViewReactor는 Custom Class
3. ViewReactor.swift
- ViewReactor.swift 생성
- 필요 모듈 import
import ReactorKit
import RxSwift
- Action, Mutation, State 선언
class ViewReactor: Reactor {
// Action is an user interaction
enum Action {
case increase // Count Increase Event
case decrease // Count Decrease Event
}
// Mutate is a state manipulator which is not exposed to a view
enum Mutation {
case increaseValue // Counter Value Increase
case decreaseValue // Counter Value Decrease
case setLoading(Bool) // is executing Event?
case setAlertMessage(String)
}
// State is a current view state
struct State {
var value: Int // Counter value
var isLoading: Bool // // is executing Event?
@Pulse var alertMessage: String?
}
}
- state 초기화
let initialState: State
init() {
self.initialState = State(
value: 0, // start from 0
isLoading: false
)
}
- mutate() 구현
// Action -> Mutation
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase: // View로부터의 Action이 increase
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.increaseValue)
.delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
Observable.just(Mutation.setAlertMessage("increased!")),
])
case .decrease: // View로부터의 Action이 decrease
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.decreaseValue)
.delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
Observable.just(Mutation.setAlertMessage("decreased!")),
])
}
}
* Observable이란 ?
http://reactivex.io/documentation/ko/observable.html
ReactiveX - Observable
Observable ReactiveX에서 옵저버는 Observable을 구독한다. Obseravable이 배출하는 하나 또는 연속된 항목에 옵저버는 반응한다. 이러한 패턴은 동시성 연산을 가능하게 한다. 그 이유는 Observable이 객체를
reactivex.io
- reduce() 구현
// Mutation -> State
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .increaseValue:
state.value += 1
case .decreaseValue:
state.value -= 1
case .setLoading(let bool):
state.isLoading = bool
case .setAlertMessage(let string):
state.alertMessage = string
}
return state
}
- 결과물
//
// ViewReactor.swift
// jh_counter
//
// Created by 추경민 on 2021/10/27.
//
import ReactorKit
import RxSwift
class ViewReactor: Reactor {
// Action is an user interaction
enum Action {
case increase
case decrease
}
// Mutate is a state manipulator which is not exposed to a view
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
case setAlertMessage(String)
}
// State is a current view state
struct State {
var value: Int
var isLoading: Bool
@Pulse var alertMessage: String?
}
let initialState: State
init() {
self.initialState = State(
value: 0, // start from 0
isLoading: false
)
}
// Action -> Mutation
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.increaseValue)
.delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
Observable.just(Mutation.setAlertMessage("increased!")),
])
case .decrease:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.decreaseValue)
.delay(.milliseconds(500), scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false)),
Observable.just(Mutation.setAlertMessage("decreased!")),
])
}
}
// Mutation -> State
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .increaseValue:
state.value += 1
case .decreaseValue:
state.value -= 1
case .setLoading(let bool):
state.isLoading = bool
case .setAlertMessage(let string):
state.alertMessage = string
}
return state
}
}
4. ViewController.swift
- 필요 모듈 import
import ReactorKit
import RxCocoa
import RxSwift
- View item 추가 / 이후 main.storyboard에서 연결할 예정
@IBOutlet var decreaseButton: UIButton!
@IBOutlet var increaseButton: UIButton!
@IBOutlet var valueLabel: UILabel!
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
- DisposeBag 객체 선언
var disposeBag = DisposeBag()
* Dispose, DisposeBag이란?
Observable은 subscribe 이후 complete, error 이벤트를 만나기 전까지 "계속" next 이벤트를 발생시킨다. 이는 메모리 누수(릭)로 이어진다. 메모리 누수를 부르는 subscribe된 Observable를 dispose해주는 역할을 한다. DisposeBag은 Observable 객체 하나하나를 dispose해야하는 귀찮음을 해결해준다.
- bind() function 추가
- View item tap action과 reactor.action mapping, binding
// Called when the new value is assigned to 'self.reactor'
func bind(reactor: ViewReactor) {
// Action
increaseButton.rx.tap // Tap event
.map { Reactor.Action.increase } // Convert to Action.increase
.bind(to: reactor.action)
.disposed(by: disposeBag)
decreaseButton.rx.tap // Tap event
.map { Reactor.Action.decrease } // Convert to Action.decrease
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
- View item value와 reactor.state mapping, binding
// State
reactor.state.map { $0.value } // 10
.distinctUntilChanged()
.map { "\($0)" } // "10"
.bind(to: valueLabel.rx.text) // Bind to valueLabel
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading }
.distinctUntilChanged()
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
* "$0" ?
: 추후 추가
* distinctUntilChanged()의 역할?
: 추후 추가
-
reactor.pulse(\.$alertMessage)
.compactMap { $0 }
.subscribe(onNext: { [weak self] message in
let alertController = UIAlertController(
title: nil,
message: message,
preferredStyle: .alert
)
alertController.addAction(UIAlertAction(
title: "OK",
style: .default,
handler: nil
))
self?.present(alertController, animated: true)
})
.disposed(by: disposeBag)
- 결과물
//
// ViewController.swift
// jh_counter
//
// Created by 추경민 on 2021/10/27.
//
import UIKit
import ReactorKit
import RxCocoa
import RxSwift
// Conform to the protocol 'View' then the property 'self.reactor' will be available
class ViewController: UIViewController, StoryboardView{
@IBOutlet var decreaseButton: UIButton!
@IBOutlet var increaseButton: UIButton!
@IBOutlet var valueLabel: UILabel!
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
var disposeBag = DisposeBag()
// Called when the new value is assigned to 'self.reactor'
func bind(reactor: ViewReactor) {
// Action
increaseButton.rx.tap // Tap event
.map { Reactor.Action.increase } // Convert to Action.increase
.bind(to: reactor.action)
.disposed(by: disposeBag)
decreaseButton.rx.tap // Tap event
.map { Reactor.Action.decrease } // Convert to Action.decrease
.bind(to: reactor.action)
.disposed(by: disposeBag)
// State
reactor.state.map { $0.value } // 10
.distinctUntilChanged()
.map { "\($0)" } // "10"
.bind(to: valueLabel.rx.text) // Bind to valueLabel
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading }
.distinctUntilChanged()
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
reactor.pulse(\.$alertMessage)
.compactMap { $0 }
.subscribe(onNext: { [weak self] message in
let alertController = UIAlertController(
title: nil,
message: message,
preferredStyle: .alert
)
alertController.addAction(UIAlertAction(
title: "OK",
style: .default,
handler: nil
))
self?.present(alertController, animated: true)
})
.disposed(by: disposeBag)
}
}
5. Main.storyboard
위 사진처럼 item 추가 후, @IBOutlet 변수들과 연결
6. 최종 결과물