티스토리 뷰

Swift

Combine 뽀개기 1장: Publisher

Esiwon 2023. 7. 26. 19:43

Publisher

Declares that a type can transmit a sequence of values over time.
형식이 시간이 지남에 따라 값 시퀀스를 전송할 수 있음을 선언합니다.

Publisher는 하나 이상의 Subscriber 인스턴스에게 Element를 전달한다.

쉬운 말로 값을 방출하는 녀셕!!!

또한 Publisher는 프로토콜이다.

내부엔 Output, Failure, receive(subscriber:)를 필수로 구현해야 한다.
하나씩 보면,

  • Output: Publisher 객체가 내보낼 값의 타입
  • Failure: Error가 발생했을 때 Error 타입(Error를 방출하고 싶지 않으면, Never로 설정)
  • receive(subscriber:): Publisher와 연결될 subscriber를 받는 메서드

근데 receive(subscriber:)와 비슷한 역할을 하는 메서드가 있다.
그건 바로 subscribe(_:) 이 녀석 공식문서를 보면, 애 역시 지정된 subscriber를 이 게시자에 연결하는 역할 공식문서에 의하면 receive(subscriber:) 대신 subscribe(_:) 메서드를 이용하라고 나와있다.

애플은 자주 사용될것 같은 Publisher 객체를 미리 구현해 두었는데 리스트로 보면

  • Just
  • Future
  • Deferred
  • Empty
  • Fail
  • Record
  • AnyPublisher

Just

A publisher that emits an output to each subscriber just once, and then finishes.
Just는 한번의 아웃풋만을 전달하는 publisher객체

Just 가장 심플한 Publisher 객체 Failure는 Never로 Error를 방출하지 않으며, 딱 한번의 값만을 방출한다.

사용 방법은

let publisher = Just("Combine 첫 교시")

let subscriber = publisher.sink { completion in
    switch completion {

    case .finished:
        print("finished")
    }
} receiveValue: { value in
    print(value)
}

//Combine 첫 교시
//finished

위와 같이 매우 간단!! 한번의 아웃풋을 방출하고, finished를 통해 종료 이벤트를 내보낸다.

Future

A publisher that eventually produces a single value and then finishes or fails.
하나의 결과를 비동기로 생성한 뒤 completion event를 보냅니다.

Future 같은 경우 특징은 이니셜라이저에서 값이 방출될때 호출되는 클로져를 받는다.

때문에 클로져 내부에서 Promise라는 클로져를 호출해 success시 값을, failure시 error를 방출한다.

또한 클로져이기 때문에 비동기적으로 로직을 구현할 수 있다.

public typealias Promise = (Result<Output, Failure>) -> Void
enum MyError: Error {
    case error
}

let futurePublisher = Future<String, MyError> { promise in
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        promise(.success("Combine"))
        //에러를 방출해야 할 경우 
        //promise(.failure(.error))
    }
}

let subscriber = futurePublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    case .failure(let error):
        print(error)
    }
} receiveValue: { value in
    print(value)
}

print("비동기적으로 구현 가능")
/*
비동기적으로 구현 가능
Combine
종료
*/

promise() 클로져를 DispatchQueueasyncAfter으로 묶었기 때문에 promise() 클로져가 비동기적으로 호출되어 아래서 호출한 print가 먼저 찍히는것을 볼 수 있다.

Deferred

A publisher that awaits subscription before running the supplied closure to create a publisher for the new subscriber.
새로운 Subscriber의 Publisher를 만들기 위해 제공된 클로저를 실행하기 전에 Subscription을 기다리는 Publisher

Deferred의 뜻은 "지연된" 모가 벌써부터 lazy 키워드와 비슷한 느낌??

Deferred 사실 lazy랑 비슷한 녀석인게 맞다.

그래서 좀 특이한 특징이 Publisher를 방출하는 Publisher라는 것 이 녀석도 생성자가 클로져인데 Publisher를 반환하는 클로져이다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
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
}

위처럼 평소에 output타입이 들어가는 제네릭 자리에도 Publisher 타입이 들어가는 것을 볼 수 있다.

직접 사용해 보면,

print("deferredPublisher 생성")
let deferredPublisher = Deferred<Just<String>>(createPublisher: {
    print("justPublisher 생성")
    return Just("Combine")
})

Thread.sleep(forTimeInterval: 5)
let subscriber = deferredPublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    }
} receiveValue: { value in
    print(value)
}

결과 같이 deferredPublisher가 생성되었고, 실제 값을 방출하는 justPublisher는 실제 사용될 때(5초 뒤)에 생성되는 것을 볼 수 있다. 즉, 구독되었을 때 생성된다.

