https://www.apeth.com/UnderstandingCombine/publishers/publishers.html
이어서 나머지 퍼블리셔에 대해서 알아보겠슴니다
키-값 관찰은 알림이나 델리게이트 대신 코코아의 일부 영역에서 사용되는 아키텍처 입니다. 이미 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 값을 같이 연관값으로 담아주면 값이 방출되는건 이전 값과 새 값의 튜플이 리턴될것임니당
이전에 설명한 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
퍼블리셔는 위의 @Published
퍼블리셔와 유사합니다. 두 퍼블리셔 모두 사용자가 지시할 대마다 단일 값을 내보내는 방식이기 때문입니다. 차이점으로 @Published
는 연결된 인스턴스가 설정될 때 단일 값을 내보내는 반면, Subject
퍼블리셔는 사용자가 지시할 때 단일 값을 내보냅니다.
또 중요한 차이점으로는 @Published
는 클래스에서 작동하고, Subject
는 단순한 퍼블리셔라 어디에서나 작동합니다.
Subject
가 값을 내보내도록 하려면 send(_:)
라고 지시하면 됩니다.
Subject
자체는 프로토콜이며 두 개의 기본 제공 클래스가 있습니다.
바로 이전에 살펴본 코드를 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
를 통해 위 신호를 보내면 해당 서브젝트는 취소되며 더 이상 값을 생성할 수 없습니다
이 용어는 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을 사용하면서 알아둬야 함니다
여기 또 다른 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
대화형으로 이를 구성할수 있습니다. 실제로 파이프라인을 실행하고 파이프라인의 끝에서 튀어나오는 값과 컴플리션으로 레코딩을 구성할 수 있슴니다. 이렇게 하기위해서는 recording
의 receive(_:),
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
}
파이프라인에서 만든 레코드는 파이프라인을 구성하는 비동기 연산을 실제로 실행하지 않고도 해당 파이프라인 또는 앱의 파이프라인 사용을 테스트할 때 유용함을 알 수 있슴니다.
마지막으로 살펴볼 퍼블리셔는 이제 저희 입맛대로 만드는 커스텀 퍼블리셔입니다. 맨 처음 퍼블리셔에 대해서 작성을 하면서 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 }
저희가 위에서 작성한 부분에서는 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
}
}
}
이제 실제로 저희가 활용할 수 있는 퍼블리셔를 만들어보겠슴니다.
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)