Apple에서 공개한 API인 Combine. 이제 Rx에서 Combine으로 많이 넘어간다고 한다. 무엇을 하는 것인지, 왜 좋은지, 바로 써먹기 위한 팁은 무엇이 있는지 알아본다.
class ViewModel {
let text: AnyPublisher<String, Error>
}
class ViewController: UIViewController {
let label = UILabel()
var viewModel = ViewModel()
init() {
self.viewModel.text
.assign(to: \.text, on: label) // label, text에 assign하겠다
}
}
let publisher: AnyPublisher<ValueType, ErrorType>
publisher는 type 2가지를 정의해야 한다. ValueType과 ErrorType이다.
typealias InnerPublisher = AnyPublisher<Int, InnerError>
let outterPublisher = AnyPublisher<InnerPublisher, OutterError>
Swift.Error
를 채택해야 함Never
도 가능let myNotification = Notification.name("MyNotification")
let publisher = NotificationCenter.default.publisher(for: myNotification, object: nil)
특정 코드에서 NotificationCenter.default.post()
호출 시마다, 변수로 선언한 publisher에서 notification이 방출된다.
[1, 2, 3].publisher
["a", "b", "c"].publisher
(1...6).publisher
우리가 기존에 사용하는 자료구조 대부분이 publisher를 제공한다.
Just(3)
Just("abc")
Just를 사용하면 해당 값 하나만 방출하는 publisher가 만들어진다. publisher를 조합할 때, 중간중간에 많이 사용된다.
let publisher = Future<ValueType, ErrorType> { result in
// write code
// API 호출 등의 작업후 성공 여부에 따라..
if success {
result(.success(방출한 값))
} else {
result(.failure(error))
}
}
URLSession.shared.dataTaskPublisher(for: url)
let view = UIView()
let publisher = view.publisher(for: \.bounds)
publisher.sink(
receiveCompletion: { param in // 이전에 배운 publisher의 completion이 넘어오는 경우
switch param {
case .finished:
// error 없이 complete된 상황
case .failure(let error):
// error 방출된 상황
}
},
receiveValue: { value in // 값이 넘어오는 경우
// value 방출된 상황
}
)
class MyClass {
var value: Int
}
let myClass = MyClass()
publisher.assign(to: \.value, on: myClass)
func method() {
let subscriber = publisher.sink(...)
// 메소드 내에 있을 경우 subscriber는 계속 동작한다.
}
// 해당 함수의 동작이 끝난 경우 local variable는 release된다.
class A {
var mySubscriber: AnyCancellable?
func method() {
self.mySubscriber = publisher.sink(...)
// 메소드를 벗어나도 release 되지 않음
}
func otherMethod() {
self.mySubscriber?.cancel() // 특정 상황에서 동작을 멈추고 싶다면 cancel하고 대체함
}
}
class A {
var subscriptions = Set<AnyCancellable>()
func method() {
let subscriber = publisher.sink(...)
subscriver.store(in: &subscriptions)
}
// class instance가 release되면 모두 release된다.
}
class A {
var subscriptions = Set<AnyCancellable>()
func someMethod() {
self.viewModel.aPublisher
.operator()
.sink(...)
.store(in: &subscriptions)
}
}
let subject = PassthroughSubject<Int, Error>
subject.sink(...) // Publisher와 마찬가지로 Subscriber 등록
subject.send(value) // 값 방출 기능 추가
마지막으로 방출했던 값을 수동으로 얻을 수도 있다.
let subject = CurrentValueSubject<Int, Error>(0)
subject.send(값1)
subject.send(값2)
let val = subject.value // 저장된 값을 얻음
Combine은 애초에 변화하는 값들의 sequence를 다루는 방법으로 나온 프레임워크이다. 그렇기 때문에 값처럼 사용될 수 있는 Subject를 남용하면, Combine을 사용하지 않는 코드와 비슷한 형태가 될 수 있다. 그렇기 때문에 남용하지 않는 것이 좋다. 가장 나중에 고려하는 용도로 두면 좋을 것이다.
수학의 operator는 숫차를 조합해서 새로운 숫자를 만드는 것이다. 연이어서 사용이 가능하다.(연산자 우선순위가 같은 경우)
Combine에서 Operator는 Publisher를 조합하여 새로운 Publisher를 만든다라고 이해하면 좋을 듯하다. 수학에서의 Operator와 마찬가지로 연이어서 사용이 가능하다.(chain)
publisher.oprator1().operator2(parameter: anotherPublisher)...
let publisher = intPublisher.map { $0 * 2 }
let strPublisher = pub.map { String($0) }
[10, 20].map { [$0 + 1, $0 + 2]} == [[11, 12], [21, 22]]
let pub1 = [10, 20].publisher
let pub2 = pub1.map{ [$0 + 1, $0 + 2].publisher }
// pub1이 방출하는 값 -> array로 변형후 -> 해당 array를 방출하는 publisher로 변환
Publisher<Publisher<Int, Error>, Error>
[11, 12].publisher
방출 후, [21, 22].publisher
방출publisher.tryMap {
if {
throw myError
}
}
publisher.map {
if {
throw myError // 빌드 에러
}
}
[[1, 2], [3, 4]].flatMap { $0 } == [1, 2, 3, 4]
let pub1: AnyPublisher<AnyPublisher<Int, Error>, Error>
let pub2: pub1.flatMap { $0 } // type: AnyPublisher<Int, Error>
위의 map의 예시와 비교해보자. 만약 map에서의 예시로 든 코드에서 flatMap으로 변경하면 결과가 어떻게 될까?
[10, 20].flatMap { [$0 + 1, $0 + 2]} == [11, 12, 21, 22]
let pub1 = [10, 20].publisher
let pub2 = pub1.flatMap{ [$0 + 1, $0 + 2].publisher }
// pub1이 방출하는 값 -> array로 변형후 -> 해당 array를 방출하는 publisher로 변환
Publisher<Int, Error>
[11, 12, 21, 22]
방출.flatMap { 사용자의 행동으로 값을 방출하는 publisher }
.flatMap { API를 쏘고 응답을 받는 Publisher }
이런 경우 사용하면 result는 API응답 값들을 방출하는 Publisher가 된다.
$0.value
로 생성해준다.$0.value
는 값 자체를 내보내는 것이 아니고, publisher를 만들어주는 행위이다.let pub1: Publisher<Publisher<Int, Error>, Error>
let pub2 = pub1.switchToLastest()
Publisher<Int, Error>
이다.flatMap
과 동일하다.[1, 2, 2, 3, 3, 2, 2, 4].publisher.removeDuplicates() == [1, 2, 3, 2, 4].publisher
let result = pub1.merge(pub2, pub3)