[Combine] Publishers - 2

rbw·2023년 12월 13일
0

Combine

목록 보기
4/11

Publishers

https://www.apeth.com/UnderstandingCombine/publishers/publishers.html

이어서 나머지 퍼블리셔에 대해서 알아보겠슴니다

Key-Value Observing Publisher

키-값 관찰은 알림이나 델리게이트 대신 코코아의 일부 영역에서 사용되는 아키텍처 입니다. 이미 KVO를 사용하고 있다면, NSObject.KeyValueObservingPublisher를 대신 사용할 수 있습니다. 이를 위해서 publisher(for:option:)NSObject의 인스턴스로 할당하믄 됨니다.

func publisher<Value>(for: Swift.KeyPath<Self, Value>, 
    options: Foundation.NSKeyValueObservingOptions = [.initial, .new]) 
    -> NSObject.KeyValueObservingPublisher<Self, Value>

위 함수로 게시자를 가져올 때 매개변수는 처음에 사용했던 observe(_:options:changeHandler:)의 앞 부분 2가지와 같습니다. 키패스를 만들 때 클래스 이름은 메시지를 보내는 객체의 클래스에서 알 수 있으므로 생략할 수 있슴니다.

예시 코드로 WKWebvView에서 isLoading을 관찰하여 웹 뷰가 콘텐츠를 로드하는 데 걸리는 시간 동안 액티비티 뷰를 표시하는 코드를 살펴보겠습니다.

self.obs.insert(wv.observe(\.isLoading, options: .new) { 
    [unowned self] wv, ch in
    if let val = ch.newValue {
        if val {
            self.activity.startAnimating()
        } else {
            self.activity.stopAnimating()
        }
    }
}

위 코드를 KVO 퍼블리셔로 리팩토링 한다면 아래와 같습니다.

wv.publisher(for: \.isLoading, options: .new)
    .sink { [unowned self] val in
        if val {
            self.activity.startAnimating()
        } else {
            self.activity.stopAnimating()
        }         
    }.store(in: &self.storage)

하지만 KVO-P는 기존의 KVO 보다 정교하지는 않다고 하네요.

KVO에서는 옵션을 사용하여서 수신하려는 값의 종류를 지정하고, 값을 수신하여 initial, new, old를 판단가능했는데 KVO-P는 요런 식으로는 불가합니다. 옵션에 initial, new를 포함할 수 있지만 이전 값은 수신되지 않으므로 old를 포함하는 것은 무의미합니다.

또 파이프라인으로 수신되는 값은 관찰된 속성이 구독 시점에 가지고 있는 값과 각 변경 후에 가지고 있는 값일 뿐이라, initial 값이 가장 먼저 도착하는 값이라는 점을 제외하고는 이후의 new 값과 구별할 수 있는 방법이 업슴니다

그래도 해결방법이 있다네용 ㅋㅌㅋ 아이디어는 .initial 값이 첫 번째 값이라는 사실을 사용하여 구별하는 방법입니다.

enum KVO<T> {
    case initial(T)
    case new(T)
}
let kvop = self.thingy.publisher(
    for: \.string, options: [.initial, .new])
let initial = kvop.first()
    .map { KVO.initial($0) }
let subsequent = kvop.dropFirst()
    .map { KVO.new($0) }
let realkvop = initial.merge(with: subsequent).eraseToAnyPublisher()

realkvop
    .sink { val in
        print(val)
    }.store(in: &cancellable)

string = "second value"
string = "last value"

위 처럼 작성을 한다면 결과 값은 아래처럼 나올것임니당. 처음에 string에는 값이 들어있어야 initial 에 nil 말고 값이 출력댐니당

initial("first value")
new("second value")
new("last value")

좀 더 정교하게 값을 관찰하려면 .prior 옵션을 사용하여 위 열거형 KVO<T> 케이스에 prior 값과 new 값을 같이 연관값으로 담아주면 값이 방출되는건 이전 값과 새 값의 튜플이 리턴될것임니당

Published

이전에 설명한 KVO는 Objective-C의 기술입니다. Swift에서 키-값 관찰에 해당하는 것은 @Published 프로퍼티 래퍼입니다. 이것 또한 클래스의 프로퍼티에서만 작동합니다. 이 프로퍼티의(Published.Publisher)의 게시자를 얻으려면 프로퍼티 래퍼의 달러 기호($)를 사용함니다.

KVO-P와 마찬가지로 방출되는 값은 구독 시 초기 값과 값의 변경 시에 새로운 값으로 구성되며, 구분하는 것은 사용자가 결정할 수 있습니다.

일반적으로 이 친구를 사용하여 VC의 프로퍼티가 항상 다른 VC의 프로퍼티 값을 반영하도록 할 수도 있습니다. 이는 대리자 패턴의 대안이 될 수 있슴니다. 아래에 예시 코드를 살펴보면서 이해하면 좋을듯함니다.

class vc2: UIViewController {
    @Published var count = 0
    @IBAction func doButton(_sender:Any) {
        self.count += 1
    }
}

vc2를 프레젠트할때 prepare(for:sender:)를 사용하여 vc2의 count 프로퍼티에서 vc의 coutn 프로퍼티로 간단한 파이프라인을 구성합니다

class vc: UIViewController {
    var storage = Set<AnyCancellable>()
    var count : Int = 0
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let vc2 = segue.destination as? vc2 {
            vc2.count = self.count
            vc2.$count.assign(to: \.count, on: self)
                .store(in:&self.storage)
        }
    }
}

