https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine
Customize and receive events from asynchronous sources.
비동기 소스로부터 이벤트를 커스터마이즈하고 수신합니다.
컴바인 프레임워크는 앱이 이벤트를 어떻게 처리할지에 대해 선언적 접근 방식을 제공합니다. 여러 딜리게이트 콜백 혹은 컴플리션 핸들러 클로저를 구현하는 것보다, 주어진 이벤트 소스에 단일 처리 연쇄를 생성해서 처리할 수 있습니다. 연쇄의 각 부분은 이전 단계로부터 수신한 요소에 각기 다른 액션을 수행하는 컴바인 오퍼레이터입니다.
텍스트 필드 컨텐츠에 기반해 테이블 혹은 컬렉션 뷰를 필터하는 기능이 필요한 앱을 생각해보겠습니다. AppKit에서 텍스트 필드에 있는 각 키스트로크는 컴바인을 사용해서 구독할 수 있는 노티피케이션을 제공합니다. 노티피케이션을 수신한 후 컨텐트 변경 및 이벤트 전달을 위해 오퍼레이터를 사용할 수 있으며, 앱의 UI를 업데이트하기 위한 최종 결과를 사용할 수 있습니다.
컴바인을 사용해서 텍스트 필드의 노티피케이션을 수신하려면 기본값 NotificationCenter
인스턴스에 접근하고, 이 인스턴스의 publisher(for:object:)
메소드를 호출하면 됩니다. 이 호출은 노티피케이션 이름 및 소스 객체를 받고, 노티피케이션 요소를 제공하는 퍼블리셔를 반환합니다.
let pub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
Subscriber
를 사용해서 퍼블리셔로부터 요소를 수신할 수 있습니다. 구독자는 수신할 타입을 선언하기 위해 연관 타입, Input
을 정의합니다. 퍼블리셔 역시 어떤 것을 제공할지 선언하기 위해 타입, Output
을 선언합니다. 퍼블리셔와 구독자 모두 타입, Failure
를 정의함으로써 오류 유형을 나타낼 수 있습니다. 섭스크라이버를 제공자에 연결하려면 Output
과 Input
이 일치해야 하며, Failure
타입 역시 일치해야 합니다.
컴바인은 두 가지 내장된 구독자를 제공하며, 모두 자동으로 출력과 실패 타입이 붙어있던 퍼블리셔의 타입과 일치합니다.
sink(receiveCompletion:receiveValue:)
는 두 가지 클로저를 사용하게 됩니다. 첫 번째 클로저는 퍼블리셔가 정상적으로 혹은 오류 오류와 함께 실패했는지를 나타내는 열거형인 Subscribers.Completion
를 수신할 때 실행됩니다. 두 번째 클로저는 퍼블리셔로부터 요소를 수신할 때 실행됩니다.assign(to:on:)
은 즉시 모든 요소를 주어진 객체 속성에 할당하고, 속성을 나타내기 위해 키 경로를 사용합니다. 예를 들어 sink
구독자를 사용해서 퍼블리셔가 완료될 때 로그를 출력할 수 있으며, 반복될 때마다 요소를 수신합니다.let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
sink(receiveCompletion:receiveValue:)
, assign(to:on:)
구독자 모두 퍼블리셔로부터 수에 제한이 없이 요소를 요청할 수 있습니다. 받은 요소의 rate를 제어하려면 Subscriber
프로토콜을 채택하고 있는 고유한 구독자를 생성해야 합니다.
이전 섹션에서 다룬 sink
구독자는 receiveValue
클로저에서 모든 작업을 수행합니다. 수신한 요소를 사용해 많은 커스텀 작업을 수행할 필요가 있거나, 호출 사이에 상태를 유지하려면 다소 부담이 될 수 있습니다. 컴바인의 이점은 이벤트 전달을 커스터마이즈하기 위해 결합할 수 있는 오퍼레이터에서 기인합니다.
예를 들어 NotificationCenter.Publisher.Output
은 텍스트 필드의 스트링 값만을 필요로 할 때 콜백에서 수신한 편의 타입이 아닙니다. 퍼블리셔의 출력이 본질적으로 시간에 걸친 요소의 시퀀스이기 때문에 컴바인은 시퀀스 수정 오퍼레이터를 제공하며 대표적으로 map(_:)
, flatMap(maxPublishers:_:)
, reduce(_:_:)
가 있습니다. 이와 같은 오퍼레이터의 동작은 스위프트 표준 라이브러리에 있는 것과 유사합니다.
퍼블리셔의 출력 타입을 변경하려면 다른 타입을 반환할 수 있는 클로저를 소유한 map(_:)
오퍼레이터를 추가하면 됩니다. 이 경우 노티피케이션의 객체를 NSTextField
로써 얻을 수 있고, 필드의 stringValue
를 얻을 수 있습니다.
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
After the publisher chain produces the type you want, replace sink(receiveCompletion:receiveValue:) with assign(to:on:). The following example takes the strings it receives from the publisher chain and assigns them to the filterString of a custom view model object:
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.assign(to: \MyViewModel.filterString, on: myViewModel)
직접 작성한 코드로 액션을 수행하는 오퍼레이터를 사용하기 위해 퍼블리셔 인스턴스를 확장할 수도 있습니다. 이와 같은 이벤트 처리 연쇄를 향상시키기 위해 오퍼레이터를 사용하는 세 가지 방식이 있습니다.
텍스트 필드에 있는 스트링 타입을 업데이트하기보다 filter(_:)
오퍼레이터를 사용해서 입력의 특정 길이를 무시하거나 알파벳, 숫자가 아닌 문자를 받지 않도록 할 수 있습니다
예를 들어 큰 데이터베이스 쿼리처럼 필터 작업의 비용이 높다면 사용자가 타이핑을 멈추는 시점 동안 기다리도록 하는 것을 원할 수 있습니다. 이 경우 debounce(for:scheduler:options:)
를 사용하면 퍼블리셔가 이벤트를 내보내기 전에 최소 시간을 설정해 그 시간만큼 지연시킬 수 있습니다. RunLoop
클래스는 몇 초에서 밀리세컨드 단위로 시간을 지연시킬 수 있도록 편의를 제공합니다.
결과가 UI를 업데이트하면 receive(on:options:)
메소드를 호출해서 메인 스레드에 콜백을 전달할 수 있습니다. 첫 번째 인자인 RunLoop
클래스가 제공하는 Scheduler
인스턴스를 구체화하면 메인 런 루프에서 구독자를 호출하도록 컴바인에게 요구할 수 있습니다.
이와 같은 퍼블리셔 선언은 아래와 같습니다.
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.receive(on: RunLoop.main)
.assign(to:\MyViewModel.filterString, on: myViewModel)
퍼블리셔는 정상적으로 혹은 실패로 완료될 때까지 요소 방출을 계속합니다. 퍼블리셔에서 구독을 더이상 하지 않길 원한다면, 구독을 취소할 수 있습니다. sink(receiveCompletion:receiveValue:)
, assign(to:on:)
로 생성된 구독자 타입 모두 Cancellable
프로토콜을 구현하고 있으며, 이 프로토콜은 cancel()
메소를 제공합니다.
sub?.cancel()
커스텀 구독자를 생성하려면 퍼블리셔는 첫 번째 구독이 발생할 때 구독자 객체를 전달해야 합니다. 이 구독을 저장하고, 퍼블리싱이 취소되기 원하는 시점에 cancel()
메소드를 호출해야 합니다. 커스텀 구독자를 생성하는 경우 Cancellable
프로토콜을 구현해줘야 하며, 저장된 구독에 호출하기 위한 cancel()
구현을 해줘야 합니다.