Skip to content

Filtering Operation  #19

@simoniful

Description

@simoniful

Ch.5 Filtering Operators

A. Getting Started

  • 여기서 배울 것은 filtering operator로, 이 것을 통해 .next이벤트를 통해 받아오는 값을 선택적으로 취할 수 있다.
  • 기존 Swift에서 filter(_:)를 사용해봤다면 이해하기 쉬울 것이다.

B. Ignoring operators

1. .ignoreElements()

  • ignoreElements.next 이벤트를 무시. completed.error 같은 완료 이벤트는 허용.
  • Marble diagram 참고
let strikes = PublishSubject<String>()
let disposeBag = DisposeBag()

strikes
    .ignoreElements()
    .subscribe({ _ in
        print("You're out!")
    })
    .disposed(by: disposeBag)

strikes.onNext("X")
strikes.onNext("X")
strikes.onNext("X")

strikes.onCompleted()
// "You're out!"

2. .elementAt()

  • Observable에서 방출된 n번째 요소만 처리하려는 경우 elementAt()을 사용.
  • 받고싶은 요소에 해당하는 index만을 방출하고 나머지는 무시.

let strikes = PublishSubject<String>()
    let disposeBag = DisposeBag()

    strikes
        .elementAt(1)
        .subscribe(onNext: { _ in
            print("You're out!")
        })
        .disposed(by: disposeBag)

    strikes.onNext("X")
    strikes.onNext("X")
    // "You're out!"
    strikes.onNext("X")

3. .filter

  • filter는 필터링 요구사항이 한 가지 이상일 때 사용 가능.

let disposeBag = DisposeBag()

Observable.of(1,2,3,4,5,6)
    .filter({ (int) -> Bool in
        int % 2 == 0
    })
    .subscribe(onNext: {
        print($0)
        // 2
        // 4
        // 6
    })
    .disposed(by: disposeBag)

C. Skipping operators

1. .skip

  • skip 연산자는 첫 번째 요소부터 n개의 요소를 skip하게 해준다.

let disposeBag = DisposeBag()

Observable.of("A", "B", "C", "D", "E", "F")
    .skip(3)
    .subscribe(onNext: {
        print($0)
        // D
        // E
        // F
    })
    .disposed(by: disposeBag)

2. .skipWhile

  • .skipWhile은 어떤 요소를 skip하지 않을 때까지 skip하고 종료하는 연산자.

  • skipwWhile은 skip할 로직을 구성하고 해당 로직이 false 되었을 때 방출한다. filter와 반대.

let disposeBag = DisposeBag()

Observable.of(2, 2, 3, 4, 4)
    .skipWhile({ (int) -> Bool in
        // 홀수인 요소가 나올 때까지 skip
        int % 2 == 0
    })
    .subscribe(onNext: {
        print($0)
        // 3
        // 4
        // 4
    })
    .disposed(by: disposeBag)

3. .skipUntil

  • 다른 observable에 기반한 요소들을 다이나믹하게 필터링

  • skipUntil은 다른 트리거 observable이 시동하여 .next이벤트를 방출하기 전까지는 기존 observable에서 방출하는 이벤트들을 skip.

let disposeBag = DisposeBag()

let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()

subject
    .skipUntil(trigger)
    .subscribe(onNext: {
        print($0)
    })
    .disposed(by: disposeBag)

subject.onNext("A")
subject.onNext("B")

trigger.onNext("X")

subject.onNext("C")
// C

D. Taking operators

1. take

  • RxSwift에서 어떤 요소를 원하는 인덱스의 값 만큼 취하고 싶을 때 사용할 수 있는 연산자는 take. skip의 반대 개념

let disposeBag = DisposeBag()

Observable.of(1,2,3,4,5,6)
    .take(3)
    .subscribe(onNext: {
        print($0)
        // 1
        // 2
        // 3
    })
    .disposed(by: disposeBag)

2. takeWhile

  • takeWhileskipWhile처럼 작동한다.

