안녕하세요! 후르륵짭짭입니다.
다시 Combine으로 돌아 왔습니다.
이번에는 Combine의 Operator들에 대해서 정리하고 주의할 점들에 대해 정리하고 마치려고 합니당!
** Operator **
- Map -
Map은 특정 타입으로 내려온 값을 다른 타입으로 변형할 때 사용합니다.
func test_map(){
let publishSubject = PassthroughSubject<[Int], Never>()
publishSubject.flatMap { elementList -> AnyPublisher<Int,Never> in
return elementList.publisher.eraseToAnyPublisher()
}
.map { element -> String in
return "Hello \(element)"
}
.sink { result in
print("\(result) World")
}
.store(in: &bag)
publishSubject.send([1,2,2,3,3,4,5])
}
- tryMap -
tryMap은 Map과 같습니다. 하지만 다른 점은 throw가 되어 있어서 Error Catch가 가능합니다.
func test_tryMap(){
let publishSubject = PassthroughSubject<Int, CombineError>()
publishSubject
.tryMap({ number -> String in
if number % 2 == 0 {
throw CombineError.fail
}
return "Hello \(number)"
})
.sink { comp in
print(comp)
} receiveValue: { result in
print(result)
}
.store(in: &bag)
publishSubject.send(3)
}
- Scan -
Scan은 초기값에 input으로 내려온 값을 원하는 연산을 한 후 방출합니다.
그리고 다음 Input이 내려오면 기존에 변경된 초기값에 연산을 처리해서 다시 방출합니다.
func test_scan(){
let publishSubject = PassthroughSubject<Int, CombineError>()
publishSubject
.scan(2, { inital , input in
return inital + input
})
.sink { comp in
print(comp)
} receiveValue: { result in
print(result)
}
.store(in: &bag)
publishSubject.send(3)
publishSubject.send(10)
}
//결과
5
15
func test_tryScan(){
let publishSubject = PassthroughSubject<Int, CombineError>()
publishSubject
.tryScan(2, { inital, input in
if (inital + input) % 15 == 0 {
throw CombineError.fail
}
return inital + input
})
.sink { comp in
print(comp)
} receiveValue: { result in
print(result)
}
.store(in: &bag)
publishSubject.send(3)
publishSubject.send(10)
}
- Filter -
Filter는 말 그대로 BOOL 값 상태로 해당 값을 분류해줍니다.
Input으로 내려온 값이 연산처리에서 True라면 DownStream으로 내려주고 그게 아니면 걸러 줍니다.
func test_tryFilter(){
let publishSubject = PassthroughSubject<Int, CombineError>()
publishSubject
.tryFilter({ number in
if number % 2 == 0 {
throw CombineError.fail
}
return true
})
.sink { comp in
print(comp)
} receiveValue: { result in
print(result)
}
.store(in: &bag)
publishSubject.send(3)
publishSubject.send(10)
}
- compactMap -
CompactMap은 원래 알던 CompactMap 처럼 Nil 값에 대해서는 필터링 해서 반환 해줍니다.
func test_compactMap(){
["1","2","3",nil,"5"].publisher
.compactMap { number -> Int? in
return Int(number ?? "")
}
.sink { result in
print(result)
}
.store(in: &bag)
}
//결과
1
2
3
5
- removeDuplicate -
Input으로 내려온 값이 이전에 이미 DownStream으로 내려간 Input과 동일하다면 걸러줍니다.
func test_removeDuplicate(){
["1","2","3","3","3"].publisher
.removeDuplicates()
.sink { result in
print(result)
}
.store(in: &bag)
["4","4","4","5","6"].publisher
.removeDuplicates(by: { before, after in
return before == after
})
.sink { result in
print(result)
}
.store(in: &bag)
}
//결과
1
2
3
4
5
6
- replaceError -
ReplaceError는 에러가 발생했을 때 receiveCompletion 뿐만 아니라 receiveValue에도 값을 Return 해줍니다.
receiveCompletion -> Fail
receiveValue - > 값
func test_replaceError(){
let publishSubject = PassthroughSubject<Int, CombineError>()
publishSubject
.tryMap({ number -> String in
if number % 2 == 0 {
throw CombineError.fail
}
return "Hello \(number)"
})
.replaceError(with: "Error Find")
.sink { comp in
print(comp)
} receiveValue: { result in
print(result)
}
.store(in: &bag)
publishSubject.send(2)
}
//결과
Error Find
finished
- Collect -
Complete 되는 시점에 Input 값들을 모아하 한번에 반출해준다.
func test_Collect(){
let publisher = PassthroughSubject<Int,Never>()
publisher.collect()
.sink(receiveValue: { item in
print(item)
}).store(in: &bag)
publisher.send(10)
publisher.send(10)
publisher.send(10)
publisher.send(10)
publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
}
- CollectByCount -
Complete 되는 시점에 Input 값들을 모아서 한번에 다 하는 것이 아니라 Count 만큼 방출한다.
func test_CollectByCount(){
let publisher = PassthroughSubject<Int,Never>()
publisher.collect(3) //3개씩 나눠서 방출합니다.
.sink(receiveValue: { item in
print(item)
}).store(in: &bag)
publisher.send(10)
publisher.send(10)
publisher.send(10)
publisher.send(10)
publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
}
- CollectByTime -
위 Complete 시점에 방출하는 Collect와 달리 시간에 따라 방출한다.
따라서 .byTime의 N 시간 마다 모아서 방출하게 된다.
** 여기서 자동으로 Connect 시켜주는 Timer Publiser를 사용해줘야한다. 따라서 autoConnect 메소드를 사용
func test_CollectByTime(){
Timer.publish(every: 0.5, on: .main, in: .default)
.autoconnect() //ConnectablePublisher는 Connect 메소드가 필요하다. 그때 autoconnect가 그 역할을 자동으로 해준다.
.map({ _ -> Int in
return Int.random(in: 0...100)
})
.collect(.byTime(DispatchQueue.main, .seconds(1))) //1초마다 방출 한다.
.sink(receiveValue: { item in
print(item)
}).store(in: &bag)
}
- CollectByTimeOrCount -
특정 시간 이전에 Count 갯수가 모이면 방출한다.
그리고 특정 시간에도 방출한다. 그러니 특정 시간에 도달했을 때는 3개 보다 작은 1 ~ 2개가 방출 될 수 있다.
func test_CollectByTimeOrCount(){
Timer.publish(every: 0.5, on: .main, in: .default)
.autoconnect() //ConnectablePublisher는 Connect 메소드가 필요하다. 그때 autoconnect가 그 역할을 자동으로 해준다.
.map({ _ -> Int in
return Int.random(in: 0...100)
})
.collect(.byTimeOrCount(DispatchQueue.main, .seconds(4), 3)) //4초 이전에 3개가 모이면 방출한다.
.sink(receiveValue: { item in
print(item)
}).store(in: &bag)
}
- reduce -
들어 오는 Input 값을 reduce 연산에 맞게 처리하였다가 Complete 시점에 방출
func test_reduce() {
let publisher = PassthroughSubject<Int,Never>()
publisher.reduce(0, {$0 + $1}) //초기값에서 새로운 값을 다 더해서 Complete 시점에 반환
.sink(receiveValue: { item in
print(item)
}).store(in: &bag)
publisher.send(10)
publisher.send(20)
publisher.send(30)
publisher.send(40)
publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
}
func test_tryreduce() {
let publisher = PassthroughSubject<Int,CombineError>()
publisher.tryReduce(0, { total , input in
if input % 2 == 0 {
throw CombineError.fail
}
return total + input
})
.sink(receiveCompletion: { competion in
switch competion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { total in
print(total)
}).store(in: &bag)
publisher.send(10)
publisher.send(20)
publisher.send(30)
publisher.send(40)
publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
}
** Sink를 Cancelable없이 사용하기 **
그런데 어느 순간 저의 호기심을 자극하는 코드를 봤습니다.
Sink와 Subscribe 였습니다.
Rx에서 Subscribe 역할을 하는 것이 Sink 였는데, Combine에서 Subscribe가 따로 있는 겁니다.
Combine의 Subscribe는 Rx의 Bind와 비슷한 역할을 하는 걸로 생각했습니다.
또한 단일 결과만 반납하고 Cancel 되는 기능도 구현이 가능하단 생각을 했습니다.
Subscribe 내부에 Subscriber를 구현하면 Cancleable에 전달할 필요가 없다는 장점이 있습니다.
class SampleSubscriber : Subscriber {
typealias Input = String
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited) //무한으로 받을 수 있도록 설정
}
func receive(_ input: String) -> Subscribers.Demand {
print("Get Data from Subscription", input) //결과값이 여기에 찍힘
return .none
}
func receive(completion: Subscribers.Completion<Never>) {
print("Subscriber End") // Complete 결과가 여기에 나옴
print(completion)
}
}
func test_SingInSubscriber(){
let publisher = PassthroughSubject<Int,Never>()
let publisherOutput = PassthroughSubject<String,Never>()
publisherOutput.sink(receiveValue: {print($0)}).store(in: &bag)
//결과값이 PublisherOutput Subejct에 전달되고 Sink로 결과값이 나옴
publisher
.map({String($0) + " to publisherOutput \n"})
.subscribe(publisherOutput).store(in: &bag) //output Subject에 Bind을 걸어놈 ^
publisher.map({String($0) + " to SampleSubscriber \n"})
.subscribe(SampleSubscriber()) //Custom Subscribe에 결과 값이 전달
publisher.map({String($0) + " to SampleSubscriber \n"})
.subscribe(Subscribers.Sink(receiveCompletion: {print($0)}, receiveValue: {print($0)}))
//Subscriber.Sink 객체를 subscribe 내부에 구현하여 Cancel시 바로 죽을 수 있도록 구현
publisher.send(1)
publisher.send(1)
publisher.send(1)
publisher.send(completion: .finished)
publisher.send(2)
}
//결과
1 to SampleSubscriber
1 to publisherOutput
Get Data from Subscription 1 to SampleSubscriber
1 to SampleSubscriber
1 to publisherOutput
Get Data from Subscription 1 to SampleSubscriber
1 to SampleSubscriber
1 to publisherOutput
Get Data from Subscription 1 to SampleSubscriber
finished
Subscriber End
finished
** Assing(to) vs Assing(to,on) **
Assing은 일단 Sink와 같이 구독 기능을 가지고 있지만 실패 없는 구독 입니다.
to 는 ios 14 이상 부터 지원이 가능한데, Published Property가 죽을 때 자동으로 Deinit 되는 것이고
Assing(to, on)은 AnyCancelable이 죽을 대 Deinit 되는 것 입니다.
@Published var name : String = "" {
didSet{
print(self.name) //Assing(to,on)을 사용할 때만 호출 됨
}
}
var studentName : String = "" {
didSet{
print(studentName)
}
}
func test_Assign(){
let publisher = PassthroughSubject<Int,Never>()
let publisher2 = PassthroughSubject<Int,Never>()
[1,2,3].publisher.map({String($0)})
.assign(to: &self.$name)
publisher.map({String($0)})
.assign(to: &self.$name)
//name이라는 PropertyWrapper 객체인 Published를 전달.
publisher2.map({String($0) + "Assign origin "})
.assign(to: \.name, on: self).store(in: &bag)
//on에 들어가는 Object에서 Property가 name인 것을 찾는 것
publisher2.map({String($0) + "Assign StudentName "})
.assign(to: \.studentName, on: self).store(in: &bag)
//PropertyWrapper 객체가 아닌 일단 Property에서도 가능함
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
publisher.send(4)
print(self.name)
})
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
publisher2.send(5)
})
}
//결과
4
5Assign origin
5Assign origin
** 참고 사이트 **
Sink vs Subscribe
https://stackoverflow.com/questions/61216852/whats-the-difference-between-sink-and-subscribers-sink
Sink vs Assign
https://sujinnaljin.medium.com/combine-sink-assign-3dc04b7b326f
https://developer.apple.com/documentation/combine/fail/assign(to:)
KeyPath
autoConnect
https://zeddios.tistory.com/1009
Combine 공부 리스트
https://icksw.tistory.com/category/iOS/Combine?page=1
Combine Ext Lib
https://github.com/CombineCommunity/CombineExt
'Xcode > Swift - PlayGround' 카테고리의 다른 글
Swift) Async - Await 정리하기 #2 (Async let, withTaskGroup, Task) (0) | 2023.06.12 |
---|---|
PlayGround) PropertyWrapper와 Dependency Injection (0) | 2023.01.23 |
PlayGround) Framework UnitTest 생성 해보기 (0) | 2022.12.17 |
PlayGround) Framework 생성 모듈화 작업 #2 (0) | 2022.12.04 |
PlayGround) Framework를 통해 모듈화 작업하기 (5) | 2022.11.27 |
댓글