RxSwift

5. Filtering Operators

kangwook 2022. 12. 19. 15:40
  • Operator는 Rx의 빌딩 블록으로, Observable 에서 발생하는 이벤트를 변환, 처리 및 반응하는데 사용할 수 있다.
  • +, -, / 와 같은 단순한 산술 연산자를 결합하여 복잡한 수식을 만드는 것처럼 Rx의 간단한 연산자를 연결하고 함께 구성하여 복잡한 앱의 논리를 표현할 수 있다.

 


Ignoring Operators

  • ignoreElements는 .next 이벤트 아이템을 무시
  • 그러나 .completed 또는 .error 이벤트와 같은 중지 이벤트를 허용

example(of: "ignoreElements") {
    let strikes = PublishSubject<String>()        // Create strikes subject
    let disposeBag = DisposeBag()
 
    strikes                                     // Subscribe to all strikes events, but ignore all.
        .ignoreElements()
        .subscribe { _ in
            print("You're out!")
        }
        .disposed(by: disposeBag)
 
    strikes.onNext("X")
    strikes.onNext("X")
    strikes.onNext("X")
 
    strikes.onCompleted()
}

ignoreElements는 onNext 이벤트를 받지 않는다.

  • ignoreElement가 실제로 Completable을 반환
  • 이 경우 완료 또는 오류 이벤트만 발생할 수 있다.

 

  • Observable에 의해 방출된 n번째 아이템만 처리하려는 경우가 있을 수 있다.
  • 이를 위해 수신하려는 아이템의 인덱스를 사용하는 elementAt을 사용할 수 있다. → 현재는 elementAt은 deprecated. element(at: )으로 변경
  • 나머지는 모두 무시

example(of: "elementAt") {
    let strikes = PublishSubject<String>()
    let disposeBag = DisposeBag()
 
    strikes
        .element(at: 2)
        .subscribe(onNext: { _ in
            print("You're out!")
        })
        .disposed(by: disposeBag)
 
    strikes.onNext("X")
    strikes.onNext("X")
    strikes.onNext("X")
}

실제로 2번째 인덱스에서 걸러주어 한번만 출력될걸로 예상했는데 두번이 출력되고 있다.(이유를 모르겠음)

  • ignoreElements 및 element(at: )은 Observable에서 내보낸 필터링 아이템

 

  • filter는 true로 해석되는 아이템만 통과하도록 허용

실제로 3보다 작은 값인 1과 2만 방출

example(of: "filter") {
    let disposeBag = DisposeBag()
 
    Observable.of(1, 2, 3, 4, 5, 6)
        .filter { $0 % 2 == 0 }
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)
}

1, 2, 3, 4, 5, 6 에서 2로 나누었을 때 나머지가 0인 2, 4, 6만 필터링하여 방출


Skipping Operators

  • 특정 수의 아이템을 건너뛰어야 할 수도 있다.
  • skip 연산자를 사용하면 첫 번째부터 매개 변수로 전달한 숫자까지 무시할 수 있다.

2까지 skip하고 그 이후의 아이템인 3부터 방출

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

실제로 3번째 이후인 D부터 출력이 된다.

  • skipWhile을 사용하면 특정 조건을 만족하는 구간동안 건너뛸 수 있다.
  • 그리고 만족하지 않는 그 이후부터는 모든 아이템을 방출한다.
  • skipWhile을 사용하면 true를 반환하면 아이템을 건너뛰고, false를 반환하면 아이템을 방출할 수 있다. → filter와 반대

1은 2로 나누었을 때 나머지가 1이므로 skip, 2는 나머지가 0이므로 그 이후 아이템들은 모두 방출된다.

example(of: "skipWhile") {
    let disposeBag = DisposeBag()
 
    Observable.of(2, 2, 3, 4, 4)
        .skip(while: { $0 % 2 == 0 })           // skipWhile은 deprecated. skipWhile이 skip(while: ) 형태로 변경
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
}

앞의 2개의 원소 2, 2는 2로 나누었을 때 나머지가 0이므로 패스, 3부터는 방출이 시작되므로 그 이후의 모든 값은 방출

  • 지금까지의 필터링은 일부 정적 조건을 기반
  • 동적으로 필터링을 진행할 수도 있다.

 

  • skipUntil은 다른 트리거 Observable이 방출될 때까지 아이템을 계속 건너뛴다.
  • skipUntil은 트리거 Observable이 .next 이벤트를 방출할 때까지 방출된 아이템을 무시. 그런 다음 트리거 Observable이 방출된 시점부터 모든 것을 방출하기 시작

