Language/Swift

[Swift] Reactive Programming Combine - 5: Combining Operators

jaewpark 2024. 5. 11. 13:18

Combining Operators

prepend

... synctax를 사용하여 다양한 값(원래 publisher와 동일한 유형)의 목록을 받습니다.

let publisher = [3, 4].publisher

publisher
  .prepend(1, 2)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)
  
// *--- RESULUT ---*
// 1
// 2
// 3
// 4

 

prepend 연산자가 체인 연결을 하는 것은 접두사로 추가한다는 의미입니다.

그렇기에 연산자의 순서가 중요합니다.

let publisher = [3, 4].publisher

publisher
  .prepend(1, 2)
  .prepend(3, 4)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)
  
// *--- RESULUT ---*
// 3
// 4
// 1
// 2
// 3
// 4

 

시퀀스에 부합하는 객체를 받을 수도 있습니다.

let publisher = [3, 4].publisher

publisher
  .prepend([1, 2])
  .prepend(Set(0...2))
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

 

두 개의 publisher를 연결할 수도 있습니다.

let publisher1 = [3, 4].publisher
let publisher2 = [103, 104].publisher

publisher2
    .prepend(publisher1)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
    
// *--- RESULT ---*
// 3
// 4
// 103
// 104

 

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = [103, 104].publisher

publisher2
    .prepend(publisher1)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

publisher1.send(3)
publisher1.send(4)
publisher1.send(completion: .finished)  // 완료를 하지 않으면 publisher2의 값을 내보내지 않음

 

append

prepend와는 반대로 뒤에 값을 연결합니다.

또한, 앞선 publisher가 완료되어야 값을 내보냅니다.

let publisher1 = [1, 2].publisher
let publisher2 = [3, 4].publisher

publisher1
  .append(publisher2)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

// *--- RESULT ---*
// 1
// 2
// 3
// 4
let publisher = PassthroughSubject<Int, Never>()

publisher
  .append(3, 4)
  .append(5)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

publisher.send(1)
publisher.send(2)

// *--- RESULT ---*
// 1
// 2
  let publisher = [1, 2, 3].publisher
  publisher
    .append([4, 5])
    .append(stride(from: 6, to: 11, by: 3))
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

 

switchToLatest

보류 중인 publisher 구독을 취소하면서 최신 구독으로 전환할 수 있습니다.

switchToLatest

 

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let publisher3 = PassthroughSubject<Int, Never>()

let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()

publishers
    .switchToLatest()
    .sink(receiveCompletion: { _ in print("Completed!") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)

publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)

publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)

publisher3.send(completion: .finished)
publishers.send(completion: .finished)

// *--- RESULT ---*
// 1
// 2
// 4
// 5
// 7
// 8
// 9
// Completed!

 

네트워크 요청을 트리거할 때 보류 중인 요청을 없애고 최신의 요청 값만 사용한다면 switchToLastest를 사용할 수 있습니다.

3초 뒤에 트리거되고 3.1초 트리거가 되면서 3초에 보낸 요청을 취소하는 것을 알 수 있습니다.

let url = URL(string: "https://source.unsplash.com/random")!

func getImage() -> AnyPublisher<UIImage?, Never> {
    return URLSession.shared
        .dataTaskPublisher(for: url)
        .map { data, _ in UIImage(data: data) }
        .print("image")
        .replaceError(with: nil)
        .eraseToAnyPublisher()
}

let taps = PassthroughSubject<Void, Never>()

taps
    .map { _ in getImage() }
    .switchToLatest()
    .sink(receiveValue: { _ in })
    .store(in: &subscriptions)

taps.send()

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    taps.send()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 3.1) {
    taps.send()
}

 

merge

다른 publisher와 결합할 수 있습니다.

merge

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

publisher1
    .merge(with: publisher2)
    .sink(receiveCompletion: { _ in print("Completed") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

publisher1.send(1)
publisher1.send(2)
publisher2.send(3)
publisher1.send(4)
publisher2.send(5)

 

 

combineLatest

서로 다른 값 유형의 publisher와 결합할 수 있습니다.

하지만 모든 publisher가 값을 내보낼 때마다 최신 값이 포함된 튜플 형식의 값을 내보냅니다.

combineLatest

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()

publisher1
    .combineLatest(publisher2)
    .sink(receiveCompletion: { _ in print("Completed") },
          receiveValue: { print("P1: \($0), P2: \($1)") })
    .store(in: &subscriptions)

publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")

publisher1.send(completion: .finished)
publisher2.send("d")
publisher2.send(completion: .finished)

// *--- RESULT ---*
// P1: 2, P2: a
// P1: 2, P2: b
// P1: 3, P2: b
// P1: 3, P2: c
// P1: 3, P2: c
// Completed

 

zip

Swift 표준 라이브러리 메서드와 비슷하게 쌍을 이루는 값의 튜플 형식의 값을 내보냅니다. 

zip

 

다른 publisher가 값을 내보낼 때까지 대기하는 방식입니다.

publisher2가 여러 번 값을 내보낸 뒤 종료가 되었다면 그 값을 무시됩니다.

 


Key points

  • switchToLatest는 최신 publisher로 전환하고 이전 publisher의 구독을 취소합니다.
  • merge는 여러 publisher의 값을 연결할 수 있습니다.
  • combineLatest는 결합된 모든 publisher가 하나 이상의 값을 내보내면 모든 publisher가 값을 내보낼 때마다 결합된 최신 값을 내보냅니다.
  • zip은 쌍으로 묶어 모든 publisher가 값을 내보내면 튜플 형식으로 값을 내보냅니다.