야매 iOS

[iOS/Swift] Combine 알아보기 ii

the Cosmos 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 - 구독을 유지하는 객체