Combine) Publisher

Havi·2021년 4월 20일
1

Combine

목록 보기
2/8

RxSwift를 공부했으니 이제 Combine에 대해 공부해보려 한다.

공식 문서와 내부 구현 먼저 보자.

Publisher

Publisher 공식 문서

Declaration

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는 InputFailure 라는 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 ... 이런거를 말하는 듯 하다.

Making Publisher

Publisher를 만드는 것을 커스텀으로 하는 것은 권장하지 않는다. (프로토콜구현과 subscription 때문인 것 같다)

따라서 다음 방법과 같이 만들 수 있다.

  • Subject타입인 PassthroughSubjectCurrentValueSubject를 활용한다.
  • 클래스 프로퍼티의 경우 @Published 프로퍼티 래퍼를 적용하면 값이 변화할 때 Subscribers에게 값을 emit한다.

다음과 같은 방법으로 Publisher를 간편하게 사용할 수 있다.

  1. Just : 하나의 값을 즉시(동기적) 한번만(just once) 발생시키고자 할 때 사용
  2. Future : 하나의 값을 비동기적으로 발생시키고자 할 때 사용
  3. Deferred : 새로운 퍼블리셔를 만드는 클로져를 실행시키기 전에 구독을 기다리고 있다가, 새로운 subscription이 발생하면 실핼
  4. Empty : 아무런 값을 발생시키지 않고, 종료시키거나 종료시키지 않을 수 있음
  5. Fail : 에러를 발생시키고 종료시킴
  6. Record : 구독할 때마다 이전에 발행했던 값을 다시 내보냄

Using Publisher

Publisher의 동작 방식은 다음과 같다.

Publisher의 값을 Subscriber가 구독하려면

  1. subscribe(_:) 메소드 사용
  2. sink 메소드 사용
  3. assign(to:on:) 메소드 사용

할 수 있다.

Sink

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

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)

Just

구현체

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같이 여러개를 넣으면 안된다.

Future

구현체

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

Deferred

구현체

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)
}

Empty

구현체

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

Fail

구현체

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)

Record

예제

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
*/

참고
라이노님 블로그
제드님 블로그
브런치

profile
iOS Developer

0개의 댓글