[RxSwift 정리 - 2]에 이어서, ViewModel에서 자주 사용했던 Signal, Driver에 대해서 살펴볼 예정이다.

썸네일
Signal과 Driver는 UI 작업에 최적화된 Hot Observable이다.
Relay나 일반 Observable과 달리 Main Thread 에서만 동작하도록 제한된 기능을 제공하는, 보다 제한된 Observable이라고 할 수 있다.
UIKit은 thread-safe하지 않기 때문에
UI 관련 작업은 반드시 main thread에서 실행해야 한다.
thread-safe: 여러 스레드가 동시에 접근해도 문제가 발생하지 않는 상태.
UIKit의 대부분의 객체는 thread-safe하지 않아서, 백그라운드 스레드에서 접근하면 크래시 발생 가능.
Relay나 Observable을 UI작업에 사용 시 꼭 observe(on: MainScheduler.instance)를 붙여줘야 했는데, Driver / Signal 에서는 그러지 않아도 된다는 뜻이다.
- 이벤트 중심(Event Stream)
- 최신값을 저장하지 않음
- 한 번만 처리하면 되는 이벤트에 사용 ex) button tap, alert 표시, 네비게이션 이동 트리거 등
-> PublishRelay와 비슷하게 사용한다.
- 상태 중심(State Stream)
- 최신값을 유지함
- 상태가 업데이트되는 데이터에 사용 ex) 리스트 데이터, 로딩 상태, 선택된 인덱스, 카운트 값
-> BehaviorRelay와 비슷하게 사용한다.
ViewController - ViewModel까지의 흐름을 정리한 예시이다.
final class ArchiveViewModel {
private let ticketsRelay = BehaviorRelay<[Ticket]>(value: [])
struct Input {
let searchText: Observable<String>
let deleteTicket: Observable<String>
}
struct Output {
let filteredTickets: Driver<[Ticket]>
let deleteCompleted: Signal<String>
}
func transform(input: Input) -> Output {
let filteredTickets = input.searchText
.map { /* ... */ }
.asDriver(onErrorJustReturn: [])
let deleteCompleted = input.deleteTicket
.flatMapLatest { /* ... */ }
.asSignal(onErrorJustReturn: "")
return Output(
filteredTickets: filteredTickets,
deleteCompleted: deleteCompleted
)
}
}
searchText: ViewController에서 전달받은 검색어 스트림deleteTicket: ViewController에서 전달받은 삭제 이벤트 스트림filteredTickets: 텍스트 변경에 따라 필터링된 티켓 리스트 상태.asDriver())deleteCompleted: 삭제 완료 이벤트.asSignal())final class ArchiveViewController: UIViewController {
private let searchTextRelay = BehaviorRelay<String>(value: "")
private let deleteTicketRelay = PublishRelay<String>()
private func bind() {
searchBar.rx.text.orEmpty // Observable
.bind(to: searchTextRelay) // Observer
.disposed(by: disposeBag)
tableView.rx.itemDeleted // Observable
.map { self.tickets[$0.row].id } // Observer
.bind(to: deleteTicketRelay)
.disposed(by: disposeBag)
let input = ArchiveViewModel.Input(
searchText: searchTextRelay.asObservable(),
deleteTicket: deleteTicketRelay.asObservable()
)
let output = viewModel.transform(input: input)
output.filteredTickets
.drive(tableView.rx.items) { _, _, _ in }
.disposed(by: disposeBag)
output.deleteCompleted
.emit(onNext: { self.showToast() })
.disposed(by: disposeBag)
}
private func showToast() { }
}
searchBar.rx.text.orEmpty : 텍스트 변경 이벤트 스트림(Observable)tableView.rx.itemDeleted : 삭제 이벤트 스트림(Observable)filteredTickets.drive(…): 상태가 변경될 때마다 tableView 업데이트deleteCompleted.emit(…): 삭제 이벤트 발생 시 토스트 표시참고: Drive와 Signal 구독
Drive와 Signal은 각각
- Driver → drive
- Signal → emit
으로 구독해야 한다.