이렇게 구성을 하면 vc와 vc2의 count 값은 늘 동일하게 보여집니다.

NOTE: @Published 변수가 변경되면 해당 퍼블리셔는 변수 자체가 새 값으로 설정되기 전에 새 값을 게시하는것을 알아두면 좋슴니다.

Subject

Subject 퍼블리셔는 위의 @Published 퍼블리셔와 유사합니다. 두 퍼블리셔 모두 사용자가 지시할 대마다 단일 값을 내보내는 방식이기 때문입니다. 차이점으로 @Published는 연결된 인스턴스가 설정될 때 단일 값을 내보내는 반면, Subject 퍼블리셔는 사용자가 지시할 때 단일 값을 내보냅니다.

또 중요한 차이점으로는 @Published는 클래스에서 작동하고, Subject는 단순한 퍼블리셔라 어디에서나 작동합니다.

Subject가 값을 내보내도록 하려면 send(_:)라고 지시하면 됩니다.

Subject자체는 프로토콜이며 두 개의 기본 제공 클래스가 있습니다.

  • PassthroughSubject: 이 친구는 전송하라는 명령을 받은 것만 전송하고 그 이상은 전송하지 않습니다.
  • CurrentValueSubject: 이 친구는 처음에 값으로 초기화되며, 해당 값을 유지합니다. 구독될 때와 값이 설정(set)될 때 방출합니다. 값을 직접 설정하거나 전송하도록 지시하여 값을 변경할 수 있으며 두 가지 방법 모두 동일 결과를 가져옴니다.

바로 이전에 살펴본 코드를 Subject 퍼블리셔로 작성해보겠슴니다.

class vc2: UIViewController {
    let countPublisher = CurrentValueSubject<Int, Never>(0)
    @IBAction func doButton(_sender:Any) {
        self.countPublisher.value += 1
    }
}

class vc: UIViewController {
    var storage = Set<AnyCancellable>()
    var count: Int = 0
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let vc2 = segue.destination as? vc2 {
            vc2.countPublisher.value = self.count
            vc2.countPublisher
                .assign(to: \.count, on: self)
                .store(in: &self.storage)
        }
    }
}

하지만 위의 @Published를 사용하는 것과 동일 효과라서 위 예제로는 장점을 알기 힘듭니다. 그래서 장점을 어필하자면 구조체나 열거형에서 Subject 퍼블리셔를 사용가능하며, private 으로 값을 비공개로 유지하면서 사용이 가능합니다.

