swift

RxSwift: Transforming Operators

kimyounggyun 2022. 7. 31. 22:59

Observable를 변환시키는 연산자

toArray

Completed 이벤트가 방출되면 지금까지 방출된 모든 요소를 하나의 배열로 만들어 Single로 전달하는 연산자이다.

Single은 Observable의 하나의 형태로 한 개의 값 또는 에러 이벤트를 방출한다. 그렇기 때문에 구독시 success와 error의 형태로 이벤트를 구분한다.

let disposeBag = DisposeBag()
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let subject = PublishSubject<Int>()

subject
    .toArray()
    .subscribe { event in
        switch event {
        case .success(let result):
            print(result)
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
    .disposed(by: disposeBag)

subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onCompleted() // completed 되어야 하나의 배열로 이벤트 방출
/*
결과 
[1, 2, 3]
*/

map

각각의 요소를 매개 변수로 받은 transform function에 적용하여 새로운 형태의 요소로 방출하는 연산자.

compactMap

observable이 방출하는 이벤트에서 값을 꺼내 optional로 바꾸고 transform function을 수행한다. 변환 결과가 nil이라면 해당 이벤트를 전달하지 않고 필터링한다. 또한 nil이 아니라면 unwrapping 하여 방출한다!

import UIKit
import RxSwift

let disposeBag = DisposeBag()
let subject = PublishSubject<String?>()

subject
    .compactMap { $0 }
    .subscribe { print($0) }
    .disposed(by: disposeBag)

Observable<Int>.interval(.milliseconds(300),
                         scheduler: MainScheduler.instance)
    .take(10)
    .map { _ in Bool.random() ? "⭐️" : nil }
    .subscribe(onNext: { subject.onNext($0) })
    .disposed(by: disposeBag)

flatMap

각각의 요소에 transform function을 적용시켜 inner observable을 생성한 후 flattening(평탄화)를 거쳐 result observable를 만들어 방출한다. inner observable이 이벤트를 방출하면 result observable 역시 지연 없이 해당 이벤트를 방출하기 때문에 순서가 뒤바뀌는 interleaving이 발생한다.

import UIKit
import RxSwift

let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "❤️"
let greenHeart = "💚"
let blueHeart = "💙"

Observable
    .from([redCircle, greenCircle, blueCircle])
    .flatMap { circle -> Observable<String> in
        switch circle {
        case redCircle:
            return Observable
                .repeatElement(redHeart)
                .take(5)
        case greenCircle:
            return Observable
                .repeatElement(greenHeart)
                .take(5)
        case blueCircle:
            return Observable
                .repeatElement(blueHeart)
                .take(5)
        default:
            return Observable.just("")
        }
    }
    .subscribe { print($0) }
    .disposed(by: disposeBag)

flatMapFirst

flatMap과정 중 가장 먼저 방출을 시작한 inner observable이 곧 result observable이 된다. flatMap 예시에서 redCircle의 inner observable이 방출하는 이벤트만 구독자는 받게 된다.

flatMapLatest

원본 observable이 방출하는 이벤트를 inner observable로 변환하는 것은 flatMap과 동일하나 새로운 이벤트가 들어오면 기존의 inner observable의 방출이 중단되고 새로운 이벤트가 inner observable이 된다. 아래 사진에서 red circle이 blue circle 뒤에 다시 들어올 때 전에 썼던 inner observable을 재사용하지 않고 새로운 Inner observable을 사용한다.

 

import UIKit
import RxSwift

func currentTimeString() -> String {
   let f = DateFormatter()
   f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
   return f.string(from: Date())
}

let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "❤️"
let greenHeart = "💚"
let blueHeart = "💙"

let sourceObservable = PublishSubject<String>()
let trigger = PublishSubject<Void>()
sourceObservable
    .flatMapLatest { circle -> Observable<String> in
        switch circle {
        case redCircle:
            return Observable<Int>.interval(.milliseconds(200), scheduler: MainScheduler.instance)
                .map { _ in redHeart}
                .take(until: trigger)
        case greenCircle:
            return Observable<Int>.interval(.milliseconds(200), scheduler: MainScheduler.instance)
                .map { _ in greenHeart}
                .take(until: trigger)
        case blueCircle:
            return Observable<Int>.interval(.milliseconds(200), scheduler: MainScheduler.instance)
                .map { _ in blueHeart}
                .take(until: trigger)
        default:
            return Observable.just("")
        }
    }
    .subscribe { print("\(currentTimeString()) : \($0)") }
    .disposed(by: disposeBag)

sourceObservable.onNext(redCircle)

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    sourceObservable.onNext(greenCircle)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    sourceObservable.onNext(blueCircle)
}
// 첫번째 inner observable을 재사용하지 않음.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    sourceObservable.onNext(redCircle)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    trigger.onNext(())
}