Empty

A publisher that never publishes any values, and optionally finishes immediately.
아무런 값도 내보내지 않고 즉시 completion 이벤트를 보낼지 선택할 수 있는 Publisher

Empty의 특징은 값을 방출하지 않는 Publisher이다. 오직 completion만 호출 하지만, 생성자에서 completeImmediatelyfalse로 하면 completion 마져 실행하지 않음(기본값은 true)

let emptyPublisher = Empty<Void, Never>(completeImmediately: true)


let subscriber = emptyPublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    case .failure(let error):
        print(error)
    }
} receiveValue: { value in
    print(value)
}

// 종료

Fail

A publisher that immediately terminates with the specified error.
Error를 즉시 보낼 수 있는 Publisher

Fail의 특징은 Error만을 방출하는 Publisher이다. 몬가 Just에 Error 버전???
Fail 또한 Just 만큼 간단하기 때문에 바로 코드로 보쟈

enum MyError: Error {
    case error
}

let failPublisher = Fail<Void, MyError>(error: .error)

let subscriber = failPublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    case .failure(let error):
        print(error)
    }
} receiveValue: { value in
    print(value)
}

//error

Fail는 생성자로 방출할 Error를 받는다 Just가 방출할 값을 받는 것 처럼

Record

A publisher that allows for recording a series of inputs and a completion, for later playback to each subscriber.
subscriber가 나중에 재생할 수 있도록 일련의 입력 및 완료를 기록할 수 있는 publisher

Record의 특징은 subscriber가 받을 intputcompletion 미리 저장해 두었다가 방출한다. 때문에 내부적으로

public struct Recording {
    public typealias Input = Output
    public var output: [Output] { get }
    public var completion: Subscribers.Completion<Failure> { get }
    public init()
    public init(output: [Output], completion: Subscribers.Completion<Failure> = .finished
    public mutating func receive(_ input: Record<Output, Failure>.Recording.Input)
    public mutating func receive(completion: Subscribers.Completion<Failure>)
}

위와 같이 Recording라는 구조체를 가지고 있다.

직접 사용해 보면,

enum MyError: Error {
    case error
}

let recordPublisher = Record<String, MyError>(output: ["Combine", "RxSwift"], completion: .finished)
//let recordPublisher = Record<String, MyError> { record in
//    record.receive("Combine")
//    record.receive("RxSwift")
//    record.receive(completion: .finished)
//}
let subscriber = recordPublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    case .failure(let error):
        print(error)
    }
} receiveValue: { value in
    print(value)
}

/*
Combine
RxSwift
종료
*/

위와 같이 사용할 수 있다.
사용 방법은 비교적 간단...

AnyPublisher

A publisher that performs type erasure by wrapping another publisher.
publisher를 AnyPublisher타입으로 래핑하여 기존 publisher의 타입을 지워주는 publisher

AnyPublisher는 말 그래도 publisherAny이다.
eraseToAnyPublisher() 메서드를 통해 publisher의 타입을 AnyPublisher타입으로 변경할 수 있다. 즉, publisher의 타입을 추상화할 수 있다.

let recordPublisher = Record<String?, MyError>(output: ["Combine",nil, "RxSwift"], completion: .finished)

만약, 위같은 상황에서 nil을 제외한 값만 방출하고 싶은 경우 AnyPublisher를 이용하여 해결할 수 있다.

let anyPublisher = recordPublisher
    .flatMap { value -> AnyPublisher<String, Never> in
        if let value = value {
            return Just(value).eraseToAnyPublisher()
        } else {
            return Empty().eraseToAnyPublisher()
        }
    }.eraseToAnyPublisher()

let subscriber = anyPublisher.sink { completion in
    switch completion {
    case .finished:
        print("종료")
    case .failure(let error):
        print(error)
    }
} receiveValue: { value in
    print(value)
}
/*
Combine
RxSwift
종료
*/

위 같이 flatMap의 반환값을 AnyPublisher로 설정하고, 값이 있을 때는 JustAnyPublisher로 래핑하여 반환하고, nil이면 값을 방출하지 않는 EmptyAnyPublisher로 래핑하여 반환한다.

이로써 AnyPublisher를 이용하여 서로다른 타입의 publisher를 반환할 수 있게 된다.

'Swift' 카테고리의 다른 글

Combine 뽀개기 3장: Subject  (0) 2023.07.26
Combine 뽀개기 2장: Subscriber  (0) 2023.07.26
[Swift] Swift Concurrency와 GCD  (0) 2022.12.14
[Swift] Race Condition과 Thread Safe  (0) 2022.12.12
[Swift] Protocol  (0) 2022.02.23
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함