ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 - 구독을 유지하는 객체

Designed by Tistory.