concatMap

transform function의 결과로 생성된 inner observable을 순서대로 연결해 이전 inner observable의 이벤트 방출이 끝나면 그다음 inner observable이 이벤트 방출을 시작한다. 원본 observable의 방출 순서와 inner observable의 방출 순서가 동일함이 보장되며 당연히 interleaving이 발생하지 않는다.

import UIKit
import RxSwift

func currentTimeString() -> String {
    let f = DateFormatter()
    f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
    return f.string(from: Date())
}
let disposeBag = DisposeBag()

let redCircle = "🔴"
let greenCircle = "🟢"
let blueCircle = "🔵"

let redHeart = "❤️"
let greenHeart = "💚"
let blueHeart = "💙"

Observable.from([redCircle, greenCircle, blueCircle])
    .concatMap { circle -> Observable<String> in
        switch circle {
        case redCircle:
            return Observable.repeatElement(redHeart)
                .take(5)
        case greenCircle:
            return Observable.repeatElement(greenHeart)
                .take(5)
        case blueCircle:
            return Observable.repeatElement(blueHeart)
                .take(5)
        default:
            return Observable.just("")
        }
    }
    .subscribe { print($0) }
    .disposed(by: disposeBag)

scan

매개변수로 기본 값과 accumulator function을 전달한다. 기본 값과 source observable이 방출하는 값을 대상으로 accumulator function을 실행하고 결과를 observable로 방출한다. source observable이 새로운 요소를 방출하면 이전 결과와 함께 함수로 전달하며 위 과정을 반복한다. accumulator function의 결과가 매번 구독자로 전달되며, 작업을 누적시키면서 중간 결과와 최종 결과가 모두 필요할 때 사용한다.

import UIKit
import RxSwift
let disposeBag = DisposeBag()

Observable
    .range(start: 1, count: 10)
    .scan(1, accumulator: *)
    .subscribe { print($0) }
    .disposed(by: disposeBag)

buffer

특정 주기 동안 source observable이 방출하는 요소를 수집하고 하나의 배열로 리턴한다.(controlled buffering) 매개 변수로 timespan, count를 전달하는데 timespan이 지나면  count개가 수집되지 않아도 즉시 방출하며 count개가 수집되면 timespan이 지나지 않아도 즉시 방출한다.

window

buffer처럼 timespan과 count를 지정해서 source observable이 방출하는 요소를 작은 단위의 observable로 분해해 수집한다. 수집된 항목을 inner observable로 방출한다. 반환형 주의을 주의해야한다.

public func window(timeSpan: RxTimeInterval, count: Int, scheduler: SchedulerType) -> Observable<Observable<Element>>

import UIKit
import RxSwift

func currentTimeString() -> String {
   let f = DateFormatter()
   f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
   return f.string(from: Date())
}

let disposeBag = DisposeBag()

Observable<Int>
    .interval(.seconds(1), scheduler: MainScheduler.instance)
    .window(timeSpan: .seconds(5), count: 3, scheduler: MainScheduler.instance)
    .take(5)
    .subscribe { print("\(currentTimeString()): \($0)")
        
        if let obsevable = $0.element {
            obsevable.subscribe { print("inner: ", $0) }
        }
        
    }
    .disposed(by: disposeBag)

groupBy

source observable이 방출하는 요소를 조건에 따라 그룹핑하여 inner observable로 방출하는 연산자이다. 보통 innber observable를 toArray로 바꾸고 flatMap으로 observable로 만들어 방출한다.

import UIKit
import RxSwift

func currentTimeString() -> String {
    let f = DateFormatter()
    f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
    return f.string(from: Date())
}

let disposeBag = DisposeBag()
let words = ["Apple", "Banana", "Orange", "Book", "City", "Axe"]

Observable
    .from(words)
    .groupBy { $0.count }
    .subscribe { event in
        print(event)
        if let groupedOb = event.element {
            groupedOb.subscribe { print($0)}
        }
    }
    .disposed(by: disposeBag)

Observable
    .from(words)
    .groupBy { $0.count }
    .subscribe(onNext: { groupedObservable in
        print("key == \(groupedObservable.key)")
        groupedObservable.subscribe { print("   \($0)")}
    })
    .disposed(by: disposeBag)

Observable
    .from(words)
    .groupBy { $0.count }
    .flatMap { $0.toArray() }
    .subscribe { print($0) }
    .disposed(by: disposeBag)


Observable
    .from(words)
    .groupBy { $0.first ?? Character(" ") }
    .flatMap { $0.toArray() }
    .subscribe { print($0) }
    .disposed(by: disposeBag)