ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Operator, Combine Framework in Swift
    iOS 2022. 12. 11. 17:00

     

     Combine 프레임워크는 앱이 이벤트를 처리하는 방법에 대한 선언적 접근 방식을 제공합니다. 지정된 이벤트 소스에 대해 단일 처리 체인, 즉 파이프라인을 만들 수 있습니다. 파이프라인은 데이터 처리 단계의 출력이 다음 단계의 입력으로 이어지는 형태로 연결된 구조로, 이렇게 연결된 데이터 처리 단계는 한 여러 단계가 서로 동시에, 또는 병렬적으로 수행될 수 있어 효율성이 향상됩니다. 이러한 각 부분은 이전 단계에서 받은 요소에 대해 고유한 작업을 수행하는 Operator입니다. 예를 들어, Combine으로 구독할 수 있는 Notification을 생성했을 때, Notification을 받은 후 operator를 사용하여 이벤트 전달 내용을 사용자 정의하고 최종 결과를 사용하여 UI를 업데이트할 수 있습니다.

     

    다음과 같은 다양한 operator를 학습해보겠습니다. map, filter, reduce 등 많은 연산자가 swift 표준 라이브러리의 연산자와 유사한 작업을 수행합니다.

    • map(_:)
    • tryMap(_:)
    • flatMap(_:)
    • filter(_:)
    • debounce(for:scheduler:options:)
    • receive(on:)

    map(_:)

    func map<T>(_ transform: @escaping (Self.Output) -> T)
    	-> Publisher.Map<Self, T>

     upstream publisher로부터 온 값을 제공한 클로저로 변환합니다. 사용자가 UITextField의 text를 변경했을 때 발생하는 Notification으로 예시를 들어보겠습니다. NotificationCenter publisher의 Failure는 Never로, 에러가 발생할 리 없다는 것을 이미 알고 있죠. 에러를 던질 수 있다면, tryMap(_:) 연산자를 사용합니다.

    @IBOutlet var myTextField: UITextField!
    var mySubscriber: AnyCancellable?
    
    // Publisher
    let myPublisher = NotificationCenter.default
    	.publisher(for: UITextField.textDidChangeNotification, object: myTextField)
    	.map { notification in
    	    (notification.object as! UITextField).text!
    	}
    
    // Subscriber
    mySubscriber = myPublisher
    		.assign(to: \.myText, on: myViewModel)
    
    
    // 에서 1로 변경되었습니다.
    // 1에서 12로 변경되었습니다.
    // 12에서 123로 변경되었습니다.
    // 123에서 12로 변경되었습니다.

     위와 같이 publisher의 output 타입을 변환하여 뷰 모델 객체의 프로퍼티에 할당해줄 수 있습니다. 물론 위의 코드에서 map(_:)은 publisher에서 수행하든, subscriber에서 수행하든, 같은 결과를 끌어낼 수 있습니다.

     


    tryMap(_:)

    func tryMap<T>(_ transform: @escaping (Self.Output) throws -> T) 
    	-> Publishers.TryMap<Self, T>

     upstream publisher로부터 온 값을 제공된, 에러를 던지는 클로저를 사용해 변환합니다. stream에서 발생하는 모든 에러를 Failure로 변환하는 기능을 추가한다는 점을 제외하면 map(_:)과 같습니다. 실제로 이 연산자의 출력은 Failure가 swift의 Error 프로토콜을 준수하는 Publisher입니다. 에러가 발생할 수 있는 디코드 작업을 수행하는 예를 들어서, 다음과 같이 사용합니다. 

    let myPublisher = NotificationCenter.default.publisher(for: myNewDownload)
    	.map { $0.userInfo?["data"] as! Data }
    	.tryMap { data in
    	    let decoder = JSONDecoder()
    	    try decoder.decode(MyModel.self, from: data)
    	}

     이는 아래와 같이 decode() 연산자로 간단하게 작성할 수도 있습니다. 또한 catch 연산자는 원래의 publisher를 새로운 것으로 교체하여 오류를 복구할 수 있습니다.

    .map { ... }
    .decode (MyModel.self, JSONDecoder())
    .catch { Just(MyModel.placeholder) }
    
    // Just는 subscriber에게 Output을 한 번 내보내고 종료되는 convenience publisher입니다.

    catch같은 에러 처리 연산자로는 다음과 같은 것들이 있습니다.

    • retry(_:): 실패한 upstream publisher에 대한 구독을 다시 생성하려고 명시한 만큼 시도.
    • mapError(_:): 에러를 다른 에러로 변환.
    • setFailureType(to:): upstream publisher에 의해 선언된 Failure 타입을 바꿈. combineLatest(_:)로 publisher들을 결합할 때, 그것들의 에러 타입이 맞지 않을 때 사용.

    flatMap(_:)

     위 내용과 이어서, catch()를 사용하여 다른 publisher로 대체하여 복구하면 그 에러에 대해서는 성공적이지만, 해당 구독을 종료했기 때문에 그 이후에 Notification을 받아서 연산을 수행하지는 못합니다. 따라서 원래의 upstream에 대한 연결을 유지하면서 placeholder를 사용하는 기능을 사용해야 합니다. 그러기 위해서 flatMap(_:)을 사용합니다.

    let myPublisher = NotificationCenter.default.publisher(for: myNewDownload)
    	.map { $0.userInfo?["data"] as! Data }
    	.flatMap {
    	    return Just(data)
    	        .decode(MyModel.self, JSONDecoder())
    	        .catch {
    	            return Just(MyModel.placeholder)
    	        }
    	}
    	.publisher(for \.name) // my model 내부에 접근하여 name 프로퍼티 추출

     map() 연산자에서 변환한 데이터를 가지고 디코딩하고, catch하고, flatMap에 반환합니다. 따라서 flatMap이 publisher를 구독할 것이며 결과적으로 publisher는 절대 실패할 수 없습니다. 또한 publisher(for:)를 사용하여 type safe key path를 통해 새로운 my model의 name 프로퍼티를 내보내는 Publisher를 생성합니다.


    filter(_:)

    func filter(_ isIncluded: @escaping (Self.Output) -> Bool) 
    	-> Publishers.Filter<Self>

     제공한 클로저에 맞는 값을 republish. 즉, isIncluded는 값 하나를 republish 할지 여부를 나타내는 Boolean 값을 반환하는 클로저입니다. filter(_:)를 사용해서 다음과 같이 텍스트필드의 텍스트 수가 5 이하면 무시하도록 할 수 있습니다.

    let myPublisher = ... // textField publisher
    mySubscriber = myPublisher
    	.filter { $0.count > 5 }

    debounce(for:scheduler:options:)

    func debounce<S>(
        for dueTime: S.SchedulerTimeType.Stride,
        schduler: S,
        options: S.SchedulerOptions? = nil
    ) -> Publishers.Debounce<Self, S> where S : Scheduler

     이벤트 간에 지정된 시간 간격 이후 값을 publish.

     

    Parameter

    dueTime

     값을 게시하기 전에 publisher가 기다려야 하는 시간

     

    scheduler

     스케줄러

     

    options

     Publisher의 값 전달을 사용자 정의하는 스케줄러 옵션. 각 스케줄러에서 자유롭게 정의할 수 있습니다.

     

     debounce(for:scheduler:options:) 연산자를 사용하여 값이 upstream publisher로부터 전달되는 간격을 제어할 수 있습니다. downstream으로 전달되는 값의 수를 지정한 비율로 줄여야 하는 대용량 이벤트를 처리하는 데 유용합니다. 예를 들어 텍스트필드의 텍스트로 네트워크 통신을 한다고 할 때, 텍스트가 변경될 때마다 네트워크 작업을 하게 되면 부담이 됩니다. 다음 예시와 같이 0.5초의 간격을 두고 처리하게 된다면, 그것을 해결할 수 있습니다.

    mySubscriber = myPublisher
    	.debounce(for: .second(0.5), scheduler: RunLoop.main)
    	// do something with the value

    Scheduled Operator

     특정 이벤트가 전달되는 시기와 장소를 설명하는 데 도움을 줍니다. 기본적으로 RunLoop와 DispatchQueue에 의해 지원되며 이러한 연산자의 몇 가지 예에는 특정 미래 시간까지 이벤트 전달을 연기하는 delay와 같은 연산자와 이벤트가 지정된 속도보다 빠르지 않게 전달되도록 보장하는 throttle이 있습니다. 또한 수신된 downstream 이벤트가 특정 스레드나 대기열에서 전달되도록 보장하는 receive(on:) 연산자도 있습니다.

    .flatMap { ... }
    .publisher(for: \.name)
    // 작업을 메인 스레드로 옮김.
    // 결과는 (String, Never)로,
    // scheduled operator는 일반적으로 Output 및 Failure가 변경되지 않음.
    .receive(on: RunLoop.main)

    참고

    WWDC 2019 Introducing Combine

    (https://developer.apple.com/videos/play/wwdc2019/722)

     

    WWDC 2019 Combine in Practice

    (https://developer.apple.com/videos/play/wwdc2019/721)

     

    Apple Documentation

    (https://developer.apple.com/documentation/combine)

     

    'iOS' 카테고리의 다른 글

    Actor in Swift concurrency  (0) 2022.12.31
    Diffable DataSource  (2) 2022.12.29
    Localization in iOS  (0) 2022.12.14
    Publisher와 Subscriber, Combine Framework in Swift  (0) 2022.12.09
    Async/await in Swift  (0) 2022.12.03

    댓글

Designed by Tistory.