다시 한번 Cominbe 기초

앞서 Combine에 대해서 소개해 드렸지만, 가볍게 다시 한번 알아보겠습니다. 앞선 예제는 매우 간단한 것을 소개했습니다.

[Publisher] -----> Stream of data(Subscription) -----> [Subscriber]
  1. Publisher는 특정 값(value)을 가지는 이벤트를 방출합니다
  2. Subscriber는 전달받은 값을 기반으로 상태를 변경할 수 있습니다.
  3. Publisher나 Subscriber는 Subscription으로 연결됩니다.
// Publisher.subscribe(subscriber)
[Publisher] <---------- 1. subscribes ---------- [Subscriber]
// receive(subscription:)
[Publisher] ----------- 2. gives subscription -> [Subscriber]
[Publisher] <---------- 3. requests values ----- [Subscriber]
// receive(_ input: String) -> Subscribers.Demand
[Publisher] ----------- 4. sends values -------> [Subscriber]
// receive(completion: Subscribers.Completion<Never>)
[Publisher] ----------- 5. sends completion ---> [Subscriber]
  1. Subscriber가 Publisher를 구독(subscribes)하고, Publisher는 구독을 제공(gives subscription)합니다.
  2. Subscriber는 value 0~N개를 받을 수 있습니다.
  3. 마지막에 completion을 받는다.

간단한 예제

class StringSubscriber: Subscriber {
    
    func receive(subscription: Subscription) {
        print("Received Subscription")
        subscription.request(.max(3))
    }
    
    func receive(_ input: String) -> Subscribers.Demand {
        print("Received value", input)
        return .none
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("Completed")
    }
    
    typealias Input = String
    typealias Failure = Never
}

let publisher = ["A", "B", "C", "D", "E", "F", "G"].publisher

let subscriber = StringSubscriber()

// 1. Subscriber가 Publisher를 구독(subscribes)하고, Publisher는 구독을 제공(gives subscription)합니다.
// 1-1. receive(subscription: Subscription)
// 1-2. receive(_ input: String) -> Subscribers.Demand
publisher.subscribe(subscriber)

내가 사용할 수 있는 Publisher엔 뭐가 있을까?

대표적인 Publisher에 대해서 알아보겠습니다.

Convenience

  • Just, 가장 단순한 형태의 Publsiher로 에러 타입은 Never 입니다.
  • Promise, Just와 비슷하지만 Filter Type을 정의할 수 있습니다.
  • Fail, 정의된 실패 타입을 내보냅니다.
  • Empty, 어떤 데이터도 발행하지 않는 퍼블리셔로 주로 에러처리나 옵셔널값을 처리할때 사용됩니다.
  • Sequence, 데이터를 순차적으로 발행하는 Publisher 입니다.
  • ObservableObjectPublisher, SwiftUI에서 사용되는 퍼블리셔입니다.

Published 어노테이션

프로퍼티에 @Published를 붙이면 해당 프로퍼티를 Publisher로 사용할 수 있습니다.

@Published var readCount: Int = 0

Framework 내장제공

Notification Center처럼 프레임워크에서 제공하는 것들이 있습니다. 대표적으로 SwiftUI에서 사용하는 ObservableObject, Notification 등이 있습니다.

Subscriber는?

Subscriber는 3가지 형태로 사용할 수 있습니다. 1) Subscriber를 상속받아 직접 구현, 2) sink를 이용하여 결과값을 소비, 3) assign을 사용해서 결과값을 소비할 수 있습니다.

import Combine

enum MyError: Error {
    case subscriberError
}

class StringSubscriber: Subscriber {
        
    func receive(subscription: Subscription) {
        print("Received Subscription")
        subscription.request(.max(2))
    }
    
    func receive(_ input: String) -> Subscribers.Demand {
        print("Received value", input)
        return .max(1)
    }
    
    func receive(completion: Subscribers.Completion<MyError>) {
        print("Completion")
    }

    typealias Input = String
    typealias Failure = MyError
}

let subscriber = StringSubscriber()