// vc2
private var count = 0
private let countPublisher = PassthroughSubject<Int, Never>()
func publishCount(startingAt: Int) -> PassthroughSubject<Int, Never> {
    self.count = startingAt
    return self.countPublisher
}
@IBAction func doButton(_ sender: Any) {
    self.count += 1
    self.countPublihser.send(self.count)
}

// vc.prepare
if let vc2 = segue.destination as? ViewController2 {
    let pub = vc2.publishCount(startingAt:self.count)
    pub.assign(to: \.count, on: self)
        .store(in:&self.storage)
}

제 생각으로는 @Published도 private로 비슷하게 사용가능할듯 싶긴합니다. 위 publishCount 의 리턴타입을 Published<Int>.Publisher로 해준다음 $count를 리턴하여서 vc에서 그대로 사용하면 동일한 결과를 얻으면서 private하게 값을 보호할 수 있을듯 함니다. 대신 바뀔때마다 자동으로 방출하기 때문에 선택적으로 값을 넘기거나 하는 부분에서는 조금 유연하지 못할듯합니다.

추가적인 장점으로 Subject에는 다른 퍼블리셔에 구독할 수 있는 기능이 있어서 파이프라인 내에서 오퍼레이터 역할을 할 수 있습니다. 퍼블리셔에게 Subject를 구독하도록 지시하면 해당 Subject가 사라지지 않게 보유해야 하는 AnyCancellable이 발생하기 때문에 조금 까다롭슴니다.

위의 예시 상황을 인위적으로 만들어보겠습니다.

let topSubject = PassthroughSubject<String, Never>()
override func viewDidLoad() {
    super.viewDidLoad()
    let sub1 = PassthroughSubject<String,Never>()

    topSubject.subscribe(sub1)
        .store(in:&self.storage)

    sub1.sink { print("sub1", $0)}
        .store(in:&self.storage)

    let sub2 = PassthroughSubject<String,Never>()

    topSubject.subscribe(sub2)
        .store(in:&self.storage)

    sub1.sink { print("sub2", $0)}
        .store(in:&self.storage)
}
topSubject.send("howdy")
// OUTPUT
// sub1 howdy
// sub2 howdy

크게 까다로워 보이지는 않네여 ㅋㅌㅋ subscribe 하고 객체를 유지하는걸 신경쓰면 될듯함니다.

마지막으로 Subject에는 send(completion:) 메소드도 존재합니다. 즉, 다른 퍼블리셔처럼 동작하게 하여서 .finished, .failure 를 파이프라인 아래로 보낼 수 있습니다.

이런 신호에 맞게 따로 처리를 하도록 파이프라인을 구성할 수 있으니 매우 유용하게 사용할수있겠져 ?

추가로, .failure 를 보낼려면 실패 유형을 Error로 설정해줘야합니다. Subject를 통해 위 신호를 보내면 해당 서브젝트는 취소되며 더 이상 값을 생성할 수 없습니다


Value Publishers

이 용어는 Combine 에서 제공한느 여러 퍼블리셔를 포함해서 나타내기 위해 만들어낸 용어라고 하네요.

