Combine의 Publisher와 Subscriber

  • Combine의 Publisher와 Subscriber에 대해 알아보겠다.
  • Xcode의 Playground에서 실습을 진행하려고 한다.

실습 준비

  • Playground에서의 실습을 보다 편하게 진행하기 위해
  • 다음과 같은 코드를 Playground의 Sources 폴더 안에 임의의 파일에 작성해두자.
import Foundation

public func example(of description: String, action: () -> Void) {
    print("\n——— Example of:", description, "———")
    action()
}

Publisher

  • Playground 최상단에 Foundation과 Combine을 import 한다.
import Foundation
import Combine
  • 우선은 Publisher와 비슷한 Notification을 통해 코드를 구현해보자.
example(of: "Publisher") {
    let myNotification =  Notification.Name("MyNotification")
    let center = NotificationCenter.default
    
    let observer = center.addObserver(forName: myNotification, object: nil, queue: nil) { (notification) in
        print("Notification received!")
    }
    
    center.post(name: myNotification, object: nil)
    center.removeObserver(observer)
}
  • 이것이 Publisher로부터 온 출력 값이라고 할 수는 없지만
  • 실행해보면 다음과 같이 출력된다.
  • 실제로 Publisher로부터 값을 받으려면 Subscriber가 필요하다.
——— Example of: Publisher ———
Notification received!

Subscriber

  • NotificationCenter의 publisher를 sink로 subscribe하는 것을 구현해보자.
example(of: "Subscriber") {
    let myNotification =  Notification.Name("MyNotification")
    let publisher = NotificationCenter.default.publisher(for: myNotification, object: nil)
    let center = NotificationCenter.default
    
    let subscription = publisher.sink { _ in
        print("Notification received from a publisher!")
    }
    
    center.post(name: myNotification, object: nil)
    subscription.cancel()
}
  • 실행해보면 다음과 같이 출력된다.
——— Example of: Subscriber ———
Notification received from a publisher!
  • 이제 Just라는 새로운 예시를 보면, Just를 통해 새로운 Publisher를 생성한다.
  • sink를 통해 publisher를 subscribe 해서 subscription을 생성한다.
  • 그리고 event를 전달 받을 때마다 메세지를 print한다.
example(of: "Just") {
    let just = Just("Hello world!")
    _ = just
        .sink(
            receiveCompletion: {
                print("Received completion", $0)
            },
            receiveValue: {
                print("Received value", $0)
            }
        )    
}
  • 실행해보면 다음과 같이 출력 된다.
——— Example of: Just ———
Received value Hello world!
Received completion finished
  • 위 코드에다 추가적인 subscriber를 통해 한번 더 subscribe 해보자.
_ = just
  .sink(
      receiveCompletion: {
          print("Received completion (another)", $0)
      },
      receiveValue: {
          print("Received value (another)", $0)
      }
  )
  • 한번 더 subscription 과정이 이뤄지고, 다음과 같이 출력된다.
——— Example of: Just ———
Received value Hello world!
Received completion finished
Received value (another) Hello world!
Received completion (another) finished
  • Combine의 또 다른 built-in subscriber인 assign도 살짝 알아보자.
  • assign(to:on:)을 다음과 같이 구현해보자.
  • didSet이 있는 프로퍼티를 갖고 있는 class를 정의하고 instance를 생성한다.
  • string array로부터 publisher를 생성한다.
  • publisher를 subscribe 한다.
  • 생성한 object의 property에 전달 받은 값을 할당한다.
example(of: "assign(to:on:)") {
    class SomeObject {
        var value: String = "" {
            didSet {
                print(value)
            }
        }
    }
    
    let object = SomeObject()
    let publisher = ["Hello", "world!"].publisher

    _ = publisher
        .assign(to: \.value, on: object)
}
  • 실행해보면 다음과 같이 출력 된다.
——— Example of: assign(to:on:) ——
Hello
world!

Cancellable

  • Subscriber가 Publisher로부터 더 이상 값을 전달 받고 싶지 않을 때,
  • resource를 낭비하지 않기 위해 Subscription을 cancel 하는 것이 좋다.
  • Subscriber는 AnyCancellable instance를 리턴하고,
  • AnyCancellable은 Cancellable 프로토콜을 따라서, cancel()을 사용하면 된다.
subscription.cancel()

커스텀 Subscriber

  • 다음과 같이 커스텀 IntSubscriber를 만들어보자.
  • 우선, 1부터 6까지의 범위 publisher를 만든다.
  • 커스텀 IntSubscriber를 선언하고, Input과 Failure를 명시한다.
  • 3가지 종류의 필수 메소드, receive를 구현한다.
  • publisher에게 불리는 receive(subscription:)를 .request로 구현한다.
  • 각 값을 print 하고, .max(0)과 동일한 .none을 return 한다.
  • completion event를 print 한다.
example(of: "Custom Subscriber") {
    let publisher = (1...6).publisher
    
    final class IntSubscriber: Subscriber {
        typealias Input = Int
        typealias Failure = Never

        func receive(subscription: Subscription) {
            subscription.request(.max(3))
        }
        
        func receive(_ input: Int) -> Subscribers.Demand {
            print("Received value", input)
            return .none
        }
        
        func receive(completion: Subscribers.Completion<Never>) {
            print("Received completion", completion)
        }
    }

    let subscriber = IntSubscriber()
    publisher.subscribe(subscriber)
}
  • 실행해보면 다음과 같이 출력 된다.
——— Example of: Custom Subscriber ———
Received value 1
Received value 2
Received value 3

Future

  • Just와 비슷하게 Future은 비동기 방식으로 하나의 결과를 emit하고 종료한다.
  • Future를 아래 코드와 같이 사용해보자.
  • Future<Int, Never>안의 코드는 promise를 생성하고,
  • 일정 시간의 delay 후 integer를 증가시킨다.
  • Future은 결국 하나의 값을 emit하고 종료되거나, fail 하는 publisher이다.
  • promise는 typealias로 (Result<Output, Failure>) -> Void 이렇게 생겼다.
  • 그 밑의 코드는 integer 1을 3초의 delay 후 증가시키고,
  • sink subscriber를 통해 value와 completion을 받아오는 것이다.
  • store에 대해서는 나중에 알아 볼 예정이다.
example(of: "Future") {
    func futureIncrement(integer: Int, afterDelay delay: TimeInterval) -> Future<Int, Never> {
        Future<Int, Never> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
                promise(.success(integer + 1))
            }
        }
    }

    let future = futureIncrement(integer: 1, afterDelay: 3)

    future
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)
}
  • 실행해보면 다음과 같이 출력 된다.
——— Example of: Future ———
2
finished