let subject = PassthroughSubject<String, MyError>()

subject.subscribe(subscriber)

let subscription = subject.sink(receiveCompletion: { (completion) in
    print("Received Completion from sink")
}) { value in
    print("Received value from sink")
}

subject.send("A")
subject.send("B")

subscription.cancel()

subject.send("C")
subject.send("D")

sink는 클로저 형태로 데이터를 받는 Subscriber입니다. Input 타입으로는 클로저로 받게되는 데이터값을 에러타입으로는 Never를 받습니다.

Subject

Publsiher의 일종으로 외부에서 안으로 데이터를 주입시킬 수 있으며, PassthroughSubject와 CurrentValueSubject를 제공합니다.

PassthroughSubject

상태값을 가지지 않는 Subject입니다.

let subject = PassthroughSubject<Int, Error>()

subject.sink(receiveCompletion: { completion in
  switch completion {
  case .failure:
    print("failure")
  case .finished:
    print("finished")
  }
}, receiveValue: { value in
  print(value)
})

subject.send(1)
subject.send(2)
subject.send(completion: .finished)

CurrentValueSubject

상태값을 가지는 Subject로 주로 UI의 상태값에따라 데이터를 발행할때 사용하기 유용합니다. 출력값을 아래와 같습니다.

let currentStatus = CurrentValueSubject<Bool, Error>(true)

currentStatus.sink(receiveCompletion: { completion in
  switch completion {
  case .failure:
    print("failure")
  case .finished:
    print("finished")
  }
}, receiveValue: { value in
  print(value)
})

print("초기값은 \(currentStatus.value)입니다.")
currentStatus.send(false)

Cancellable

Subscriber는 대부분 Cancellable을 반환 값을 가집니다. Cancellable은 데이터 발행 중 cancel() 메서드가 호출되었을 때 모든 작동을 멈추고 끝나게 됩니다. 스트림을 강제로 중단해야 할 때 사용될 수 있습니다.

let subject = PassthroughSubject<Int, Error>()

let anyCancle = subject.sink(receiveCompletion: { completion in
  switch completion {
  case .failure:
    print("failure")
  case .finished:
    print("finished")
  }
}, receiveValue: { value in
  print(value)
})

subject.send(1)
subject.send(2)

anyCancle.cancle()

subject.send(completion: .finished)

Opeator

Publisher와 Subscriber 중간에 위치하여 데이터 스트림을 가공해주는 역할을 합니다. 중간에 위치하여 데이터를 가공하여 보내줄 수 있습니다.

다른 데이터 타입으로 변형(scan, setFailureType, map, flatMap), 필터링(compactMap, replaceEmpty, filter, replaceError, removeDuplicates), 데이터 결합(collect, reduce, tryReduce, ignoreOutput), 데이터 스트림 변형(prepend, firstWhere, tryFirstWhere, first, lastWhere, tryLastWhere, last, dropWhile), 간단한 연산(max, count, min)등이 포함됩니다.

간단한 예제를 만들어봐요!

enum HTTPError: LocalizedError {
  case statusCode
  case post
}

struct Post: Codable {
  let id: Int
  let title: String
  let body: String
  let userId: Int
}

let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!

//dataTaskPublsiher는 URLSession에서 제공하는 Publisher입니다.
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
  .map { $0.data }
  .decode(type: [Post].self, decoder: JSONDecoder())
  .replaceError(with: [])
  .eraseToAnyPublisher()
  .sink(receiveValue: { posts in
    print("전달받은 데이터는 총 \(posts.count)개 입니다.")
  })
  • erastToAnyPublisher는 AnyPublisher형태로 리턴해줍니다. eraseToAnyPublisher은 지금까지의 데이터 스트림이 어떠했던 최종적인 형태의 Publisher를 리턴합니다. 아래 예를 들어보겠습니다.
let x = PassthroughSubject<String, Never>()
  .flatMap { name in
    return Future<String, Error> { promise in
      promise(.success(""))
    }.catch { _ in
      Just("No user found")
  }.map { result in
    return "\(result) foo"
  }
}