Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering Operation #19

Open
simoniful opened this issue Apr 12, 2022 · 0 comments
Open

Filtering Operation #19

simoniful opened this issue Apr 12, 2022 · 0 comments
Labels

Comments

@simoniful
Copy link
Owner

simoniful commented Apr 12, 2022

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
@simoniful simoniful added the Rx label Apr 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant