ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 4: FilteringOperators
    Raywenderlich/Combine: Asynchronous Programming 2020. 7. 17. 09:27

    Version

    Swift 5.3, iOS 14, Xcode 12

     

    연산자(operators)는 기본적으로 Combine의 publishers를 조작(manipulate)하는 데 사용하는 어휘(vocabulary)이다. "단어(words)"를 많이 알수록, 데이터를 보다 잘 제어할 수 있다.

    이전(previous) 장(chapter)에서는 값(values)을 사용(consume)하고 다른 값(values)으로 변환(transform)하는 방법을 배웠다. 이는 분명히, 일반적인 작업에서 가장 유용한 연산자(operator) 범주(categories) 중 하나이다.

    그러나 publisher가 내보(emitted)낸 값(values)이나 이벤트(events)를 제한(limit)하고, 일부만 사용(consume)하려는 경우가 있을 수도 있다. 이 장(chapter)에서는 필터링 연산자(filtering operators)를 사용하여 이런 작업을 수행하는 방법을 설명한다.

    다행히도 이러한 연산자(operators) 중 다수는 Swift 표준 라이브러리(standard library)에 동일한 이름으로 존재한다.

     

    Getting started

    이 장(chapter)의 프로젝트(project) 폴더(folder)에서 Starter.playground를 찾을 수 있다. 이 장(chapter)을 진행하면서 playground에 코드를 작성하고, 실행(run)한다. 그렇게 하면서 여러 연산자(operators)가 어떻게 publisher가 내보(emitted)낸 이벤트(events)를 조작(manipulate)하는지 이해하게 될 것이다.

    Note: 이 장(chapter) 대부분의 연산자(operators)에 try 접두사(prefix)를 추가한 유사(parallels) 연산자가 있다(예 : filter vs. tryFilter). 이들 사이의 유일한 차이점은 후자(tryFilter)가 오류를 처리하는(throw) 클로저(closure)를 제공한다는 것이다. 클로저(closure) 내에서 발생(thrown)하는 모든 오류(error)는 해당 오류(error)와 함께 publisher를 종료(terminate)한다. 오류 처리가 없는(non-throwing) 연산자와 사실상 동일하므로 이 장(chapter)에서는 따로 다루지 않는다.

     

    Filtering basics

    첫 번째 섹션(section)에서는 publisher의 값(values)을 사용(consuming)하고, 그 중 어떤 것을 전달할 지 조건부(conditionally)로 결정하는 필터링(filtering)의 기본에 대해 다룬다.

    가장 쉬운 방법은 이름 그대로(aptly-named) filter 연산자(operator)를 사용하는 것이다. filter는 Bool을 반환(returning)하는 클로저(closure)를 사용해, 제공된(provided) 조건(predicate)과 일치하는 값(values)만 전달한다:

     

    이 예제를 playground에 추가한다:

    example(of: "filter") {
        let numbers = (1...10).publisher //1 ~ 10까지의 정수를 내보내고 완료하는 publisher
        //Sequence 타입에서 publisher 속성을 사용해 간단하게 publisher를 생성할 수 있다.
    
        numbers
            .filter { $0.isMultiple(of: 3) } //3의 배수만 필터링한다.
            .sink(receiveValue: { n in //구독
                print("\(n) is a multiple of 3!")
            })
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면, 콘솔(console)의 출력은 다음과 같다:

    ——— Example of: filter ———
    3 is a multiple of 3!
    6 is a multiple of 3!
    9 is a multiple of 3!

    앱 수명(lifetime)동안 무시(ignore)하고 싶은 동일한(identical) 값(values)을 연달아(in a row) 내보내(emit)는 publishers가 있을 수 있다. 예를 들어, 사용자가 "a"를 연속(in a row)해서 5번 입력(types)한 다음 "b"를 입력(types)한다면, 과도하게(excessive) 입력된 "a"를 무시(disregard)해야 할 것이다.

    Combine은 이와 같은 작업을 위해 removeDuplicates라는 완벽한 연산자(operator)를 제공한다:

     

    이 연산자(operator)는 인수(arguments)가 필요 없다.

    removeDuplicateString을 포함하여, Equatable을 준수(conforming)하는 모든 값(values)에 대해 자동으로 작동한다.

    removeDuplicates() 예제를 playground에 추가하고 words 변수(variable)의 공백(space) 앞에 ?를 포함한다:

    example(of: "removeDuplicates") {
        let words = "hey hey there! want to listen to mister mister ?"
            .components(separatedBy: " ") //words를 공백 단위로 나눈다.
            .publisher //publisher 생성. 단어들을 내보낸다.
    
        words
            .removeDuplicates() //단위별로 연속되는 중복 제거
            //hey hey there hey 에 적용한다면 hey there hey가 된다.
            .sink(receiveValue: { print($0) }) //구독
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하고, 디버그(debug) 콘솔(console)을 살펴본다:

    ——— Example of: removeDuplicates ———
    hey
    there!
    want
    to
    listen
    to
    mister
    ?

    보다시피, 두 번째 "hey"와 두 번째 "mister"를 생략(skipped)한다.

    Note: Equatable을 준수(conform)하지 않는 값(values)을 사용해야 할 때는, 두 개의 값으로 클로저(closure)를 취하는 removeDuplicates의 또 다른 버전(overload)을 사용할 수 있다. 여기에서 Bool을 반환(return)하여 값이 동일한지 확인한다.

     

    Compacting and ignoring

    Optional 값(values)을 내보(emitting)내는 publisher를 다뤄야 경우가 종종 있다. 또는 더 일반적으로, nil을 반환(return)할 수 있는 값(values)을 처리해야 한다.

    Swift 표준 라이브러리(standard library) SequenceCompactMap과 같은 이름의 연산자(operator)가 Combine에도 있다.

     

    map, flatMap, compactMap 비교
    https://jinshine.github.io/2018/12/14/Swift/22.%EA%B3%A0%EC%B0%A8%ED%95%A8%EC%88%98(2)%20-%20map,%20flatMap,%20compactMap/

    다음을 playground에 추가한다:

    example(of: "compactMap") {
        let strings = ["a", "1.24", "3", "def", "45", "0.23"].publisher
        //문자열 배열을 내보내는 publisher를 생성한다.
    
        strings
            .compactMap { Float($0) } //각 String을 Floatd으로 변환한다.
            //문자열로 변환할 수 없다면 nil을 반환한다.
            .sink(receiveValue: { print($0) }) //성공적으로 Float으로 변환된 문자열만 출력한다.
            .store(in: &subscriptions) //저장
    }

    예제를 playground에서 실행(run)하면, 위의 도표(diagram)와 유사한 출력을 확인할 수 있다:

    ——— Example of: compactMap ———
    1.24
    3.0
    45.0
    0.23

    때로는 publisher가 내보(emitting)내는 실제 값(values)을 무시하고, 방출(emitting)이 완료(finished)되었다는 사실만 알고 싶을 때가 있다. 이러한 상황에는 ignoreOutput 연산자(operator)를 사용할 수 있다.

     

    위의 도표(diagram)에서 볼 수 있듯이 어떤 값이 방출(emitted)되는지 또는 얼마나 많은 값들이 방출(emitted)되는지는 중요하지 않다. 완료(completion) 이벤트(event)를 전달하기만 하면 된다.

    playground에 다음 코드를 추가하여, 예제를 확인해 본다:

    example(of: "ignoreOutput") {
        let numbers = (1...10_000).publisher
        //1 ~ 10,000까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .ignoreOutput() //모든 값을 생략하고 완료 이벤트만 방출한다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    }

    어떤 값(values)도 출력되지 않는다. playground를 실행하고 디버그(debug) 콘솔(console)을 확인해 본다:

    ——— Example of: ignoreOutput ———
    Completed with: finished

     

    Finding values

    이 섹션(section)에서는 Swift 표준 라이브러리(standard library)에도 있는 두 연산자, first(where:)last(where:)에 대해 설명한다. 이름에서 알 수 있듯이, 각 제공된 조건(predicate)과 일치하는 첫 번째(first) 또는 마지막(last) 값(value)만 찾아서 내보(emit)낸다.

    first(where:)부터 시작하여 몇 가지 예제를 확인한다.

     

    이 연산자는 lazy이기 때문에 흥미롭다. 즉, 제공한 조건(predicate)와 일치(match)하는 항목을 찾을(finds) 때까지 필요한 만큼의 값만 가져온다. 일치하는 항목을 찾으면, 구독(subscription)을 취소(cancels)하고 완료(completes)한다.

    다음 코드를 playground에 추가하여 어떻게 작동하는지 확인한다:

    example(of: "first(where:)") {
        let numbers = (1...9).publisher
        //1 ~ 9까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .first(where: { $0 % 2 == 0 }) //처음으로 방출된 짝수를 찾는다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    }

    playground에서 예제를 실행(run)하고 콘솔(console)의 출력(output)을 확인한다:

    ——— Example of: first(where:) ———
    2
    Completed with: finished

    생각했던 것처럼 작동합니다. 그러나 업 스트림(upstream) 구독(subscription)이 numbers publisher를 의미하는지 생각해볼 필요가 있다. 짝수(even)를 찾아내고도 계속해서 값(values)을 내보(emitting)내는지 확인해보기 위해 아래 행(line)을 찾는다:

    numbers

    그런 다음 해당 행(line) 바로 뒤에 print("numbers") 연산자(operator)를 추가한다. 다음과 같이 된다:

     numbers 
         .print("numbers")

    Note: 운영자(operator) 체인(chain)의 어디에서든 print 연산자(operator)를 사용하여, 해당 시점에서 어떤 이벤트(events)가 발생하는지 정확하게 확인할 수 있다.

    playground을 다시 실행(run)하고 살펴본다. 출력(output)은 다음과 같아야 한다:

    ——— Example of: first(where:) ———
    numbers: receive subscription: (1...9)
    numbers: request unlimited
    numbers: receive value: (1)
    numbers: receive value: (2)
    numbers: receive cancel
    2
    Completed with: finished

    보다시피, first(where:)가 일치하는 값(matching value)을 찾으면, 구독(subscription)에 취소(cancellation)를 전송(sends)하여 업 스트림(upstream)에서의 값 방출(emitting)을 중지한다. 매우 편리하게 사용할 수 있다.

    이 연산자(operator)와는 반대로, last(where:)는 제공된 조건(predicate)과 일치(matching)하는 마지막 값(value)을 찾는 것이 목적(purpose)이다.

     

    first(where:)와 달리, 이 연산자(operator)는 일치하는 값이 있는지 여부를 알기 위해 모든 값(all values)이 방출(emit)될 때까지 기다려야한다(greedy). 따라서, 업 스트림(upstream)은 특정 시점에 완료(completes)되는 publisher이여야 한다.

    playground에 다음 코드를 추가한다:

    example(of: "last(where:)") {
        let numbers = (1...9).publisher
        //1 ~ 9까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .last(where: { $0 % 2 == 0 }) //마지막으로 방출된 짝수를 찾는다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    }

    playground을 실행(run)하면, 다음과 같은 결과를 확인할 수 있다:

    ——— Example of: last(where:) ———
    8
    Completed with: finished

    앞서, 이 연산자(operator)가 작동하려면 publisher가 완료(complete)되야한다고 언급했다.

    연산자(operator)에서 publisher가 조건(criteria)과 일치하는 값을 내보낼(emit) 것인지 알 수 있는 방법이 없기 때문에, 연산자(operator)는 publisher의 전체 범위를 알아야 조건(predicate)과 일치하는 마지막 항목(last item)을 결정할 수 있다.

    이를 확인해보기 위해, 예제를 다음과 같이 변경한다:

    example(of: "last(where:)") {
        let numbers = PassthroughSubject<Int, Never>() //변경
    
        numbers
            .last(where: { $0 % 2 == 0 })
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    
        numbers.send(1)
        numbers.send(2)
        numbers.send(3)
        numbers.send(4)
        numbers.send(5)
    }

    이 예제에서는 PassthroughSubject를 사용하고, 수동으로(manually) 이벤트(events)를 내보(send)낸다.

    다시 playground를 실행(run)하면, 아무 것도 출력되지 않는다:

    ——— Example of: last(where:) ———

    publisher는 완료(completes)되지 않기 때문에 조건(criteria)과 일치하는 마지막 값(last value)을 결정할 방법이 없다.

    이 문제를 해결(fix)하려면, 예제의 마지막 행(line)에 다음을 추가하여 subject의 완료(completion)를 내보내(send)야 한다:

    numbers.send(completion: .finished)

    playground를 다시 실행(run)하면, 모든 것이 예상대로 작동한다:

    ——— Example of: last(where:) ———
    4
    Completed with: finished

     

    Dropping values

    값 제거(dropping values)는 publishers를 사용할 때 종종 사용하는 유용한 기능이다. 예를 들어 한 publisher의 값(values)을 두 번째 publisher가 게시(publishing)하기 전까지 무시하거나, 스트림(stream) 시작 시 특정 값(values)을 무시하려는 경우 사용할 수 있다.

    이 범주(category)에는 세 개의 연산자(operators)가 있으며, 가장 간단한 dropFirst 부터 배운다.

    dropFirst 연산자(operator)는 count 매개 변수(parameter, 기본값은 1)를 사용하며, publisher가 내보내(emitted)는 첫 번째 count 값(values)을 무시(ignores)한다. count 값(values)이 방출(emitted)된 후에, 내보내진(emitted) 값들만 통과할 수 있다.

     

    playground에 코드를 추가하여, 이 연산자(operator)를 사용해 본다:

    example(of: "dropFirst") {
        let numbers = (1...10).publisher
        //1 ~ 10까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .dropFirst(8) //첫 8개의 값을 생략한다.
            .sink(receiveValue: { print($0) }) //9, 10만 출력
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면, 다음과 같은 결과가 출력된다:

    ——— Example of: dropFirst ———
    9
    10

    유용한 연산자(operators)들은 종종 이와같이 매우 간단하게 사용한다.

    값 제거(dropping values) 계열(family)의 다음 연산자(operator)는 drop(while:) 이다. 이는 조건(predicate) 클로저(closure)를 사용해, 조건(predicate)이 처음으로 만족할 때까지 publisher가 내보낸(emitted) 값(values)을 무시하는 매우 유용한 연산자이다. 조건(predicate)이 만족되는 즉시, 값(values)이 연산자(operator)를 통과(flow through)하기 시작한다:

     

    다음 예제를 playground에 추가하여 확인한다:

    example(of: "drop(while:)") {
        let numbers = (1...10).publisher
        //1 ~ 10까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .drop(while: { $0 % 5 != 0 })
            .sink(receiveValue: { print($0) }) //5로 나누어 떨어지는 첫 번째 값을 기다린다.
            //조건이 충족되는 즉시 값들은 통과하기 시작하며, 더 이상 값이 제거되지 않는다.
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하고, 디버그(debug) 콘솔(console)을 확인한다:

    ——— Example of: drop(while:) ———
    5
    6
    7
    8
    9
    10

    보다시피 처음 네개의 값(values)을 삭제(dropped)했다. 5는 조건을 만족(true)하므로, 5와 이후의(future) 모든 값(values)을 내보(emits)낸다.

    이 연산자(operator)는 filter와 비슷해 보일 수도 있다. 두 연산자 모두 클로저(closure)의 결과에 따라 어떤 값이 방출(emitted)될 지 제어한다.

    첫 번째 차이점(difference)은 클로저(closure)에서 true를 반환(return)하면, filter에서는 값(values)이 통과하지만 drop(while:)에서는 해당 값을 건너뛴다(skips).

    두 번째로 더 중요한 차이점은, filter는 업 스트림(upstream) publisher가 게시하는(published) 모든 값(all values)에 대해 조건(condition)을 확인한다는 것이다. filter에서는 조건이 true가 되더라도, 이후의 값들을 계속해서 확인한다.

    이와 달리 drop(while:)은 조건(condition)이 충족되면, 이후부터는 다시 조건을 확인하지 않는다. 이를 확인하려면 다음 행(line)을

    .drop(while: { $0 % 5 != 0 })

    아래와 같이 변경한다:

    .drop(while: { 
        print("x")
        return $0 % 5 != 0
    })

    클로저(closure)가 호출(invoked)될 때마다, 디버그(debug) 콘솔(console)에 x를 출력하는 print 구문(statement)을 추가했다. playground를 실행(run)하면 다음과 같은 결과를 확인할 수 있다:

    ——— Example of: drop(while:) ———
    x
    x
    x
    x
    x
    5
    6
    7
    8
    9
    10

    보다시피, x는 정확히 5번 출력된다. 조건(condition)이 충족되자마자(5가 방출될 때), 클로저(closure)는 다시 확인하지(evaluated) 않는다.

    지금까지 2가지 제거 연산자(dropping operators)을 알아봤다. 아직 하나가 남아있다.

    필터링(filtering) 범주(category)의 최종적(final)이고 가장 정교한(elaborate) 연산자(operator)인 drop(untilOutputFrom:)이다.

    사용자가 버튼(button)을 눌러도, isReady publisher가 결과(result)를 내보낼(emits) 때까지 모든 탭(tapping)을 무시(ignore)하는 경우를 생각해 본다. 이 연산자(operator)는 이러한 조건에 적합(perfect)하다.

    두 번째 publisher가 값(values)을 내보내기(emitting) 시작할 때까지, publisher가 내보낸(emitted) 값(values)을 건너 뛰어(skips) 다음과 같은 관계(relationship)를 만든다:

     

    맨 위 줄(top line)은 isReady 스트림(stream)을 나타내고, 두 번째 줄(line)은 drop(untilOutputFrom:)을 통과하는 사용자의 탭(taps)을 나타내며 isReady를 인수(argument)로 사용한다.

    playground에 이 도표(diagram)를 재현하는 다음 코드를 추가한다:

    example(of: "drop(untilOutputFrom:)") {
        let isReady = PassthroughSubject<Void, Never>() //isReady 상태
        let taps = PassthroughSubject<Int, Never>() //사용자에 의한 탭
        //수동으로 값을 내보낼 수 있는 두 개의 PassthroughSubject를 생성한다.
    
        taps
            .drop(untilOutputFrom: isReady)
            //isReady에서 값을 하나 이상 내보낼 때까지 탭을 무시한다.
            .sink(receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    
        (1...5).forEach { n in //5개의 탭을 내보낸다.
            taps.send(n)
    
            if n == 3 { //3번째 탭일때, isReady에서 값을 내보낸다.
                isReady.send()
            }
        }
    }

    playground를 실행(run)한 다음, 디버그(debug) 콘솔(console)을 살펴보면 다음과 같다:

    ——— Example of: drop(untilOutputFrom:) ———
    4
    5

    이 출력(output)은 위의 도표(diagram)와 동일하다:

    • 사용자의 탭(taps)이 5번 있다. 처음 세 번은 무시(ignored)한다.
    • 3번째 탭(tap) 이후, isReady는 값(value)을 내보(emits)낸다.
    • 이후, 사용자의 모든 탭(taps)은 통과(passed through)된다.

    원치 않는 값(values)을 제거하는 데 꽤 익숙해졌을 것이다. 이제 마지막 필터링(filtering) 연산자(operators) 범주(group)인 값의 제한(Limiting values)에 대해 알아본다.

     

    Limiting values

    이전 섹션(section)에서는 특정 조건(condition)이 충족될 때까지 값(values)을 삭제(drop)하거나 건너뛰는(skip) 방법을 배웠다. 그 조건(condition)은 어떤 정적 값(static value), 조건 클로저(predicate closure), 다른 publisher에 대한 종속성(dependency) 등으로 일치 여부를 판별할 수 있다.

    이 섹션(section)에서는 반대로, 조건(condition)이 충족될 때까지 값(values)을 수신(receiving)한 후 publisher가 완료(complete)되도록 하는 방법을 배운다. 예를 들어, 알 수 없는 양(amount)의 값(values)을 내보내(emit)지만, 단일 방출(single emission)만 하며 나머지(rest)의 값들은 신경 쓰지 않는 요청(request)을 고려(consider)해 볼 수 있다.

    Combine은 이 일련의 문제를 prefix 계열(family) 연산자(operators)를 사용하여 해결(solves)한다. 이름이 완전히 직관적(intuitive)이지는 않지만, 이러한 연산자(operators)가 제공하는 기능은 다양한 실제 상황에서 유용하다.

    연산자(operators)의 prefix 계열(family)은 drop 계열(family)과 유사하며, prefix(_:), prefix(while:), prefix(untilOutputFrom:)가 있다. 그러나 해당 조건(condition)이 충족될 때까지 값(values)을 삭제(dropping)하는 대신, prefix 연산자(operators)는 그 조건(condition)이 충족될 때까지 값(values)을 가져(take)온다.

    prefix(_:)를 시작으로, 이 장(chapter)의 마지막 연산자(operators) 집합(set)을 살펴본다.

    dropFirst와는 반대로 prefix(_:)는 제공된 양(amount)까지만 값(values)을 가져 와서 완료(complete)한다:

     

    playground에 다음 코드를 추가한다:

    example(of: "prefix") {
        let numbers = (1...10).publisher
        //1 ~ 10까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .prefix(2) //처음 두 값만 방출.
            //2개의 값을 내보내면 publisher는 완료된다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    }

    playground를 실행(run)하면, 다음과 같은 출력(output)을 확인할 수 있다.

    ——— Example of: prefix ———
    1
    2
    Completed with: finished

    first(where:)와 마찬가지로, 이 연산자(operator)는 lazy이다. 필요한 만큼만 값(values)을 가져오고 종료(terminates)한다. 또한 완료(completes)되기 때문에, numbers12를 넘어서는 추가 값(additional values)을 생성(producing)하는 것을 막는다(prevents).

    다음은 prefix(while:)로, 조건(predicate) 클로저(closure)를 가져와 해당 결과가 true인 동안(as long as) 업 스트림(upstream) publisher의 값(values)을 허용한다. 결과가 false이되면, publisher는 완료(complete)될 것이다:

     

    playground에 다음 예제를 추가한다:

    example(of: "prefix(while:)") {
        let numbers = (1...10).publisher
        //1 ~ 10까지 값을 내보내는 publisher를 생성한다.
    
        numbers
            .prefix(while: { $0 < 3}) //3보다 작을 때 통과한다.
            //3보다 크거나 같은 값을 내보내면, publisher는 완료된다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    }

    이 예제는 접두사(prefixing) 조건(condition)을 평가하기(evaluate) 위해, 클로저(closure)를 사용한다는 점 외에는 이전 예제와 거의 동일하다.

    playground를 실행(run)한 다음, 디버그(debug) 콘솔(console)을 확인한다. 이전 연산자(operator)의 출력(output)과 동일해야 한다.:

    ——— Example of: prefix(while:) ———
    1
    2
    Completed with: finished

    다음은 가장 복잡한 prefix(untilOutputFrom:) 연산자(operator)이다. 다시 한 번 언급하지만, 두 번째 publisher가 내보낼(emits) 때까지 값(values)을 건너 뛰는 drop(untilOutputFrom:)과 달리, prefix(untilOutputFrom:)은 두 번째 publisher가 내보낼(emits) 때까지 값을 사용한다.

    사용자가 두 번만 누를 수있는 버튼(button)이 있다고 가정할 때, 두 번 누르는 즉시 버튼(button)의 추가 탭(tap) 이벤트(events)를 생략해야 한다:

     

    마지막 예제를 playground에 추가한다:

    example(of: "prefix(untilOutputFrom:)") {
        let isReady = PassthroughSubject<Void, Never>() //isReady 상태
        let taps = PassthroughSubject<Int, Never>() //사용자에 의한 탭
        //수동으로 값을 내보낼 수 있는 두 개의 PassthroughSubject를 생성한다.
    
        taps
            .prefix(untilOutputFrom: isReady)
            //isReady에서 값을 하나 이상 내보낼 때까지 통과시킨다.
            .sink(receiveCompletion: { print("Completed with: \($0)") },
                  receiveValue: { print($0) })
            .store(in: &subscriptions) //저장
    
        (1...5).forEach { n in //5개의 탭을 내보낸다.
            taps.send(n)
    
            if n == 2 { //2번째 탭일때, isReady에서 값을 내보낸다.
                isReady.send()
            }
        }
    }

    drop(untilOutputFrom:) 예제를 상기해본다면 더 이해하기 쉬울 것이다.

    playground를 실행(run)한다. 콘손(console)은 다음은 같이 출력되어야 한다:

    ——— Example of: prefix(untilOutputFrom:) ———
    1
    2
    Completed with: finished

     

    Challenge

    지금까지 필터링(filtering)에 대해 꽤 많은 것을 배웠다. 과제(challenge)를 진행해보면서 복습해 본다.

     

    Challenge: Filter all the things

    1에서 100까지의 숫자 컬렉션(collection)을 게시(publishes)하는 예제를 만들고, 필터링 연산자(filtering operators)를 사용하여 다음 작업을 수행한다:

    1. 업 스트림(upstream) publisher가 내보낸(emitted) 처음 50개 값(values)은 건너뛴다(skip).
    2. 첫 50개의 값(values) 이후에 다음 20개 값(values)을 가져온다.
    3. 짝수만 가져온다.

    예제의 출력은 한 줄에 하나씩 다음과 같은 숫자가 되어야 한다:

    52 54 56 58 60 62 64 66 68 70

    Note 이 챌린지(challenge)에서는 여러(multiple0 연산자(operators)를 함께 연결(chain)하여, 원하는 값(desired values)을 산출(produce)해야 한다.

    projects/challenge/ Final.playground에서 이 챌린지(challenge)에 대한 완전한 해결책(solution)을 확인할 수 있다.

    solution

    let numbers = (1...100).publisher
    
    numbers
        .dropFirst(50) //1번
        .prefix(20) //2번
        .filter { $0 % 2 == 0} //3번
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)

     

    Key points

    이 장(chapter)에서 배운 내용은 다음과 같다:

    • 필터링 연산자(filtering operators)를 사용하면 업 스트림(upstream) publisher가 다운 스트림(downstream), 다른 연산자(another operator), 소비자(consumer) 등 에게 내보낸(emitted) 값(values)을 제어(control)할 수 있다.
    • 값(values) 자체에 신경 쓸 필요없고, 완료(completion) 이벤트(event)만 필요한 경우 ignoreOutput을 사용할 수 있다.
    • 값 찾기(Finding values)는 다른 종류(sort)의 필터링(filtering)으로, first(where:)last(where:)를 사용하여 각각 제공된(provided) 조건(predicate)과 일치하는 첫 번째(first) 또는 마지막(last) 값을 찾을 수 있다.
    • first 유형(First-style)의 연산자(operators)는 lazy이다. 필요한 만큼의 값(values)만 가져온 다음 완료(complete)한다. last 유형(Last-style)의 연산자(operators)는 조건(condition)을 충족(fulfill)시키는 마지막 값(values)을 결정하기 전에 값의 전체 범위(scope)를 알아야 한다(greedy).
    • drop 계열(family) 연산자(operators)를 사용하여 다운 스트림(downstream) 값(values)을 내보내(sending)기 전에 업 스트림(upstream) publisher가 내보낸(emitted) 값(values)을 얼만큼 무시(ignored)할 지 제어(control)할 수 있다.
    • 마찬가지로 prefix 계열(family) 연산자(operators)를 사용하여, 완료(completing) 전에 업 스트림(upstream) publisher가 내보낼(emit) 수 있는 값(values)의 수를 제어(control)할 수 있다.
Designed by Tistory.