Language/Swift

[Swift] Reactive Programming Combine - 4: Filtering Operators

jaewpark 2024. 5. 8. 21:48

Filtering Operatros

filter

어떤 값을 전달할 지 조건부로 결정하는 filter입니다.

filter

var subscriptions = Set<AnyCancellable>()

let numbers = (1...6).publisher

numbers
    .filter { $0.isMultiple(of: 2) }
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// *--- RESULT --*
// 2
// 4
// 6

 

removeDuplicates

동일한 값을 연속으로 내보내는 publisher를 무시합니다.

removeDuplicates

let words = "hey hey there! want to listen to mister mister ? ? ?"
    .components(separatedBy: " ")
    .publisher

words
    .removeDuplicates()
    .sink(receiveValue: { print($0, terminator: " ") })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// hey there! want to listen to mister ?

 

 

Equtable에 부합하지 않는 값이라면 클로저에서 값이 같은지를 Bool로 반환하면 됩니다.

let tuples = [(1, 3), (1, 3), (2, 2), (2, 4), (2, 2), (1, 3), (1, 3)]
    .publisher

tuples
    .removeDuplicates { $0.0 == $1.0 && $0.1 == $1.1 }
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// (1, 3)
// (2, 2)
// (2, 4)
// (2, 2)
// (1, 3)

 

compactMap

optional value를 반환하는 publisher인 경우, 모든 nil을 처리할 때 사용합니다.

let strings = ["a", "1.24", "3",
               "def", "45", "0.23"].publisher

strings
    .compactMap { Float($0) }
    .sink(receiveValue: {
        print($0)
    })
    .store(in: &subscriptions)


// *--- RESULT ---*
// 1.24
// 3.0
// 45.0
// 0.23

 

ignoreOutput

publisher의 실제 값을 무시하고 값만 output 했다는 사실만 알고 싶은 경우에 사용됩니다.

ignoreOutput

let numbers = (1...10_000).publisher

numbers
    .ignoreOutput()
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

// *--- RESULT ---*
// Completed with: finished

 

first

조건에 맞는 첫 번째 값만 내보냅니다.

first

 

조건과 일치하는 값을 찾은 뒤, 구독을 통해 취소를 전송하여 upstream에서 값 전송을 중지합니다.

let numbers = (1...9).publisher
    .print("numbers")

numbers
    .first(where: { $0 % 2 == 0 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// numbers: receive subscription: (1...9)
// numbers: request unlimited
// numbers: receive value: (1)
// numbers: receive value: (2)
// numbers: receive cancel
// 2
// Completed with: finished

 

last

조건에 일치하는 마지막 값만 내보냅니다.

first와 달리 일치하는 값을 찾았는지 확인하기 위해 모든 값이 나올 때까지 기다려야 합니다.

따라서 upstream은 완료되는 publisher여야 합니다.

last

 

dropFirst

특정 개수의 값을 무시할 때 사용합니다.

dropFirst

 

drop(while:)

조건을 처음 충족할 때까지 내보내는 모든 값을 무시합니다.

조건문에서 false를 무시하다가 true가 되는 시점 이후 모든 값을 내보냅니다. 

drop(while:)

 

조건이 충족된 후에는 다시 실행되지 않습니다.

let numbers = (1...10).publisher

numbers
    .drop(while: {
        print("x")
        return $0 % 4 != 0
    })
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// x
// x
// x
// x
// 4
// 5
// 6
// 7
// 8
// 9
// 10

 

drop(untilOutputFrom:)

특정 publisher가 어떤 값을 내보낼 때까지 모든 값을 무시합니다.

drop(untilOutputFrom:)

 

isReady에 값이 오기 전까지 timerPublisher의 값들은 무시됩니다.

prev가 4가 되면 isReady에 Void 값을 내보내고 5부터 출력됩니다.

let isReady = PassthroughSubject<Void, Never>()
let timerPublisher = Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
    .autoconnect()
    .scan(0) { prev, _ in
        if prev % 5 == 4 {
            isReady.send()
        }
        return prev + 1
    }

timerPublisher
    .drop(untilOutputFrom: isReady)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

 

prefix

dropFirst와 반대인 prefix는 제공된 양까지만 값을 가져온 다음 완료합니다.

first(where:)과 마찬가지로 lazy하기 때문에, 필요한 만큼의 값만 취하고 종료합니다.

prefix

let numbers = (1...10).publisher

numbers
    .prefix(2)
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

// *--- RESULT ---*
// 1
// 2
// Completed with: finished

 

prefix(while:)

클로저를 사용하여 해당 결과가 true면 upstream 값을 통과시킵니다. 결과가 false라면 publisher가 완료됩니다.

let numbers = (1...10).publisher

numbers
    .prefix(while: { $0 < 3 })
    .sink(receiveCompletion: { print("Completed with: \($0)") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// 1
// 2
// Completed with: finished

 

prefix(untilOutputFrom:)

두번째 publisher가 값을 내보낼 때까지 값을 가져옵니다.

두번째 publisher에서 값을 내보내면 종료합니다.

drop(untilOutputFrom:)

 

let isReady = PassthroughSubject<Void, Never>()
let timerPublisher = Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
    .autoconnect()
    .scan(0) { prev, _ in
        if prev % 5 == 4 {
            isReady.send()
        }
        return prev + 1
    }

timerPublisher
    .prefix(untilOutputFrom: isReady)
    .sink(receiveCompletion: {print("Completion with: \($0)")},
          receiveValue: { print($0) })
    .store(in: &subscriptions)

// *--- RESULT ---*
// 1
// 2
// 3
// 4
// Completion with: finished

 


Key points

  • Filtering operators를 사용하면 upstream publisher가 어떤 값을 보낼지 제어할 수 있습니다.
  • 값 자체를 신경쓰지 않고 완료 이벤트만 원할 때에는 ignoreOutput을 사용합니다.
  • provided predicate (클로저에서 주어진 조건문)과 일치하는 첫번째 또는 마지막 값은 first(where:) 및 last(where:)로 찾을 수 있습니다.
  • First-style의 operators는 lazy하기 때문에 필요한 만큼의 값만 취한 다음 완료합니다.
  • Last-style의 operators는 greedy하기 때문에 마지막 값인지 알기 위해 전체 범위를 알아야 합니다.
  • drop 계열의 operators는 downstream으로 보내기 전에 무시할 값의 수를 제어할 수 있습니다.
  • prefix 계열의 operators는 upstream publisher가 완료하기 전에 방출할 수 있는 값의 수를 제어할 수 있습니다.