티스토리 뷰
Combine ) Operator (8) - Combining Elements from Multiple Publishers
Zedd0202 2020. 9. 6. 00:55
안녕하세요 :) Zedd입니다.
오늘은..8번째 시리즈..
Combining Elements from Multiple Publishers
입니다.
combineLatest
merge(with:)
zip
봐야할 것은 이 3개입니다.
이 전 시리즈들을 보시려면 여기를 참고해주세요!
combineLatest
combineLatest는 additional publisher를 구독하고,
두 게시자로부터 ouput을 수신하면 클로저를 호출하는 친구입니다.
일단은
이런식으로 사용할 수 있습니다.
왜 저런 결과가 나왔는지 하나씩 보도록 합시다.
일단 보기전에, "두 게시자로부터 ouput을 수신하면" 클로저를 호출하는 친구라는 사실을 기억해야합니다.
먼저 combineLatest를 사용한 코드부터 보겠습니다.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
.combineLatest(pub2) { (first, second) in
return first * second
}
.sink { print("Result: \($0).") }
코드는 간단합니다. pub1과 pub2에 들어가는 integer를 곱해서 결과를 내놓습니다.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
.combineLatest(pub2) { (first, second) in
return first * second
}
.sink { print("Result: \($0).") }
pub1.send(2) // ✔️
pub1에 2를 보냈습니다.
하지만 아무것도 출력이 안됩니다.
왜냐? combineLatest는 "두 게시자로부터 ouput을 수신하면" 클로저를 호출합니다.
pub1으로부터 값을 받았지만, pub2에게서는 아무값도 못받았으니, 클로저가 실행이 안됩니다.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
.combineLatest(pub2) { (first, second) in
return first * second
}
.sink { print("Result: \($0).") }
pub1.send(2)
pub2.send(2) // ✔️
// Prints:
//Result: 4. (pub1 latest = 2, pub2 latest = 2)
자 이제야 비로소 4가 출력이 됩니다.
왜냐면 pub1과 pub2 둘 다에 output이 수신됐기 때문이죠.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
.combineLatest(pub2) { (first, second) in
return first * second
}
.sink { print("Result: \($0).") }
pub1.send(2)
pub2.send(2)
pub1.send(9) // ✔️
// Prints:
//Result: 4. (pub1 latest = 2, pub2 latest = 2)
//Result: 18. (pub1 latest = 9, pub2 latest = 2)
자 그리고 pub1에 9를 보내겠습니다.
pub1만 보냈으니 값이 안나올거야! 라고 생각하실수도 있지만..아닙니다.
바로 18이 나오게 됩니다. (2 * 9)
pub2에는 이전에 우리가 넣어줬던 2가 마지막 값으로 있기 때문입니다.
(combineLatest는 1의 버퍼크기를 이용하여 각 버퍼에 가장 최근값을 가지고 있는다고 해요.)
그러니까 pub2의 버퍼에는 2가 들어있는겁니다.
그래서 pub1의 9와 pub2의 2가 곱해져서 18의 결과가 나오게 된것이죠.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
.combineLatest(pub2) { (first, second) in
return first * second
}
.sink { print("Result: \($0).") }
pub1.send(2)
pub2.send(2)
pub1.send(9)
pub1.send(3)
pub2.send(12)
pub1.send(13)
//
// Prints:
//Result: 4. (pub1 latest = 2, pub2 latest = 2)
//Result: 18. (pub1 latest = 9, pub2 latest = 2)
//Result: 6. (pub1 latest = 3, pub2 latest = 2)
//Result: 36. (pub1 latest = 3, pub2 latest = 12)
//Result: 156. (pub1 latest = 13, pub2 latest = 12)
그 뒤로는 다 똑같습니다. 하나하나 해보세요.
combineLatest이름을 그냥 생각하시면 됩니다. 결합하다 + 가장 최근(값이랑)
RxSwift때도 그랬지만..CombineLatest가 Validation체크에 종종 쓰이곤 하는데요
class LoginViewModel: ObservableObject {
@Published var id: String = ""
@Published var password: String = ""
var validInfo: AnyPublisher<Bool, Never> {
return self.$password.combineLatest(self.$id) {
return $0 == $1
}.eraseToAnyPublisher()
}
}
뭐 이런식으로 사용될 수 있겠죠?
놀랍게도 id와 password가 같아야지만 valid하다고 판단하는...코드입니다.
뭐 이 부분은 제가 대충한거라 알아서들 바꾸시면 될 것 같아요.
struct ContentView: View {
@ObservedObject var viewModel: LoginViewModel = LoginViewModel()
@State private var isValid = false
var body: some View {
Form {
TextField("아이디", text: self.$viewModel.id)
TextField("비밀번호", text: self.$viewModel.password)
Button("로그인", action: {
}).disabled(!self.isValid)
}
.onReceive(self.viewModel.validInfo,
perform: { self.isValid = $0 })
}
}
View쪽에서는 이런식으로 사용할 수 있습니다.
class LoginViewModel: ObservableObject {
@Published var id: String = ""
@Published var password: String = ""
var validInfo: AnyPublisher<Bool, Never> {
return Publishers.CombineLatest(self.$id, self.$password).map {
return $0 == $1
}.eraseToAnyPublisher()
}
}
이렇게도 사용할 수 있습니다.
참고로 combienLatest의 publisher가 완료(finish)되려면 모든 업스트림 publisher가 완료되어야합니다.
만약 업스트림이 계속 값을 publish하지 않는다면 이 publisher는 종료가 안됩니다...
역시나 결합된 publisher중 하나라도 실패로 종료되면 이 publisher역시 실패하게 됩니다.
combineLatest는...
6개가;;; 있는데..뭐 별건 아닌것 같고..
아셔야 할 건 그냥 결합할 수 있는 친구들의 개수가 최대 4개입니다.
return self.$password.combineLatest(self.$id, self.$id, self.$id) { (id, password, id2, id3) in
return id == id2
}.eraseToAnyPublisher()
return Publishers.CombineLatest3(self.$id, self.$password, self.$id).map { (id, pw, id2) in
return id == id2
}.eraseToAnyPublisher()
뭐 이런식....CombineLatest4까지 있습니다.
merge
merge가 머지...?
ㅋㅋ
웃기죠
(머지가 merge..? 로 응용도 가능)
merge는 말 그대로 합치는겁니다.
publisher의 요소를 동일한 타입의 다른 publisher의 요소와 결합하여
interleave된 요소 시퀀스를 제공하는 친구라고 해요.
그냥 정말 하나의 스트림으로 합쳐주는 역할입니다.
2개의 publisher를 하나의 publisher로 다루고 싶을 때 유용합니다.
이것도 하나씩 살펴보도록 합시다.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
let cancellable = pub1.merge(with: pub2)
.sink { print("\($0)", terminator: " " )}
pub1.send(2)
// Prints: "2"
combineLatest처럼 뭐 다른 publisher에 값이 와야하는게 아닙니다. 그냥 바로바로 와요.
let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
let cancellable = pub1.merge(with: pub2)
.sink { print("\($0)", terminator: " " )}
pub1.send(2)
pub2.send(2)
pub1.send(3)
pub2.send(22)
pub1.send(45)
pub2.send(22)
pub1.send(17)
// Prints: "2 2 3 22 45 22 17"
pub1과 pub2를 merge하는 스트림을 구독하고 있으므로
pub1에 보내든 pub2에 보내든..그냥 다 값을 잘 받습니다.
merged publisher는 모든 업스트림 publisher가 완료(finish)될 때 까지 계속 요소를 내보내며,
업스트림 publisher가 오류를 생성하면 merged publisher가 해당 오류와 함께 실패하게 됩니다.
Merge는 최대 8개까지 할 수 있을 것 같지만..
Publishers.MergeMany라는 것이 존재합니다..
Publishers.MergeMany(self.$id, self.$password, self.$id, self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id).sink { (result) in
print(result)
}.store(in: &self.cancellable)
대충 이런거 가능
merge를 어떨 때 쓰면 좋냐면..일단 뭔가 응답오는 순서는 상관없고
그냥 일괄적으로 api를 요청하고 싶을 수 있습니다.
출처 : stackoverflow.com/a/58708381
이럴 때 merge를 쓰면 좋습니다.
1, 2, 3순으로 요청했다고 응답이 1, 2, 3으로 오는건 아니구요.
실행때마다 달라질 수 있습니다.
zip
zip은 다른 publisher의 요소를 결합하고 요소 쌍을 tuple로 제공하는 친구입니다.
zip는 각각의 publisher가 주는 값을 "결합하는" 친구이기 때문에
combineLastest처럼 두 publisher모두에게 값이 오지않으면 값을 방출하지 않습니다.
그리고 묶인 publisher는 각 publisher에서 "가장 오래 사용되지 않은" 이벤트를
구독자에게 tuple로 넘겨준다는 사실도 아셔야합니다.
예제로 봅시다.
numbersPub과 lettersPub을 zip으로 함께 묶었습니다.
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
cancellable = numbersPub
.zip(lettersPub)
.sink { print("\($0)") }
numbersPub.send(1) // ✔️
numbersPub에 값을 보내고, lettersPub에는 값을 안보냈죠.
두 publisher모두에게 값이 오지않으면 값을 방출하지 않으므로 아무것도 출력되지 않습니다.
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
cancellable = numbersPub
.zip(lettersPub)
.sink { print("\($0)") }
numbersPub.send(1)
numbersPub.send(2) // ✔️
역시 lettersPub에는 아직 값이 없으므로 아무것도 출력되지 않습니다.
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
cancellable = numbersPub
.zip(lettersPub)
.sink { print("\($0)") }
numbersPub.send(1)
numbersPub.send(2)
lettersPub.send("A") // ✔️
// Prints:
// (1, "A")
자..lettersPub에도 값이 보내졌으니 두 publisher에 모두 값이 있습니다.
tuple형식으로 나왔고, (1, A)가 나왔네요. 2는 어디간거죠...?
아까
각 publisher에서 "가장 오래 사용되지 않은" 이벤트를 구독자에게 tuple로 넘겨준다..고 그랬었죠?
numbersPub에서 가장 오래 사용되지 않은 값은 1이기 때문에 1을 넘겨준겁니다.
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
cancellable = numbersPub
.zip(lettersPub)
.sink { print("\($0)") }
numbersPub.send(1)
numbersPub.send(2)
lettersPub.send("A")
numbersPub.send(3) // ✔️
// Prints:
// (1, "A")
자.. numbersPub에 3을 보냈습니다만..값이 나올까요?
네 나오지 않습니다! 왜냐면 lettersPub에 있는 A는 이미 (1, "A")에서 써버렸거든요.
let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()
cancellable = numbersPub
.zip(lettersPub)
.sink { print("\($0)") }
numbersPub.send(1)
numbersPub.send(2)
lettersPub.send("A")
numbersPub.send(3)
lettersPub.send("B") // ✔️
// Prints:
// (1, "A")
// (2, "B")
lettersPub에 B를 보냅니다.
numbersPub에서 "가장 오래 사용되지 않은" 값은 2네요?
그럼 (2, "B")를 묶어서 tuple로 나오게 됩니다.
아시겠나요?
Zip은 4개까지 할 수 있는 것 같습니다. (ZipMany같은거 없음;;)
그럼 역시나..실제로 어떻게 쓰면 좋을지..생각해봅시다. merge때 썼던..예제를 좀 수정해봤어요.
api를 호출하고 싶은데 이 비동기 작업이 완료될 때 까지 기다리고 싶다! 하면 zip을 사용하면 간단해집니다.
combineLatest를 사용해도 되겠지만..그냥 어떤 느낌으로 쓰는건지 봐주세요!
공부하면서 정말 재밌었네요 XD
틀린 부분을 발견하시면 댓글로 알려주세요!!
'Combine' 카테고리의 다른 글
Combine ) Operator (9) - Handling Errors (0) | 2020.10.24 |
---|---|
Combine ) Operator (7) - Selecting Specific Elements (0) | 2020.07.30 |
Combine ) Operator (6) - Applying Matching Criteria to Elements (1) | 2020.07.27 |
Combine ) Operator (5) - Applying Sequence Operations to Elements (0) | 2020.07.26 |
Combine ) Operator (4) - Applying Mathematical Operations on Elements (1) | 2020.06.14 |
- Swift
- swift tutorial
- swift 공부
- 제이슨 파싱
- WWDC
- np-hard
- swift sort
- Accessibility
- fastlane
- Combine
- swift delegate
- 스위프트 문법
- FLUTTER
- WidgetKit
- ios 13
- WKWebView
- IOS
- 회고
- UIBezierPath
- SwiftUI
- swift3
- iOS delegate
- swift array
- Git
- 피아노
- 스위프트
- github
- np-complete
- actor
- Xcode
- Total
- Today
- Yesterday