여기에 있는 퍼블리셔들은 단순한 값을 게시하는 퍼블리셔, 게시할 값이 퍼블리셔 자체에 포함되어 있는 퍼블리셔들입니다.

  • Just: 이 친구는 값으로 초기화된 퍼블리셔입니다. 값을 방출하고 중지합니다. 예를 들어 Just(10)은 정수형 10을 게시합니다. (그 뒤에 .finished가 붙슴니다) 실패 타입은 Never입니다.

  • Optional: 이 친구도 값으로 초기화된 게시자입니다. 옵셔널로 래핑된 값을 게시하고 중지합니다. 옵셔널이 nil 이면 퍼블리셔는 아무 작업도 수행하지 않슴니다. 예로, Optional.Publihser(Optional(10))는 10을 게시합니다. 마찬가지로 .finished가 따라옵니다. 하지만 Optional.Publisher(Optional<Int>.none).finished 만 보냅니다. 이는 항상 .finished 전에 값을 보내는 Just와 큰 차이입니다. 얘도 마찬가지로 실패유형은 Never임니다.

  • Result: 이 친구는 publisher 프로퍼티를 통해 퍼블리셔를 제공합니다. 또 이 친구는 오류 유형을 포함하므로 실패 유형이 Never말고도 Error 등을 받을 수 있습니다. 결과가 .success인 경우 퍼블리셔는 연관값을 게시합니다. .failure 인 경우 실패메시지와 함께 오류 값을 보냅니다.

  • Sequence: 이 친구는 publisher 프로퍼티를 통해 게시자를 전달합니다. 이 퍼블리셔는 시퀀스의 모든 요소를 게시합니다. ex. [1,2,3].publisher는 1,2,3을 게시합니다. 실패 유형은 Never임니다.

  • Empty: 이 친구는 아무 작업도 수행하지 않는 게시자 입니다. completeImmediately라는 매개변수로 초기화되며, 이 매개변수가 true 라면 .finished를 보내지만 false라면 아무 것도 내보내지 않습니다.

  • Fail: 이 친구는 .failure 완료를 보내는 퍼블리셔입니다. .failure 의 관련 값을 표현하는 에러 파라미터로 초기화됩니다.

위에서 설명한 퍼블리셔는 매우 단순합니다. 주로 사용 되는 부분은 파이프라인을 테스트하는 데 유용하며, 이 퍼블리셔가 시작될 떄 파이프라인을 따라 내려오는 내용을 확인 가능합니다.

파이프라인 중간 에서 유용하게 쓰일 수 있습니다.

이를 이해하려면 .flatMap 연산자에 대해 알아야합니다. 이 친구는 업스트림에서 값을 받고, 퍼블리셔를 반환하여 다운 스트림의 다음 구독자에게 퍼블리싱합니다. 이 친구의 출력은 map이 생성하는 출력과 마찬가지임니다.

따라서 .flatMap 오퍼레이터의 내부에서는 업스트림에서 어떤 값을 가져왔는지에 따라 퍼블리셔를 생성할 수 있습니다. 예를 들어, Just에 의해 게시되는 값이 Just가 처음 하드코딩된 값이 아니여도 됨니다. 업스트림에서 받은 값일 수 있습니다.

이 부분은 아래 코드를 보면 이해가 더 쉬울거 같슴니다.

enum MyError: Error { case tooBig }

(1...10).publisher
    .setFailureType(to: Error.self)
    .flatMap { (i:Int) -> AnyPublisher<Int,Error> in
        if i < 5 {
            return Just(i).setFailureType(to: Error.self)
                .eraseToAnyPublisher()
        } else {
            return Fail<Int,Error>(error: MyError.tooBig)
                .eraseToAnyPublisher()
        }
}.sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
.store(in:&self.storage)

시퀀스 퍼블리셔의 실패 유형은 Never라서 오류를 전달하기위해 실패 타입을 변경하는것도 가능하네용

위 코드는 1~4까지를 Just로 내려보내고, 5부터는 실패로 처리합니다. 그러면 5를 받는 순간에 해당 파이프라인은 종료가되어 6,7,8,9,10은 보지도 않고 종료가 됨니다

그리고 Just, Fail의 리턴 타입을 동일하게 해주기 위해 .eraseToAnyPublisher()도 사용해야겠져 그렇게 하면 AnyPublisher<Int, Error> 타입으로 동일해짐니다..!

마지막으로는 Just의 실패 유형이 Never이므로 한 번 더 setFailureType(to: Error.self)를 하여 Fail과 동일하게 맞춰주면 됩니다.

이는 인위적인 예시이며, 실제로 위와 같은 작업을 수행하는 경우에는 tryMap을 사용해야 합니다. 하지만 위처럼 값 게시자를 사용하여 파이프라인에 주입하고 게시하는 것은 Combine을 사용하면서 알아둬야 함니다

Record