example(of: "skipUntil") {
    let disposeBag = DisposeBag()
 
    let subject = PublishSubject<String>()
    let trigger = PublishSubject<String>()
 
    subject
        .skip(until: trigger)               // skipUntil은 deprecated. skipUntil이 skip(until: ) 형태로 변경
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
 
    subject.onNext("A")
    subject.onNext("B")
 
    trigger.onNext("X")
 
    subject.onNext("C")
}

"A"와 "B"가 .next 이벤트로 방출되었지만 아직 트리거 Observable이 방출되지 않았기 때문에 방출되지 않고 트리거 Observable이 방출된 이후의 .next 이벤트인 "C"가 방출


Taking Operators

  • Taking은 Skip과 반대로 아이템을 가져올 수 있다.
  • take 연산자를 사용하여 가져오는 방법이 있다.

Observable의 이벤트로 1, 2, 3 이 있는데 이 중 2개를 가져오려면 take(2) 조건을 중간에 걸 수 있다.

example(of: "take") {
    let disposeBag = DisposeBag()
 
    Observable.of(1, 2, 3, 4, 5, 6)
        .take(3)
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
}

다음과 같이 처음 방출된 이벤트부터 3개를 가져올 수 있다.

  • takeWhile은 skipWhile과 유사하게 동작
  • 방출되는 아이템의 인덱스를 참조할 수도 있다.
  • 이를 위해 Observable에서 방출된 각 아이템의 인덱스와 아이템을 포함하는 튜플을 생성하는 열거 연산자를 사용할 수 있다.

example(of: "takeWhile") {
    let disposeBag = DisposeBag()
 
    Observable.of(2, 2, 4, 4, 6, 6)
        .enumerated()                               // enumerated() 를 통해 방출된 각 아이템의 인덱스와 값을 포함하는 튜플을 가져온다.
        .take(while: { index, integer in                // takeWhile은 deprecated. takeWhile은 take(while: ) 형태로 변경
            integer % 2 == 0 && index < 3            // 방출할 아이템의 조건
        })
        .map { $0.element }                         // map을 사용하여 튜플에 접근하여 아이템을 가져온다.
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
}

  • skipUntil과 마찬가지로 takeUntil 연산자도 있으며 트리거 Observable이 아이템을 방출할 때까지 아이템을 가져온다.

example(of: "takeUntil") {
    let disposeBag = DisposeBag()
 
    let subject = PublishSubject<String>()
    let trigger = PublishSubject<String>()
 
    subject
        .take(until: trigger)
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
 
    subject.onNext("1")
    subject.onNext("2")
 
    trigger.onNext("X")
 
    subject.onNext("3")
}

trigger가 아이템을 방출할 때까지 subject는 아이템을 방출

// 여기서 takeUntil을 중지시키는 트리거는 ViewController 또는 ViewModel인 self의 할당해제
_ = someObservable
        .take(until: self.rx.deallocated)
        .subscribe {
            print($0)
        })

Distinct Operators

  • distinctUntilChanged 를 사용하면 바로 옆에 있는 중복만 방지

example(of: "distinctUntilChanged") {
    let disposeBag = DisposeBag()
 
    Observable.of("A", "A", "B", "B", "A")
        .distinctUntilChanged()
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
}

바로 옆의 중복만 방지하므로 두번째 A가 방출

 

  • 앞의 예에선 String 타입이 Equtable 프로토콜을 준수하는 타입이기 때문에 Equatable을 기반으로 동등성을 비교
  • 그러나 외부에서 이름이 지정되지 않은 매개 변수를 비교하기 위해선 distinctUntinChanged(_ : )를 사용하여 비교할 수 있다.

example(of: "distinctUntilChanged(_ :)") {
    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
 
            for aWord in aWords where bWords.contains(aWord) {          // 첫번째 배열의 모든 단어를 반복하고 두번째 배열에 포함되어 있는지 확인
                containsMatch = true
                break
            }
             
            return containsMatch
        }
        .subscribe {
            print($0)
        }
        .disposed(by: disposeBag)
}

결과적으로 원소의 배열이 다른 배열에 포함되어 있는 3개가 방출된다.