3. enumerated

  • 방출된 요소의 index를 참고하고 싶은 경우. 이럴 때는 enumerated 연산자를 통해 확인 가능.
  • 기존 Swift의 enumerated 메소드와 유사하게, Observable에서 나오는 각 요소의 index와 값을 포함하는 튜플을 생성하여 반환.
let disposeBag = DisposeBag()

Observable.of(2,2,3,4,6,6)
    .enumerated()
    .takeWhile({ index, element in
        element % 2 == 0 && index < 3
    })
    .map { $0.element }
    .subscribe(onNext: {
        print($0)
    })
    .disposed(by: disposeBag)

4. takeUntil

  • takeUntilskipUntil처럼 작동한다.

  • takeUntil은 다른 트리거 observable이 시동하여 .next이벤트를 방출하기 전까지는 기존 observable에서 방출하는 이벤트들을 취한다.

let disposeBag = DisposeBag()

let subject = PublishSubject<String>()
let trigger = PublishSubject<String>()

subject
    .takeUntil(trigger)
    .subscribe(onNext: {
        print($0)
    })
    .disposed(by: disposeBag)

subject.onNext("1")
// 1
subject.onNext("2")
// 2
trigger.onNext("X")

subject.onNext("3")

E. Distinct operators

1. .distinctUntilChanged

  • 그림에서처럼 distinctUntilChanged는 연달아 같은 값이 이어질 때 중복된 값을 막아주는 역할을 한다.
  • 2는 연달아 두 번 반복되었으므로 뒤에 나온 2가 배출되지 않음.
  • 1은 중복이긴 하지만 연달아 반복된 것이 아니므로 그대로 배출.
let disposeBag = DisposeBag()

Observable.of("A", "A", "B", "B", "A")
    .distinctUntilChanged()
    .subscribe(onNext: {
        print($0)
        // A
        // B
        // A
    })
    .disposed(by: disposeBag)

2. .distinctUntilChanged(_:)

  • distinctUntilChanged는 기본적으로 구현된 로직에 따라 같음을 확인. 그러나 커스텀한 비교로직을 구현하고 싶다면 distinctUntilChanged(_:)를 사용.

  • 그림은 value라 명명된 값을 서로 비교하여 중복되는 값을 제외하고 있다.

let disposeBag = DisposeBag()

let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

Observable<NSNumber>.of(10, 110, 20, 200, 210, 310)
    .distinctUntilChanged({ a, b in
        // 스펠링으로 전환
        guard let aWords = formatter.string(from: a)?.components(separatedBy: " "),
            let bWords = formatter.string(from: b)?.components(separatedBy: " ") else {return false}
        var containsMatch = false

        // 각 스펠링을 분리(ex. ["ten"] ["one", "hundred", "ten"]) 하여 동일하게 일치하는 지 비교하여 flag 변경
        for aWord in aWords {
            for bWord in bWords {
                if aWord == bWord {
                    containsMatch = true
                    break
                }
            }
        }
        // flag true 시 중복되었다고 판단되여 방출 X, 다음 요소 비교 진행
        // flag false 시 중복이 아니라고 판단되여 비교 대상 방출
        // a, b, c를 비교해가면서 만약 b가 a와 중첩되는 부분이 있어 prevent 되면, 다음엔 b와 c를 비교하는 것이 아니라 a와 c를 비교
        return containsMatch
    })
    .subscribe(onNext: {
        print($0)
    })
    .disposed(by: disposeBag)

F. 구독 공유(Share)하기

1. Subscriber의 독립된 elements sequence

  • Observable은 매우 게으른 pull-driven 시퀀스
    • Observable에 아무리 여러 연산자를 호출해도 subscribe() 가 호출되기 전까진 아무 동작도 하지 않음
  • Observable에 Subscribe를 한 횟수만큼 Observable 시퀀스가 생성
    • subscriber는 각자 독립된 elements sequence를 가진다!
  • Operator들은 기본적으로 stateless
    • 각각의 Operator들은 이전의 동작과 독립적으로 동작한다.
var disposeBag = DisposeBag()
var start = 0