여기 또 다른 Value Publisher인 Recored에 대해서 알아보겠습니다. 이 친구는 시퀀스 퍼블리셔와 비슷하지만, 한 단계 더 나아가서 Record 안에 하드코딩된 값은 시퀀스와 완료라는 점이 다름니다. 코드로 살펴보겠슴니다

enum MyError : Error { case tooBig }
let rec = Record(output: [1,2,3,4], completion: .failure(MyError.tooBig))
rec
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)

// OUTPUT
1 2 3 4
failure(MyError.tooBig)

레코드 퍼블리셔 안에는 레코딩 객체(Record.Recording)가 있습니다. 이 객체는 실제로 값의 순서와 완성이 이루어지는 곳입니다. 객체는 recording 속성을 통해 사용가능하며, 이는 레코드를 생성 후 해당 recording을 추출하여 다른 레코드에서 사용할 수 있음을 의미함니다.

let rec1 = Record(output: [1,2,3,4], completion: .failure(MyError.tooBig))
let recording = rec1.recording
let rec2 = Record(recording: recording)
rec2
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)
// same result

대화형으로 이를 구성할수 있습니다. 실제로 파이프라인을 실행하고 파이프라인의 끝에서 튀어나오는 값과 컴플리션으로 레코딩을 구성할 수 있슴니다. 이렇게 하기위해서는 recordingreceive(_:), receive(completion:)를 호출하면 됨니다. 이 두 메서드는 모두 구독자 메서드임니다. 하지만 recording은 구독자가 아님니다. 그래서 sink` 등으로 수동으로 호출할 수 있습니다. 아래 예시코드임니다.

var recording = Record<Int,Error>.Recording()
pipeline
    .sink(receiveCompletion: { recording.receive(completion:$0) }) { 
        recording.receive($0) 
    }
    .store(in:&self.storage)

이를 이용해서 이전에 살펴본 flatMap 의 반환값들을 recording에 담아보겠슴니다.

// pub은 위이이이에서 살펴본 1,2,3,4,completion 반환하는 퍼블리셔임니당
var recording = Record<Int,Error>.Recording()
pub.sink(receiveCompletion: { recording.receive(completion:$0) }) { 
    recording.receive($0) 
}.store(in:&self.storage)

// 이렇게 저장을 하고 sink로 구독하면 아래와 같슴니다.
Record(recording:recording)
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)
// OUTPUT
// 1,2,3,4
// failure(MyError.tooBig)

또 레코드는 아래 처럼 간단하게 작성도 가능합니다.

// inout 파라미터로 레코딩을 받고있기 때문에 가능함니다.
let rec = Record<Int,Error> { r in
    var recording = Record<Int,Error>.Recording()
    pub
        .sink(receiveCompletion: { recording.receive(completion:$0) }) { 
            recording.receive($0) 
        }.store(in:&self.storage)
    r = recording
}

파이프라인에서 만든 레코드는 파이프라인을 구성하는 비동기 연산을 실제로 실행하지 않고도 해당 파이프라인 또는 앱의 파이프라인 사용을 테스트할 때 유용함을 알 수 있슴니다.

Custom Publishers

마지막으로 살펴볼 퍼블리셔는 이제 저희 입맛대로 만드는 커스텀 퍼블리셔입니다. 맨 처음 퍼블리셔에 대해서 작성을 하면서 UI 컴포넌트에 관한 퍼블리셔를 만든다고 했는데 이번 섹션을 살펴보면서 만들어보겠습니당

먼저 Publisher 프로토콜을 만족해야겠져. 간단함니다 아래 메소드를 구현하면 댐니드

func receive<S>(subscriber: S) 
    where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input

여기서 아이디어는 구독자에 의해 구독되도록 요청받는다는 것입니다.

struct MyCoolPublisher : Publisher {
    typealias Output = String
    typealias Failure = Never
    func receive<S>(subscriber: S) 
        where S:Subscriber, S.Input == Output, S.Failure == Failure {
            // now what?
    }
}

이제 receive(subscriber:) 내부에 뭘 구현해야할까요? 여기서 저희가 해야할 일은 Subscription을 만들고 구독자의 receive(subscription:) 를 호출하는 것임니다.

// receive method
let subscription = // make a Subscription
subscriber.receive(subscription:subscription)

구독을 살펴보기전에, 게시자와 구독이 같이 유지된다면, 게시자와 구독의 의무를 함께 이행해야합니다 구독자가 퍼블리셔에게 무엇을 기대하는지르 살펴보면 의무가 무엇인지 알 수 있슴니다 !

  • 구독자에게 값 전달을 요청하는 메시지를 보낸 후 값이 생성되면 구독자에게 해당 값과 함께 receive(_:) 메서드를 보내야함니다.
  • 실패가 있을 수 있는 경우에 구독자에게 .failure를 포함한 receive(completion:)를 보내야함니다.
  • 생성할 값의 수를 제한하는 경우, 마지막 값이라면 구독자에게 .finished가 포함된 receive(completion:)를 보내야함니다
  • 구독은 Cancellable을 준수해야함니다. 취소 메서드가 전송되면 값 생성을 중지하고 순서대로 해지해야함니다.

이제 구독을 만들어보겠씀니다. 구독 클래스는 일반적으로 중첩 클래스입니다. 또 저희는 값을 전송할 수 있도록 구독자에 대한 참조를 유지해야함니다. 그래서 저희는 다운스트림으로 부르는 Subscriber 인스턴스 변수를 제공함니다

구독 클래스를 중첩클래스로 하지 않는 코드도 많이있슴니다. 중첩 클래스로 작성하면 가독성이 더 좋아 보이긴 함니다. 관리하기가 편해서 ? 나누어도.. 크게 상관은 없지 싶슴니다

struct MyCoolPublisher : Publisher {
    typealias Output = String
    typealias Failure = Never
    func receive<S>(subscriber: S) 
        where S:Subscriber, S.Input == Output, S.Failure == Failure {
            subscriber.receive(subscription:Inner(downstream:subscriber))
    }
    class Inner<S> : Subscription 
        where S:Subscriber, S.Input == Output, S.Failure == Failure {
            var downstream : S?
            init(downstream:S) {
                self.downstream = downstream
            }
            func request(_ demand: Subscribers.Demand) {
                // ???
            }
            func cancel() {
                // ???
            }
    }
}

이제 나머지 부분을 여기서 구현하면 댐니다. 이를 수행하는 방법은 퍼블리셔가 게시하는 내용마다 달라집니다 예를 들어 아래 이름을 순서대로 방출하고 중지하는 경우를 생각해보겠슴니다.

var boys = Array(["Manny", "Moe", "Jack"].reversed()) // so we can use popLast
func request(_ demand: Subscribers.Demand) {
    guard let downstream = self.downstream else { return }
    while let boy = boys.popLast() {
        _ = downstream.receive(boy)
        if boys.isEmpty {
            downstream.receive(completion: .finished)
            self.downstream = nil
            return
        }
    }
}

보시다시피, 완료신호를 내보내면 다운스트림 구독자도 해제됩니다. 취소 구현도 마찬가지로 구현해야합니다.

func cancel() { downstream = nil }

Responding to Backpressure

저희가 위에서 작성한 부분에서는 Bakcpressure에 반응하지 않슴니다. 위에서 구현한 request(_:) 에서는 몇개의 값이 요청되는지 체크도 안하고 있고, 값이 반환되는 수요를 무시하고 있슴니다. 하지만 이런 퍼블리셔도 존재할수는있겠져. 일부 퍼블리셔는 하나의 값만 게시하는 경우도 충분히 많이 있씀니다.

그래도 백프레셔에 대한 대응을 구현하는걸 살펴봐야져. 여기서 까다로운 부분은 값의 수요를 판단하는 부분이 까다로울수 있슴니다. 다행히도 Subscribers.Demand 구조체는 연산자를 많이 지원해주고, Int와도 함께 작동합니다.

이제 저희는 .none 으로 시작하는 limit라는 수요 인스턴스 속성을 유지함니다. 수요가 들어오면 limit에 추가함니다. 구독자에게 값을 보내면 limit에서 1을 뺌니다. .none인경웨는 어떤 값도 제공하지 않슴니다.

var limit = Subscribers.Demand.none
func request(_ demand: Subscribers.Demand) {
    guard let downstream = self.downstream else { return }
    self.limit += demand
    while let boy = boys.popLast(), limit > .none {
        let newdemand = downstream.receive(boy)
        self.limit -= 1 // because we just vended one
        self.limit += newdemand // because more demand just arrived
        if boys.isEmpty {
            downstream.receive(completion: .finished)
            self.downstream = nil
            return
        }
    }
}

A UIControl Publisher

이제 실제로 저희가 활용할 수 있는 퍼블리셔를 만들어보겠슴니다.

ControlPublisher 구조체를 만들어 보겠슴니다. 이 구조체의 역할은 컨트롤이 이벤트를 실행할 때 값을 내보내는 것임니다. 이 퍼블리셔는 어떤 종류의 값을 내보내야 할까요 ? 이벤트가 발생하면 컨트롤이 이벤트 핸들러 함수에 전달하므로, 컨트롤 자체에 대한 참조를 내보내는게 좋겠슴니다.

구독이 컨트롤의 이벤트 대상이 되려면, 컨트롤에 대한 참조를 유지해야 함니다. 게시자 자체도 컨트롤로 초기화하여 구독에 전달할 수 있어야겠졍

struct ControlPublisher : Publisher {
    typealias Output = UIControl
    typealias Failure = Never

    let control : UIControl
    let event: UIControl.Event

    init(control:UIControl, for event: UIControl.Event) { 
        self.control = control 
        self.event = event    
    }

    func receive<S>(subscriber: S) 
        where S : Subscriber, S.Input == Output, S.Failure == Failure {
            subscriber.receive(subscription: 
                Inner(downstream: subscriber, sender: self.control, event: event))
    }

    final class Inner <S:Subscriber>: Subscription 
        where S.Input == Output, S.Failure == Failure {
            private let sender: UIControl
            private let event: UIControl.Event
            var downstream : S?
            init(downstream: S, sender: UIControl, event: UIControl.Event) {
                self.downstream = downstream
                self.sender = sender
                self.event = event
            }
            func request(_ demand: Subscribers.Demand) {
                // ?
            }
            func cancel() {
                // ?
            }
        }
}

이제 빈칸채우기를 해봅시당. 먼저 request(_:) 는 어떤일을 해야 할까요? sink 등으로 구독을 시작하면 요청이 도착하고 얘를 실행하는데, 이게 의미한다는 것은 구독자가 컨트롤의 이벤트에 대해 들을 준비가 되었다는 것을 의미합니다. 그럼 이 이벤트가 일어나는 소식을 듣게 해줘야함니다.

self.sender.addTarget(self,action: #selector(doAction), for: event)

// request에서 아래 함수를 호출하고, 여기서는 구독자에게 값을 전달해야함니다
@objc func doAction(_ sender: UIControl) {
    _ = self.downstream?.receive(sender)
}

이제 cancel에서는 위의 작업을 끊어주는 부분을 하면 됨니다. 아래의 초기화 함수를 따로 만들어서 사용하는게 좋겠슴니다.

private func finish() {
    self.sender?.removeTarget( self, 
        action: #selector(doAction), for: event)
    self.downstream = nil
}

cancel() {
    self.finish()
}

이렇게 구현하면 퍼블리셔는 끗임니다. UIControl을 확장해서 퍼블리셔를 만드는 함수를 추가하고 사용하는 코드는 아래와 같슴니다.

extension UIControl {
    func publisher(for event: UIControl.event) -> ControlPublisher {
        ControlPublisher(control: self, for: event)
    }
}
// vc
self.btn.publisher(for: .touchUpInside)
    .sink { _ in print("btn") }
    .store(in: &self.storage)
profile
hi there 👋

0개의 댓글