안녕하세요. 후르륵짭짭이 입니다!
한달만에 다시 글을 작성하네요 ㅋㅋㅋ
이번에 작성할 내용은 Swift Combine 입니다.
기존에 비동기 라이브러리를 Rx를 사용해왔어요. 하지만 최근 프로젝트는 Rx가 아닌 Combine으로 작업하고 있습니다.
두개 라이브러리의 장단점이 있는데, 앞으로 Combine으로 작성하는게 좋을 것 같단 생각이 들어요.
** Combine의 구조 **
일단 콤바인도 Rx와 동일하게 비동기 작업을 처리하기 위한 라이브러리 입니다.
이름만 다를 뿐 사용 방식은 어느정도 비슷하더라구요!
일단 콤바인도 Rx와 같이 Subject, Observer, Subscribe 가 있습니다.
이를 콤바인도 동일하게 매칭이 가능합니다.
Subject, Publisher, Subscribe, Subscription 이렇게 총 4가지가 있습니다.
Combine | RxSwift | 내용 |
Observable | Publisher | 이벤트 생성 주체 |
Subject | Subject | 이벤트 생성 및 전달 모두 가능한 주체 |
Subscribe | Subscribe | 이벤트 최종 결과 수신 주체 |
X | Subscription | Subscribe에서 전달한 내용을 처리하는 부분 |
(위의 내용은 제가 경험 한 것을 주관적인 생각으로 정리한것 입니다.)
이 외에도 Operator도 존재하지만 이는 부수적인 기능이라 판단하고 위의 표에서는 제외 했습니다.
** Custom Publisher, Subscribe, Subscription을 통해 콤바인 이해해보기 **
일단 Publisher, Subscribe, Subscription이 무엇인지 대한 내용은 아리 핑구님이 잘 설명하고 계십니다.
https://icksw.tistory.com/category/iOS/Combine
가장 중요한 그림을 보도록 하겠습니다.
아래와 같은 구조로 콤바인은 하나의 Stream을 생성 합니다.
class SamplePublisher : Publisher {
typealias Output = String
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, String == S.Input {
let subscription = SampleSubscription<S>.init(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
========================
class SampleSubscription <Target : Subscriber> : Subscription where Target.Input == String {
var target : Target
init(subscriber : Target){
target = subscriber
}
func request(_ demand: Subscribers.Demand) {
print("Subscription Created and send Value to Subscriber")
let _ = target.receive("Hello Subsriber")
self.cancel()
}
func cancel() {
print("Subscription End")
target.receive(completion: .finished)
}
}
========================
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")
print(completion)
}
}
========================
func test_sampleCustom(){
let publisher = SamplePublisher()
let subscriber = SampleSubscriber()
publisher.receive(subscriber: subscriber)
}
//결과
Subscription Created and send Value to Subscriber
Get Data from Subscription Hello Subsriber
Subscription End
Subscriber End
finished
위에 코드를 순서대로 봅시다.
Publisher -> Subscription -> Subscriber -> 결과
위와 같이 되어 있습니다.
Publisher 객체가 생성되고 recevie 메소드를 통해 Subscriber를 등록 해줍니다.
그 다음 Subscriber는 자신를 관리 해줄 Subscription을 등록해줍니다.
요약하자면
Publisher는 Stream의 생성자
Subscriber는 결과를 보여주는자
Subscription을 Subscriber에게 결과를 보내주는자
이렇게 정리할 수 있습니다.
** 지속적으로 살아있는 Custom Publisher 생성하기 **
이것은 참고로 봐주면 될 것 같습니다.
위에 같은 Custom Publisher는 결과를 한번만 반환하게 되어있는 구조 입니다.
즉, 한번만 사용하기 위해 Publisher, Subscription, Subscribe 모두 생성해야하죠.
굉장히 비효율적 입니다.
-- Publisher --
class CustomPublisher : Publisher {
typealias Output = String
typealias Failure = CombineError
var upload : (@escaping(Output) -> ()) -> () //Closure 방식
var subscription : NotifySub? //Protocol 방식
init(load : @escaping (@escaping(Output) -> ()) -> () ){
self.upload = load
}
func receive<S>(subscriber: S) where S : Subscriber, ViewModel.CombineError == S.Failure, String == S.Input {
let subscription = CustomSubscription<S>()
subscription.target = subscriber //Subscriber를 Subscription에 등록
subscription.upload = self.upload //Closure 등록
self.subscription = subscription
subscriber.receive(subscription: subscription)
}
func callNotify(name : String) {
self.subscription?.call(name: name) //Protocol 호출
}
}
-- Subscription --
class CustomSubscription <Target : Subscriber> : Subscription , NotifySub where Target.Input == String{
var target : Target?
var upload : ( (@escaping(String) -> ()) -> () )?
func request(_ demand: Subscribers.Demand) {
upload?({ input in
let _ = self.target?.receive(input) // Subscriber에서 결과 보내기
})
}
func call(name : String) {
guard let target = self.target else { return }
let _ = target.receive(name) // Subscriber에서 결과 보내기
}
func cancel() {
}
}
-- Caller --
func test_CustomPublisher(){
let publisher = CustomPublisher(load: { closure in
closure("Hello world")
DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: {
closure("Hello world")
})
})
let a = publisher.sink(receiveCompletion: { completResult in
print(completResult)
}, receiveValue: { resultValue in
print(resultValue)
})
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
publisher.callNotify(name: "HAHA")
})
a.store(in: &bag)
}
위의 내용을 보면 두가지 방법으로 지속적인 결과를 제공할 수 있는 Publisher를 생성하였습니다.
1) Closure 방식입니다. (권장 X) -> 이중 Closure에 대한 이해
저는 이중 Closure라고 부르는데, 이 방식으로 지속적인 결과를 제공하는 Publisher를 생성하고 있습니다.
C++에서 이중 포인터가 이해하기 어려웠듯 Swift에서는 N중 Closure가 이해하기 어렵습니다 ㅎㅎㅎ
(@escaping(Output) -> ()) -> ()
을 보면 이중 Closure입니다. 그리고 위의 클로저를 분석하면
(Function) -> ()
으로 구성이 되어 있는것을 알 수 있고 Function을 호출하면 Function 내부의 로직을 수행하겠죠?
쉽게 이해하면 F(g(x)) 구조 입니다.
그렇다면 어떻게 된 걸까요??
- 1 -
CustomPublisher(load: { closure in
closure("Hello world")
})
- 2 -
var upload : ( (@escaping(String) -> ()) -> () )?
func request(_ demand: Subscribers.Demand) {
upload?({ input in
let _ = self.target?.receive(input)
})
}
upload가 F(x)이고 (@escaping(String) -> () 가 g(x)라고 한다면
- 2 - 의 upload 내부에서 g(x) 형태를 정의 해주고 있습니다.
그러면 - 1 - 에서 closure는 g(x)가 되겠죠?
2) Protocol 방식
Closure 방식은 사실 협업하는 관점에서 코드가 깔끔하지 않습니다.
하지면 로직을 잘 이해하고 있다면 코드가 더욱 간결할 수 있습니다.
근데 전 코드에서 가장 중요한것을 쉬운 이해력이라 생각하기 때문에 비추합니다.
protocol NotifySub {
func call(name : String)
}
이렇게 Protocol을 선언 해줍니다.
class CustomSubscription <Target : Subscriber> : Subscription , NotifySub where Target.Input == String{
,,,,
func call(name : String) {
guard let target = self.target else { return }
let _ = target.receive(name)
}
,,,
}
그리고 Subscription에 동일한 Protocol을 준수하게 하고
class CustomPublisher : Publisher {
,,,,
var subscription : NotifySub?
,,,
func callNotify(name : String) {
self.subscription?.call(name: name)
}
}
이렇게 Publisher에서 subscription의 Protocol의 function을 호출하게 해주는 방법도 있습니다.
지금까지 콤바인의 기본적인 작동 원리를 정리해봤습니다.
사실 RxSwift랑 큰 차이를 느끼지 않았지만,
장단점이 분명히 존재했습니다.
나중에는 콤바인에서 제가 주로 사용하는 디자인 패턴의 방식을 정리해보도록 하겠습니다.
** 참고 사이트 **
https://brunch.co.kr/@tilltue/79
콤바인 기타 :
https://cocoacasts.com/combine-essentials-changing-a-publisher-failure-type-with-setfailuretype
https://stackoverflow.com/questions/59018880/swift-combine-how-to-specify-the-error-type-of-trymap
https://trycombine.com/posts/subscribe-on-receive-on/
https://stackoverflow.com/questions/58227096/set-a-given-publishers-failure-type-to-never-in-combine
Futer가 Calling이 되지 않는 이유 :
다른 Publisher 형태로 변경하는 방법
https://stackoverflow.com/questions/60699070/mapping-swift-combine-future-to-another-future
콤바인에서 Rx의 Bind와 동일한 동작 하는 방법 :
https://stackoverflow.com/questions/71021923/combine-equivalent-of-rxswifts-bindto
'Xcode > Swift - PlayGround' 카테고리의 다른 글
PlayGround) Framework 생성 모듈화 작업 #2 (0) | 2022.12.04 |
---|---|
PlayGround) Framework를 통해 모듈화 작업하기 (5) | 2022.11.27 |
PlayGround) Objective-C (상속, 카테고리, 프로토콜, 구조체) 정리 (1) | 2022.09.26 |
PlayGround) Async - Await 경험 정리#1 (0) | 2022.08.13 |
PlayGround) RxTest에서 Timer들어간 Observable 테스트 (0) | 2022.04.24 |
댓글