Introducing Combine 정리

SeongHo Hong
7 min readJun 25, 2019

WWDC 2019

Cocoa SDK에는 다양한 비동기API가 있습니다. 이런 인터페이스들은 각각 중요합니다. 하지만 이것을 조합해서 사용할 때 어려움을 겪게 됩니다. Combine을 사용하면 이런 비동기 조합을 간편하게 할 수 있습니다.

Combine Features

Combine은 swift로, swift를 위해 만들었습니다. Generic같은 Swift feature을 사용할 수 있다는 뜻입니다.

  • Generic: boilerplate 코드를 줄여줌.
  • Type safe
  • Composition first: 핵심 컨셉은 심플. 조합해서 사용할 때 효과적.
  • Request driven: 앱의 메모리 사용, 퍼포먼스를 관리하기 좋음.

Key Concepts

  • Publisher
  • Subscriber
  • Operators

Publisher

  • 선언적인 부분(declarative part).
  • 단지 ‘설명’(description)의 역할을 하기 때문에, value type.
  • struct로 되어있음.
protocol Publisher {
associatedtype Output
associatedtype Failure: Error

func subscribe<S: Subscriber>(_ subscriber: S)
where S.Input == Output, S.Failure == Failure
}

Error가 발생할 일이 없다면, Never라는 타입을 지정해주면 됩니다. 다음은 Publisher의 한 가지 예입니다.

extension NotificationCenter {
struct Publisher: Combine.Publisher {
typealias Output = Notification
typealias Failure = Never
init(center: NotificationCenter, name: Notification.Name, object: Any? = nil)
}
}

기존의 API와 비슷한 init을 가지도록 만들었습니다.

Subscriber

  • Subscriber는 Publisher의 Output을 구독함.
  • Publisher가 끝나는 경우 completion을 받음.
  • Subscriber는 전달받은 value을 기반으로 상태를 변경함.
  • Subscriber는 class로 되어있음.
protocol Subscriber {
associatedtype Input
associatedtype Failure: Error

func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}

아래는 Subscriber의 한 가지 예입니다. 클래스로 되어있고, Failure가 발생하지 않아서 Never로 타입을 지정했습니다.

extension Subscribers {
class Assign<Root, Input>: Subscriber, Cancellable {
typealias Failure = Never
init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>)
}
}

Publisher와 Subscriber는 이런 패턴을 가집니다.

  • Subscriber가 Publisher를 구독한다.
  • Subscriber는 value를 하나도 못받을 수도 있고,
  • 여러번 value를 받을 수도 있다.
  • 마지막에 completion을 받는다.

Operator

  • Operator는 Publisher이다.
  • 선언적이다. 그래서 value type.
  • 값을 변경하기 위해 존재함.
  • upstream을 구독하고, 결과값을 downstream으로 전달함.
// Using Operatorslet graduationPublisher =
NotificationCenter.Publisher(center: .default, name: .graduated, object: merlin)
let gradeSubscriber = Subscribers.Assign(object: merlin, keyPath: \.grade)let converter = Publishers.Map(upstream: graduationPublisher) { note in
return note.userInfo?["NewGrade"] as? Int ?? 0
}
converter.subscribe(gradeSubscriber)

Publisher.Output과 Subscriber.Input이 같아야하기 때문에, 위의 예제에서 가운데에 Map을 넣어줬습니다. (실제 syntax와 다름. 원리를 보여주기 위한 것으로 보입니다.)

실제로 사용할 때는 아래와 같이 사용됩니다.

// Chained Publishers
NotificationCenter.default.publisher(for: .graduated, object: merlin)
.map { note in
return note.userInfo?["NewGrade"] as? Int ?? 0
}
.assign(to: \.grade, on: merlin)

Declarative Operator API

  • Functional transformations
  • List operations
  • Error handling
  • Thread or queue movement
  • Scheduling and time

Composition!

Combine은 작은 일을 하는 operator들을 많이 제공하고 있습니다. 이런 operator들을 조합해서 사용하는 것을 권장합니다.

NotificationCenter.default.publisher(for: .graduated, object: merlin)
.compactMap { note in
return note.userInfo?["NewGrade"] as? Int
}
.filter { $0 >= 5 }
.prefix(3)
.assign(to: \.grade, on: merlin)

Combining Publishers

영상에서 Zip, CombineLatest를 소개했는데 Rx에서 사용하는 개념과 동일해서 Rx의 자료를 가져왔습니다. (https://rxmarbles.com)

정리

RxSwift를 처음 사용할 때 바로 completion이 호출되는 경우에도 무리하게 Rx를 사용했던 경험이 있습니다. 무작정 사용하기 보다 정말 필요를 느낄 때 사용하면 좋을 것 같습니다. 여러 비동기 API 호출들을 제어 하거나, 스케줄링, 스레드 제어를 할 필요가 있을 때 사용하면 유용할 것 같습니다.

--

--

SeongHo Hong

Software Engineer 🧑‍💻https://github.com/cozzin