-
[iOS/Swift] Combine 알아보기 ii야매 iOS 2023. 2. 23. 23:49
Publisher??
Combine에서 모든 것은 Publisher로부터 시작한다.
- Publisher가 없다면 구독할 대상이 없고 그러므로 값을 받지 못한다.
Publisher 알아보기
💡 Playground를 사용했다.
// publisher의 type = Publishers.Sequence<[Int], Never> let publisher = [1,2,3].publisher
위 코드 블럭에 있는 코드를 배열을 publisher로 변환한 코드다.
- publisher는 구독되면 배열의 요소를 하나씩 방출한다.
Publisher 타입에 대해 알아보기
위 코드 블럭에서 publisher의 타입은 Publishers.Sequence<[Int], Never>
타입으로부터 알 수 있는 것은
- Combine은 Publishers라고 불리는 객체를 담고 있고 Publishers가 여러 다른 publishers를 정의한다.
Publishers?
공식 문서에 따르면 Publishers는 enum 타입으로 여러 타입의 publishers의 네임스페이스다.
- Publishers는 Combine의 built-in publishers를 포함한다.
- 위의 코드에서 사용되는 Publishers.Sequence, Publishers.Map, Publishers.Filter 모두 built-in publishers이다.
Sequence Publishers
- 주어진 sequence의 요소들을 publish(방출)한다
struct Sequence<Elements, Failure> where Elements : Sequence, Failure : Error
- 첫 번째 Generic 인자는 시퀀스(Sequence)를 준수하는 인자, 두 번째 Generic 인자는 Error를 준수하는 인자
Publisher Protocol
Publishers.Sequence는 Publisher 프로토콜을 준수한다.
지금 이 프로토콜에서 가장 눈여겨봐야 하는 포인트는 바로 Publisher의 associated types
- Output and Failure
- Combine의 모든 publisher는 Output과 Failure를 갖는다.
Output은 publisher가 생성하는 값의 타입
- [1,2,3].publisher의 Output은 Int
- 이 publisher의 구독자는 Int를 입력으로 받는다.
Publishers.Sequence의 Failure는 Never
- Publisher가 항상 성공적으로 종료된다는 것을 의미한다.
- 에러 이벤트가 절대로 방출되지 않는다.
URLSession Example
let url = URL(string: "<https://www.google.com>") // typealias URLSession.DataTaskPublisher.Output = (data: Data, response: URLResponse) // typealias URLSession.DataTaskPublisher.Failure = URLError let publisher = URLSession.shared.dataTaskPublisher(for: url)
URLSession.shared.dataTaskPublisher(for:)의 요청이
- 성공하면 (data: Data, response: URLResponse)를 전달받는다
- 실패하면 URLError를 전달받는다.
- 요청은 한번만 실행되므로 요청이 성공하거나 실패하면 publisher는 종료된다.
Notification Example
// typealias NotificationCenter.Publisher.Output = Notification // typealias NotificationCenter.Publisher.Failure = Never let publisher = NotificationCenter.default.publisher( for: UIResponder.keyboardWillShowNotification )
URLSession.DataTaskPublisher와 다르게 이 publisher는 앱이 살아있을 때까지 절대 종료되지 않는다.
- 절대 종료되지 않기 때문에 에러를 방출하지 않는다.
Publisher의 스트림 구독하기
Combine의 Publisher는 값을 방출하고 이것을 스트림이라고 한다.
- 이런 값을 받는 객체들을 subscribers라고 한다
Combine은 범용으로 사용할 수 있는 2가지 subscriber를 제공한다.
sink
[1, 2, 3].publisher.sink(receiveCompletion: { completion in print("completed \\(completion)") }, receiveValue: { value in print("received a value: \\(value)") }) received a value: 1 received a value: 2 received a value: 3 completed finished
sink는 Publisher 프로토콜에 정의되어 있다.
- Combine에 있는 publisher는 sink를 통해 구독할 수 있다.
receiveCompletion
Publisher가 종료되었을 때 호출되는 클로저
receiveValue
값이 방출됐을 때 어떻게 처리할 것인지에 대한 클로저
Subscribers.Completion<Self.Failure>
receiveCompletion 클로저는 인자로 Subscribers.Completion<Self.Failure>를 받는다.
- Swift의 Result 타입과 유사하다
[1, 2, 3].publisher.sink(receiveCompletion: { completion in switch completion { case .finished: print("well finished") case .failure(let error): print(error) } }, receiveValue: { value in print("received a value: \\(value)") })
Failure 타입이 Never일 땐?
Failure 타입이 Never이면 절대 오류가 나지 않는다.
그러므로 receiveCompletion 내부에서 switch문을 통해 성공했을 때, 오류 났을 때를 분기해서 처리할 필요가 없다.
[1,2,3].publishers.sink(receiveValue: { value in print("received a value: \\(value)") })
그럴 땐 위와 같이 간편한 버전을 사용하면 된다.
assign
assign 또한 Publisher에 정의되어 있다.
객체의 프로퍼티에 방출된 publisher 값을 직접적으로 assign할 때 사용
class Person { var name = "anonymous" } var person = Person() ["Martin Odegaard"].publisher.assign(to: \\.name, on: person) print(person.name) // Output = Martin Odegaard
Publisher가 방출한 string이 person 객체의 name 프로퍼티에 값이 assign되었다.
이 메서드는 ReferenceWriteableKeyPath에 값을 할당한다.
- keypath가 클래스에 소속되어 있어야 함을 의미한다.
스트림 생애주기
Combine에서 subscriber가 없다면 publisher는 값을 방출하지 않는다.
AnyCancellable
sink 또는 assign 메서드는 AnyCancellable 객체를 반환한다.
- AnyCancellable 객체가 할당 해제 되면 구독이 사라진다. (또는 취소된다)
let myNotification = Notification.Name("customNotification") func listenToNotifications() { NotificationCenter.default.publisher(for: myNotification) .sink(receiveValue: { notification in print("Received a notification!") }) NotificationCenter.default.post(Notification(name: myNotification)) } listenToNotifications() NotificationCenter.default.post(Notification(name: myNotification)) /* 출력 Received a notification! */
위의 코드에서 NotificationCenter.default.post는 두 번 호출되지만 sink에 전달된 클로저는 한 번만 호출된다.
- listenToNotifications 코드 블럭 내부에 sink 코드가 있고 sink 메서드로부터 반환된 AnyCancellable 객체의 scope는 listenToNotifications 함수 블럭이기 때문이다
let myNotification = Notification.Name("customNotification") var subscription: AnyCancellable? func listenToNotifications() { subscription = NotificationCenter.default.publisher(for: myNotification) .sink(receiveValue: { notification in print("Received a notification!") }) NotificationCenter.default.post(Notification(name: myNotification)) } listenToNotifications() NotificationCenter.default.post(Notification(name: myNotification)) /* 출력 Received a notification! Received a notification! */
위와 같이 AnyCancellable을 보관한다면 구독이 계속 유지된다.
하지만 구독해야 하는 publisher가 많다면?
구독해야 하는 publisher가 많다면 그 수만큼 AnyCancellable 객체를 만들어 줄 필요는 없다.
let myNotification = Notification.Name("customNotification") var cancellables = Set<AnyCancellable>() func listenToNotifications() { NotificationCenter.default.publisher(for: myNotification) .sink(receiveValue: { notification in print("Received a notification!") }) .store(in: &cancellables) NotificationCenter.default.post(Notification(name: myNotification)) } listenToNotifications() NotificationCenter.default.post(Notification(name: myNotification))
AnyCancellable의 store(in:) 메서드를 사용하면 쉽게 사용할 수 있다.
- inout 파라미터를 사용해 sink 메서드로부터 생성된 AnyCancellable을 추가해 준다.
Publisher가 종료됐다면?
Set 또는 프로퍼티에 저장된 AnyCancellable은 Publishsr가 종료되어도 자동으로 할당해제되지 않는다.
요약
Publisher - 값 방출
Subscriber - 값을 받는 대상
AnyCancellable - 구독을 유지하는 객체
'야매 iOS' 카테고리의 다른 글
[iOS/Swift] Combine 알아보기 iv (0) 2023.02.27 [iOS/Swift] Combine 알아보기 iii (0) 2023.02.25 [iOS/Swift] Combine 알아보기 i (1) 2023.02.22 [Swift] Array(repeating: , count:)와 UIStackView (0) 2023.02.19 [iOS/Swift] How to use iOS WidgetKit / 위젯 만들기 (0) 2023.02.19