felix-iOS

[Combine] 유용한 Filtering Operator(1) 본문

Combine

[Combine] 유용한 Filtering Operator(1)

felix-mr 2021. 6. 4. 15:33

 

안녕하세요 🙋‍♂️ 

 

이번 게시글은 유용하게 사용할 수 있는 Filtering Operator에 대해 알아보려고 합니다.

이름에서도 알 수 있듯이 방출되는 element에 대하여 Filter를 하는 Operator들 입니다. 이 Operator에 지정된 제약사항들을 만족하지 못한다면 해당 element들은 삭제됩니다 :)

 

자 그럼 하나씩 직접 사용해보면서 알아봅시다!🔥

 

filter / tryFilter

func filter(_ isIncluded: @escaping (Self.Ouput) -> Bool) -> Publishers.Filter<Self>

제공된 clousure와 일치하는 모든 요소를 다시 publish합니다.

 

filter는 Combine이 아니더라도 자주 사용해봤기 때문에 다들 익숙하실 것 같습니다. 정의된 바와 같이 closure를 통해 return 받는 Bool값이 true일 경우에만 down stream으로 다시 publish 되는 형태의 Operator입니다.

간단한 예제를 통해 사용해봅시다🧑🏻‍💻

let numbers: [Int] = [1, 2, 3, 4, 5]
cancellable = numbers.publisher
  .filter { $0 % 2 == 0 }
  .sink { print("\($0)", terminator: " ") }
  
// 결과값: 2 4

filter operator를 통해 방출되는 값이 2의 배수인지를 확인하고 true인 경우 값을 계속해서 publish하는 형태입니다. 간단데쓰!

 

tryFilter의 경우에도 filter와 같은 기능을 하지만 제공되는 isIncluded closure로 인해 Error가 발생하는 경우에 사용합니다. 바로 예제로 알아봅시다.

struct DivisionByZeroError: Error {}

let numbers: [Int] = [1, 2, 3, 4, 0, 5]
cancellable = numbers.publisher
  .tryFilter {
    if $0 == 0 {
    throw DivisionByZeroError()
  } else {
    return $0 % 2 == 0
  }
}
.sink(
  receiveCompletion: { print ("\($0)") },
  receiveValue: { print ("\($0)", terminator: " ") }
)

// 결과값: 2 4 failure(DivisionByZeroError())

역시 간단합니다. 분자가 0이 되는 경우에는 DivisionByZeroError로 throw합니다. filter를 해야되는데 error throws 가 필요한 경우에 사용하면 유용할 것 같습니다!

 

dropFirst / drop(untilOutputFrom:) / drop(while:) / tryDrop(while:)

func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self>

다음 요소를 다시 publish하기 전에 지정한 수의 요소를 생략합니다.

 

dropFirst(_:)도 마찬가지로 간단한 operator이기 때문에 바로 예제로 확인해봅시다.

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
cancellable = numbers.publisher
  .dropFirst(5)
  .sink { print("\($0)", terminator: " ") }
  
  // 결과값: 6 7 8 9 10

dropFirst의 인자로 5를 넘겨줬기 때문에 처음 5개의 요소들을 생략합니다. 1 2 3 4 5는 해당 operator를 통해 생략이 되고 이후의 요소들만 publish 되는 모습을 확인할 수 있습니다 👏

 

다음은 drop(untilOutputFrom:)입니다. 일단 공식문서의 설명을 확인해볼까요?

func drop<P>(untilOutputFrom publisher: P) -> Publishers.DropUntilOutput<Self, P> where P : Publisher, Self.Failure == P.Failure

두 번째 publisher로부터 요소를 수신할 때까지 업스트림 publisher의 요소를 무시합니다.

 

오잉 이번에는 publisher가 2개가 필요하네요? 예제를 확인하면서 알아봅시다.

let upstream = PassthroughSubject<Int,Never>()
let second = PassthroughSubject<String,Never>()

cancellable = upstream
  .drop(untilOutputFrom: second)
  .sink { print("\($0)", terminator: " ") }

upstream.send(1)
upstream.send(2)
second.send("A")
upstream.send(3)
upstream.send(4)

// 결과값: 3 4