func getStartNumber() -> Int {
    start += 1
    return start
}

let numbers = Observable<Int>.create { observer in
    let start = getStartNumber()
    observer.onNext(start)
    observer.onNext(start+1)
    observer.onNext(start+2)
    observer.onCompleted()
    return Disposables.create()
}

numbers
    .subscribe(
      onNext: { el in
        print("element [\(el)]")
          // element [1]
          // element [2]
          // element [3]
      },
      onCompleted: {
        print("-------------")
      }
    )
    .disposed(by: disposeBag)

numbers
    .subscribe(
      onNext: { el in
        print("element [\(el)]")
          // 두번째 subscribe을 하면서 getStartNumber()가 다시 호출되고 start값이 1 증가한 상태로 시작
          // element [2]
          // element [3]
          // element [4]
      },
      onCompleted: {
        print("-------------")
      }
    )
    .disposed(by: disposeBag)

2. share

API를 한번 콜하고 그 콜한 결과를 여러 곳에서 구독하여 다르게 쓰고 싶을 때 주로 사용

share()

  • 1개의 구독의 이벤트를 여러 Observer가 공유
  • 새로운 subscriber가 observing하기 전 과거 elements를 어떻게 다룰지(publish)와 언제 공유할지(refCount)가 필요하다.
  • 이거를 publish().refCount(), 합쳐서 share(replay: 1)로 사용
  • share() 연산자를 사용하면 Subscribe()할때마다 새로운 Observable 시퀀스가 생성되지 않고, 하나의 시퀀스에서 방출되는 아이템을 공유해 사용 가능
  • share()는 구독이 영향을 받기 전까지는 어떠한 값 방출도 내지 않는다.

share(replay:scope:)

  • replay에 넣는 인자는 버퍼의 크기를 의미
    • 마지막 몇개의 방출 값에 대한 버퍼를 가지며 새로운 관찰자가 구독했을 때 이를 제공
  • scope에 넣는 인자는 버퍼의 생명주기에 관한 것
    • .forever : Subscription이 0이 되더라도 버퍼가 유지. 그래서 새로운 Subscription은 Subscribe() 를 하면 마지막에 버퍼에 남아있던 replay개수 만큼의 값을 수신하게 됩니다.
    • .whileConnected : 1개 이상의 Subscriber가 존재하는 동안만 버퍼가 유지 됩니다. Subscription이 0이 되면 버퍼가 비워지고 새로운 Subscription은 버퍼에 남아 있던 값이 없으므로 replay시 새 값을 요청해 수신하게 됩니다.
class ViewController: UIViewController {
    @IBOutlet weak var requestMoreButton: UIButton!
    @IBOutlet weak var remainCountLabel: UILabel!

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
    }

    private func bind() {
        // 서버에 API를 요청해 결과를 반환하는 시퀀스라 가정
        let networkRequestAPI = Observable.of(100).debug("networkRequestAPI")

        let result = requestMoreButton.rx.tap
            .flatMap { networkRequestAPI }
            // result 시퀀스를 공유하도록 처리
            // .share()
        result
            .map { $0 > 0 }
            .bind(to: requestMoreButton.rx.isHidden)
            .disposed(by: disposeBag)

        // bind(to:)는subscribe()의 별칭(Alias)으로 Subscribe()를 호출한 것과 동일
        result
            .map { "Count:\($0)" }
            .bind(to: remainCountLabel.rx.text)
            .disposed(by: disposeBag)
    }
}
// .share를 하지 않은 경우 debug 결과
// networkRequestAPI -> subscribed
// networkRequestAPI -> Event next(100)
// networkRequestAPI -> Event completed
// networkRequestAPI -> isDisposed
// networkRequestAPI -> subscribed
// networkRequestAPI -> Event next(100)
// networkRequestAPI -> Event completed
// networkRequestAPI -> isDisposed

// .share를 한 경우 debug 결과
// networkRequestAPI -> subscribed
// networkRequestAPI -> Event next(100)
// networkRequestAPI -> Event completed
// networkRequestAPI -> isDisposed

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions