[Combine] Advanced Combining Operator
저번 시간에는 결합 연산자로 prepend
및 append
를 살펴봤어요!
이번에는 publisher
의 결합과 관련된 족굼 더 복잡한 연산자를 알아보도록합시다!
switchToLatest
이 함수는 모든 publisher의 subscription을 최신의 것으로 변경합니다. 기존의 구독은 당연히 취소되겠죠?
오직 publisher
를 방출하는 publisher
에서만 사용할 수 있어요!
// 1. 3개의 PassthroughSubject를 생성합니다.
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let publisher3 = PassthroughSubject<Int, Never>()// 2. PassthroughSubject를 방출하는 PassthroughSubject를 만듭니다.
let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()// 3. switchToLatest를 적용함에 따라 publishers가 다른 publisher를 방출할때 마다 이전의 subscription은 취소되고 새로운 것으로 전환됩니다.
publishers
.switchToLatest()
.sink(receiveCompletion: { _ in print("Completed!") },
receiveValue: { print($0) })
.store(in: &subscriptions)// 4. publisher1을 publishers에 보낸 후 해당 publisher에 1과 2를 보냅니다.
publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)// 5. publisher2를 보냄으로써 publisher1의 subscription은 취소됩니다. 따라서 publisher1에 3을 보내더라도 무시될 것입니다. 현재 subscription은 publisher2이기 때문에 4와 5를 보내면 push 됩니다.
publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)// 6. 5의 상황과 유사합니다.
publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)// 7. 현재 publisher인 publisher3에 완료 이벤트를 보내고, publishers에도 완료 이벤트를 보냄으로써 active한 subscriptions을 모두 완료시킵니다.
publisher3.send(completion: .finished)
publishers.send(completion: .finished)
}
다이어그램에서 추측할 수 있듯이 결과는 아래와 같습니다.
1
2
4
5
7
8
9
Completed! //publisher3, publishers 모두 .finished를 보내줘야 출력됩니다.
음.. 실생활에서 어떻게 쓸지 잘 감이 안오시나요? 아래와 같은 시나리오를 생각해봅시다.
유저가 네트워크 요청을 위해 버튼을 클릭했습니다. 성격이 급한 유저는 기다리지 못하고 또 다시 버튼을 클릭했습니다. 두번째 네트워크 요청이 가겠죠? 이때 이전의 요청은 버리고 최신의 요청만 사용하고 싶다면 어떻게 할까요? 이럴때 바로 switchToLatest
를 사용할 수 있습니다! 코드를 통해 확인해보죠
let url = URL(string: "https://source.unsplash.com/random")!
// 1. 네트워크 요청을 통해 랜덤 이미지를 가져오는 함수입니다.
func getImage() -> AnyPublisher<UIImage?, Never> {
return URLSession.shared
.dataTaskPublisher(for: url)
.map { data, _ in UIImage(data: data) }
.print("image")
.replaceError(with: nil)
.eraseToAnyPublisher()
}// 2. 유저의 탭을 재현하기 위해 PassthroughSubject를 생성합니다
let taps = PassthroughSubject<Void, Never>()taps
// 3. 버튼을 탭할때마다 이미지를 요청합니다. 이를 통해 Publisher<Void, Never>에서 Publisher<Publisher<UIImage?, Never>, Never> 형식으로 바뀝니다.
.map { _ in getImage() }
// 4. Publishers의 Publisher이기 때문에 switchToLatest를 사용할 수 있습니다. 이는 하나의 publisher만 값을 방출하고 이전의 구독은 취소될 것을 의미합니다.
.switchToLatest()
.sink(receiveValue: { _ in })// 5. DispatchQueue를 이용해 세번의 지연된 버튼 탭을 재현합니다.
taps.send() DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
taps.send()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10.1) {
taps.send()
}
콘솔에는 다음과 같이 출력됩니다.
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(<UIImage:0x600000364120 anonymous {1080, 720}>))
image: receive finished //첫번째 요청 완료
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive cancel //두번째 요청 취소됨
image: receive subscription: (DataTaskPublisher)
image: request unlimited
image: receive value: (Optional(<UIImage:0x600000378d80 anonymous {1080, 1620}>))
image: receive finished //세번째 요청 완료
세번의 요청이 있었지만 두개의 이미지만 가져온것을 확인할 수 있습니다. 그 이유는 다음과 같습니다.
- 첫번째 요청이 이뤄진 10초 후에 두번째 요청이 이뤄졌기 때문에 첫번째 요청은 완료될 충분한 시간이 있었습니다.
- 두번째 요청의 경우 0.1초 후에 바로 세번째 요청이 이뤄졌습니다. 즉, 두번째는 완료될 충분한 시간이 없이 바로 다른 요청이 들어온 것입니다.
- 처리 도중에 요청이 들어왔기 때문에 이전의 구독(두번째)은 취소됩니다. 콘솔창에 찍힌
image: receive cancel.
을 확인할 수 있습니다. - 새로운 요청(세번째)으로 변경 및 처리됩니다.
각 publisher의 값 결합하기
자 마지막으로 각 publisher에서 방출하는 값들을 결합하는 세 종류의 연산자를 살펴봅시다!
merge(with:)
해당 연산자는 아래 그림과 같이 각 publisher
의 값을 교차 배치합니다. 물론 각 publisher
는 같은 타입입니다.
// 1. 두개의 PassthroughSubject를 만듭니다.
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()// 2. publisher1과 publisher2를 merge함으로써 방출되는 값들을 교차 배치합니다. 최대 8개까지 결합할 수 있습니다.
publisher1
.merge(with: publisher2)
.sink(receiveCompletion: { _ in print("Completed") },
receiveValue: { print($0) })// 3. publisher1과 publisher2에 값을 보냅니다.
publisher1.send(1)
publisher1.send(2) publisher2.send(3) publisher1.send(4) publisher2.send(5)// 4. 완료 이벤트를 보냅니다.
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
결과는 아래와 같습니다.
1
2
3
4
5
Completed
combineLatest
combineLatest
는 각 publisher들을 결합시켜주는 또 하나의 연산자입니다. 이는 다른 타입의 값도 결합시킴으로써 매우 유용합니다. 하지만 값들을 교차 배치하는 대신, 어느 한 publisher가 값을 방출할때마다 각 publisher
의 마지막 값들을 tuple
로 방출합니다.
한가지 유의할 점은 기존 publisher
및 combineLatest
로 전달되는 모든 publisher
가 적어도 한개의 값을 방출해야 combineLatest
가 동작한다는 점입니다.
// 1. 두개의 PassthroughSubject를 만듭니다.
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()// 2. combineLatest를 통해 마지막 방출 값들을 결합합니다. 최대 4개의 publisher까지 결합할 수 있습니다.
publisher1
.combineLatest(publisher2)
.sink(receiveCompletion: { _ in print("Completed") },
receiveValue: { print("P1: \($0), P2: \($1)") })// 3. publisher1과 publisher2에 각각 값을 전송합니다.
publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")// 4. 완료 이벤트를 보냅니다.
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
결과는 아래와 같습니다.
P1: 2, P2: a
P1: 2, P2: b
P1: 3, P2: b
P1: 3, P2: c
Completed
여기서 주목해야할 점은 publisher1
에서 방출된 1은 combineLatest
를 통해 나오지 않았다는 점입니다. 왜냐하면 combineLatest
는 모든 publisher
가 한번씩은 값을 방출해야 동작하니까요!!
zip
zip
연산자는 위의 combineLatest
와 비슷하게 동작합니다 .다만 차이점은 같은 index의 값들이 tuple
로 방출된다는 점이죠! 즉, 모든 publisher
가 특정 index
에 해당하는 값을 내보냈을때 하나의 tuple
을 방출합니다.
만약 두개의 publisher
를 zip
으로 묶는다면, 두 publisher
모두 값을 방출할때 하나의 tuple
을 얻을 수 있다는 뜻인데..! 그림으로 보면 조금 더 이해가 쉬울거예요!
기존 publisher
가 1
, 2
를 방출할때까지는 아무 일도 일어나지 않습니다. 이때 각각의 index
를 0
, 1
이라고 해볼게요.
시간이 조금 지난 후에 publisher2
가 a
를 방출하자마자 (1,a)
의 tuple
로 묶입니다. 즉!! 기존 publisher
의 index 0
에 해당하는 1
과 publisher2
의 index 0
에 해당하는 a
가 묶인거죠!!
조금 후에 publisher2
에서 b
가 방출됩니다. 이는 publisher2
의 index 1
값에 해당하겠죠? 기존 publisher
의 index 1
에 해당하는 2
는 이때서야 비로소 다른 publisher
에서도 자신의 index
와 대응되는 값이 방출된것을 알고 (2,b)
라는 tuple
로 묶여 나옵니다.
// 1. 두개의 PassthroughSubject를 만듭니다.
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()// 2. publisher1과 publisher2를 zip함으로써 대응되는 값들을 짝짓습니다.
publisher1
.zip(publisher2)
.sink(receiveCompletion: { _ in print("Completed") },
receiveValue: { print("P1: \($0), P2: \($1)") })// 3. publisher1과 publisher2에 각각 값을 전송합니다.
publisher1.send(1)
publisher1.send(2)
publisher2.send("a")
publisher2.send("b")
publisher1.send(3)
publisher2.send("c")
publisher2.send("d")// 4. 완료 이벤트를 보냅니다.
publisher1.send(completion: .finished)
publisher2.send(completion: .finished)
}
결과는 아래와 같습니다.
P1: 1, P2: a
P1: 2, P2: b
P1: 3, P2: c
Completed
두번째 publisher
가 마지막으로 방출한 d
는 콘솔에 안찍혔는데요!! 이유는 첫번째 publisher
에서 이에 대응하는 값을 방출하지 않았기 때문입니다.
마무리
자자 저번 포스팅에 이어 지금까지 결합 연산자에 대해 알아보았습니다! 도움이 되었으면 좋겠네욥 ㅎ 그럼 안뇽~~~