-
Operator, Combine Framework in SwiftiOS 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