[iOS/Swift] Combine 알아보기 ii
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 - 구독을 유지하는 객체