RxSwift를 공부했으니 이제 Combine에 대해 공부해보려 한다.
공식 문서와 내부 구현 먼저 보자.
protocol Publisher
public protocol Publisher {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
Publisher는 시간에 따라 값의 sequence를 전달할 수 있는 "프로토콜"이다.
하나 혹은 그 이상의 Subscriber에게 elements를 전달할 수 있다.
subscriber는 Input 과 Failure 라는 associated types를 가지고 있는데, 이는 Pubslisher에서 정의된 Output, Failure과 일치해야한다.
말 그대로 Input이란 subscriber가 받는 타입, Output은 publisher가 방출하는 타입, Failure란 Publisher가 방출할 때 나올 수 있는 에러타입이다.
만약 publisher가 에러를 방출하지 않는다면 Never를 사용하면 된다.
receive(subscriber:)
함수는 subscribe request를 받아서 Subscription instance를 반환한다.
receive(_:)
함수는 element하나를 publisher에서 subscriber로 방출한다.
receive(completion:)
함수는 subscriber에게 publishing이 끝났음을 알린다.(normally 혹은 error와 함께)
다양한 operators
를 통해 event-processing chain을 만들 수 있다.
Publishers.map.filter ... 이런거를 말하는 듯 하다.
Publisher를 만드는 것을 커스텀으로 하는 것은 권장하지 않는다. (프로토콜구현과 subscription 때문인 것 같다)
따라서 다음 방법과 같이 만들 수 있다.
다음과 같은 방법으로 Publisher를 간편하게 사용할 수 있다.
Publisher의 동작 방식은 다음과 같다.
Publisher의 값을 Subscriber가 구독하려면
subscribe(_:)
메소드 사용sink
메소드 사용assign(to:on:)
메소드 사용할 수 있다.
let HaviPublisher = [1, 2, 3, 4, 5].publisher
let HaviReceiver = HaviPublisher
.sink(receiveCompletion: {
print("구독 완료 : \($0)")
}, receiveValue: {
print("element : \($0)")
})
/*
element : 1
element : 2
element : 3
element : 4
element : 5
구독 완료 : finished
*/
요로코롬 Publisher를 만들어 준 뒤, sink로 구독하여 value를 받았을 때, completion을 받았을 때 처리를 해주면 된다.
assign(to:, on:)
함수의 경우 어떠한 객체에 keyPath의 형태로 값을 바인딩 시켜준다. 이 과정에서 주의해야할 점은 해당 객체와 강한 참조가 생긴다는 점이다. 따라서 upstream의 Publisher가 complete할 경우 nil로 만들어주거나 아직 알아보지 못한 다른 방법으로 참조를 관리해야한다.
Root를 제네릭하게 받으며, binding되는 object 또한 Root타입이다.
assign함수들은 republish의 개념으로 @Published property wrapper에게 값을 방출한다.
다음은 예제 코드이다.
class SomeObject {
@Published var value = 0
}
let object = SomeObject()
object.$value
.sink {
print($0)
}
(0..<10).publisher
.assign(to: &onject.value)
public struct Just<Output> : Publisher {
public typealias Failure = Never
public let output: Output
public init(_ output: Output)
public func receive<S>(subscriber: S) where Output == S.Input, S : Subscriber, S.Failure == Just<Output>.Failure
}
Just는 struct타입으로 Output타입을 가지고 있고, receive하는 subscriber와 타입이 일치해야한다.
// prints 1
Just(1).sink { number in
print(number)
}
// prints [1,2,3]
Just([1,2,3]).sink { number in
print(number)
}
// Error
Just(1,2,3).sink { number in
print(number)
}
Just는 Element하나만 방출하므로 1,2,3같이 여러개를 넣으면 안된다.
final public class Future<Output, Failure> : Publisher where Failure : Error {
public typealias Promise = (Result<Output, Failure>) -> Void
public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}
Future<Int, Error> { future in
future(.success(200))
}.sink {
print($0)
} receiveValue: {
print("received value is", $0)
}
// value를 방출한 뒤에 finish
// prints
// received value is 200
// finished
enum HaviError: String, Error {
case failed
}
Future<String, Error> { future in
future(.failure(HaviError.failed))
}.sink { completion in
print("completion : ",completion)
} receiveValue: { value in
print("value : ", value)
}
// value를 방출하지 않고 finish with Error
// prints
// completion : failure(__lldb_expr_13.HaviError.failed)
Future<String, Never> { future in
future(.success("Havi Never"))
}.sink {
print($0)
}
// Error가 Never 일 경우 receive value만 해주면 됨
// completion 블럭 없이 사용 가능
// prints
// Havi Never
public struct Deferred<DeferredPublisher> : Publisher where DeferredPublisher : Publisher {
public typealias Output = DeferredPublisher.Output
public typealias Failure = DeferredPublisher.Failure
public let createPublisher: () -> DeferredPublisher
public init(createPublisher: @escaping () -> DeferredPublisher)
public func receive<S>(subscriber: S) where S : Subscriber, DeferredPublisher.Failure == S.Failure, DeferredPublisher.Output == S.Input
}
import Foundation
class HaviPublisher: Publisher {
typealias Output = String
typealias Failure = Never
init() {
NSLog("\(type(of: self)) init")
}
deinit {
print("\(type(of: self)) deinit")
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
DispatchQueue.global(qos: .utility).async {
["Havi1", "Havi2", "Havi3"].forEach {
_ = subscriber.receive($0)
}
subscriber.receive(completion: .finished)
}
}
}
let haviPublisher = HaviPublisher()
print(haviPublisher)
haviPublisher.sink {
print("sink :", $0)
} receiveValue: {
print("receive value : ", $0)
}
let deferedHaviPublisher = Deferred<HaviPublisher> { () -> HaviPublisher in
return HaviPublisher()
}
print("deferred init")
deferedHaviPublisher.sink {
print("sink :", $0)
} receiveValue: {
print("receive value : ", $0)
}
public struct Empty<Output, Failure> : Publisher, Equatable where Failure : Error {
public init(completeImmediately: Bool = true)
public init(completeImmediately: Bool = true, outputType: Output.Type, failureType: Failure.Type)
public let completeImmediately: Bool
public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
public static func == (lhs: Empty<Output, Failure>, rhs: Empty<Output, Failure>) -> Bool
}
Empty<String, Never>()
.sink {
print("sink : ", $0)
} receiveValue: {
print("receive value : ", $0)
}
// value를 내보내지 않고 스트림을 종료한다.
// prints
// sink : finished
public struct Fail<Output, Failure> : Publisher where Failure : Error {
public init(error: Failure)
public init(outputType: Output.Type, failure: Failure)
public let error: Failure
public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}
Fail<String, Error>(error: HaviError.failed)
.sink {
print("sink : ", $0)
} receiveValue: {
print("receive value : ", $0)
}
// 아무런 value를 내보내지 않고 오류를 내보내며 스트림을 종료한다.
// prints
// sink : failure(__lldb_expr_23.HaviError.failed)
let record = Record<String, Never> { record in
print("record start!")
record.receive("havi 1")
record.receive("havi 2")
record.receive("havi 3")
record.receive(completion: .finished)
}
record
.sink {
print("sink :", $0)
} receiveValue: {
print("receive value : ", $0)
}
record
.sink {
print("sink :", $0)
} receiveValue: {
print("receive value : ", $0)
}
// 구독될 때 마다 자기가 기록하고 있던 값을 방출
/*
record start!
receive value : havi 1
receive value : havi 2
receive value : havi 3
sink : finished
receive value : havi 1
receive value : havi 2
receive value : havi 3
sink : finished
*/