본문 바로가기
Xcode/Swift - PlayGround

PlayGround) Swift Combine 적응기 #1 (Custom Publisher)

by 후르륵짭짭 2022. 11. 5.
728x90
반응형

 

한옥입니다. 어딘지는 기억이,,,

안녕하세요. 후르륵짭짭이 입니다!

한달만에 다시 글을 작성하네요 ㅋㅋㅋ

이번에 작성할 내용은 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

 

'iOS/Combine' 카테고리의 글 목록

iOS 개발자가 되기 위해 iOS 공부, Swift 공부, CS 공부를 하고 있습니다.

icksw.tistory.com

 

가장 중요한 그림을 보도록 하겠습니다. 

아래와 같은 구조로 콤바인은 하나의 Stream을 생성 합니다.

https://betterprogramming.pub/how-to-create-custom-publishers-in-combine-if-you-really-need-them-5bfab31b4ade

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

 

Custom Publisher 만들기

Swift Combine | * 이글은 Swift 5 기준으로 작성했다. 비동기 작업을 Combine Puslisher 로 만들고 싶다면 어떻게 만들어야 할까? 아주 좋은 예제인 Alamofire 의 Publisher 를 살펴보자 https://github.com/Alamofire/Alamof

brunch.co.kr

https://betterprogramming.pub/how-to-create-custom-publishers-in-combine-if-you-really-need-them-5bfab31b4ade

 

How to Create Custom Publishers in Combine

If you really need them

betterprogramming.pub

 

콤바인 기타 : 

https://cocoacasts.com/combine-essentials-changing-a-publisher-failure-type-with-setfailuretype

 

Changing a Publisher's Failure Type with SetFailureType

Every Combine publisher defines two associated types, the Output type defines the type of elements the publisher can emit and the Failure type defines the type of errors the publisher can emit. The fact that a publisher is required to define an Output type

cocoacasts.com

https://stackoverflow.com/questions/59018880/swift-combine-how-to-specify-the-error-type-of-trymap

 

Swift Combine: How to specify the Error type of tryMap(_:)?

In the Combine framework, we can throw a generic Error protocol type while using tryMap. However, how can we be more specific about the Error type? For example, let publisher = urlSession.

stackoverflow.com

https://trycombine.com/posts/subscribe-on-receive-on/

 

subscribe(on:) vs receive(on:)

The difference between subscribe(on:) and receive(on:) in Combine and when to use which operator

trycombine.com

https://stackoverflow.com/questions/58227096/set-a-given-publishers-failure-type-to-never-in-combine

 

Set a given Publishers Failure type to Never in Combine

Is there a way to transform a given AnyPublisher<AnyType, SomeError> to AnyPublisher<AnyType, Never>?

stackoverflow.com

https://stackoverflow.com/questions/56782078/swift-combine-how-to-create-a-single-publisher-from-a-list-of-publishers

 

Swift Combine: How to create a single publisher from a list of publishers?

Using Apple's new Combine framework I want to make multiple requests from each element in a list. Then I want a single result from a reduction of all the the responses. Basically I want to go from ...

stackoverflow.com

Futer가 Calling이 되지 않는 이유 : 

https://stackoverflow.com/questions/62264708/execute-combine-future-in-background-thread-is-not-working

 

Execute Combine Future in background thread is not working

If you run this on a Playground: import Combine import Foundation struct User { let name: String } var didAlreadyImportUsers = false var importUsers: Future<Bool, Never> { Future {

stackoverflow.com

 

다른 Publisher 형태로 변경하는 방법 

https://stackoverflow.com/questions/60699070/mapping-swift-combine-future-to-another-future

 

Mapping Swift Combine Future to another Future

I have a method that returns a Future: func getItem(id: String) -> Future<MediaItem, Error> { return Future { promise in // alamofire async operation } } I want to use it in anot...

stackoverflow.com

 

콤바인에서 Rx의 Bind와 동일한 동작 하는 방법 :

https://stackoverflow.com/questions/71021923/combine-equivalent-of-rxswifts-bindto

 

Combine equivalent of RxSwift's bind(to:)

I'm an experienced RxSwift user, and had a good working MVVM structure in RxSwift. I'm new to Combine, but I can't for the love of God figure out how to do something similar in Combine. The biggest

stackoverflow.com

 

728x90
반응형

댓글