역시 예제를 통해서 알아보는게 확실히 머리에 쏙 들어오는 거 같아유😁. second가 값을 receive되기 전까지 upstream에 send되는 요소들은 모두 무시가 됩니다. second에 "A"가 send된 후에 들어오는 요소에 대해서는 값을 방출하는 것을 확인할 수 있습니다. 정말 유용할 것 같지 않나요? 깔깔!

 

다음은 drop(while:) / tryDrop(while:)입니다. try가 붙어있는 이유는 위의 filter와 마찬가지이기 때문에 설명을 생략하도록 할께요.

func drop(while predicate: @escaping (Self.Output) -> Bool) -> Publishers.DropWhile<Self>

closure가 false를 반환할 때까지 upstream publisher의 요소를 제외합니다.

 

자자 예제로 알아봐야겠죠?

let numbers = [-62, -1, 0, 10, 0, 22, 41, -1, 5]
cancellable = numbers.publisher
  .drop { $0 <= 0 }
  .sink { print("\($0)") }

// 결과값: 10 0 22 41 -1 5

살짝 헷갈리실 수도 있을 것 같습니다. 저는 헷갈렸습니다..ㅎㅎ

코드를 확인해보면 closure는 "요소가 0보다 작거나 같은가요?"를 물어보고 있습니다. 해당 closure는 -62, -1, 0까지는 true를 반환하고 10일 때 false를 반환합니다. 공식문서대로 false가 반환될 때까지(-62, -1, 0) 요소들은 무시를 해버립니다. 그리고 closure가 false를 반환하고 난 이후(10부터 끝까지)에는 나머지 요소들을 다 반환하는 것을 확인하실 수 있습니다. :)

tryDrop(while:)은 closure에 Error를 발생하는 경우에 사용하겠죠? 예쓰!

 

ignoreOuput 

오옹 이름에서부터 빡! 느낌이 오는 operator네요. 무시해라! 아웃풋을!

자 공식문서를 확인해볼까요?

func ignoreOutput() -> Publishers.IgnoreOutput<Self>

모든 요소를 무시하지만 upstream 게시자의 완료 상태(finish 또는 failure)를 따라 통과합니다.

 

띠용! 예상한대로 무시해라! 아웃풋을! 은 맞지만 완료상태는 또 통과를 하는 것 같네요? 예제 코드를 보면서 확인해봅시다 :)

struct NoZeroValuesAllowedError: Error {}

let numbers = [1, 2, 3, 4, 5, 0, 6, 7, 8, 9]

cancellable = numbers.publisher
  .tryFilter {
    guard $0 != 0 else { throw NoZeroValuesAllowedError() }
    return $0 < 20
  }
  .ignoreOutput()
  .sink(
    receiveCompletion: { print("completion: \($0)") },
    receiveValue: { print("value \($0)") }
  )

// 결과값: completion: failure(NoZeroValuesAllowedError())

먼저 tryFilter를 사용한 후에 ignoreOutput을 사용한 것을 볼 수 있습니다. ignoreOuput을 사용했기 때문에 receiveValue에는 아무것도 찍히지 않죠?

 

대신 receiveCompletion을 통해서는 결과값이 나오는 것을 확인할 수 있습니다. 요소가 0이면 에러가 발생하게 되는데 numbers에는 0이 포함되어 있어서 throw가 됩니다. 에러가 발생하면서 stream은 종료가 되고 completion으로는 failure로 결정되네요!

 

공식문서에 따르면 ignoreOutput을 통해 completion을 확인하라고 하네요 :)

 

 

이번 게시글에서는 filter / dropFirst / drop / ignoreOutput에 대해서 살펴봤습니다.

 

너무 길어지면 가독성이 떨어질 것 같아서..ㅠㅠ 2개의 게시글로 나누기로 결정했습니다 :-)

혹시 잘못된 정보나 코드에 대해서는 지적 부탁드립니다.

 

이어지는 다음 게시글에서는 prefix와 ouput에 대해 알아보겠습니다. 감사합니다.🙇‍♂️

 

reference

https://developer.apple.com/documentation/combine/publisher/filter(_:)/ 

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/combine/publisher/dropfirst(_:)/ 

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/combine/publisher/ignoreoutput()/ 

 

Apple Developer Documentation

 

developer.apple.com

 

Comments