반응형
iOS 13 이상 부터 Combine을 사용할수 있기에 Rx를 사용하지 않더라도 다음과 같은 Extension으로 UIKit Control들의 이벤트를 처리 할 수 있습니다.
import UIKit
import Combine
extension UIControl {
/// Control Publisher
func controlPublisher(for event: UIControl.Event) -> UIControl.EventPublisher {
return UIControl.EventPublisher(control: self, event: event)
}
/// Event Publisher
struct EventPublisher: Publisher {
typealias Output = UIControl
typealias Failure = Never
let control: UIControl
let event: UIControl.Event
func receive<T>(subscriber: T) where T: Subscriber, Never == T.Failure, UIControl == T.Input {
let subscription = EventSubscription(control: control, subscrier: subscriber, event: event)
subscriber.receive(subscription: subscription)
}
}
/// Event Subscription
private class EventSubscription<EventSubscriber: Subscriber>: Subscription where EventSubscriber.Input == UIControl, EventSubscriber.Failure == Never {
let control: UIControl
let event: UIControl.Event
var subscriber: EventSubscriber?
init(control: UIControl, subscrier: EventSubscriber, event: UIControl.Event) {
self.control = control
self.subscriber = subscrier
self.event = event
control.addTarget(self, action: #selector(eventDidOccur), for: event)
}
func request(_ demand: Subscribers.Demand) {}
func cancel() {
subscriber = nil
control.removeTarget(self, action: #selector(eventDidOccur), for: event)
}
@objc func eventDidOccur() {
_ = subscriber?.receive(control)
}
}
}
extension UITextField {
/// 텍스트 필드 입력
var textPublisher: AnyPublisher<String?, Never> {
controlPublisher(for: .editingChanged)
.compactMap { $0 as? UITextField }
.map { $0.text }
.eraseToAnyPublisher()
}
/// 텍스트 필드 입력 Debounce
var textDebouncePublisher: AnyPublisher<String?, Never> {
textPublisher.debounce(for: 0.1, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
/// 텍스트 필드 편집 종료 Publisher
var didEndEditingPublisher: AnyPublisher<Void, Never> {
controlPublisher(for: .editingDidEnd)
.map { _ in }
.eraseToAnyPublisher()
}
}
extension UIButton {
/// 버튼 touchUpInside 처리
var tapPublisher: AnyPublisher<Void, Never> {
controlPublisher(for: .touchUpInside)
.map { _ in }
.eraseToAnyPublisher()
}
/// 버튼 touchUpInside Debounce
var tapDebouncePublisher: AnyPublisher<Void, Never> {
tapPublisher.debounce(for: 0.1, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
extension UISwitch {
/// valueChanged 처리
var statePublisher: AnyPublisher<Bool, Never> {
controlPublisher(for: .valueChanged)
.compactMap { $0 as? UISwitch }
.map { $0.isOn }
.eraseToAnyPublisher()
}
}
extension UIStepper {
/// valueChanged 처리
var valuePublisher: AnyPublisher<Double, Never> {
controlPublisher(for: .valueChanged)
.compactMap { $0 as? UIStepper }
.map { $0.value }
.eraseToAnyPublisher()
}
}
extension UISegmentedControl {
/// valueChanged 처리
var selectionPublisher: AnyPublisher<Int, Never> {
controlPublisher(for: .valueChanged)
.compactMap { $0 as? UISegmentedControl }
.map { $0.selectedSegmentIndex }
.eraseToAnyPublisher()
}
}
extension UISlider {
/// valueChanged 처리
var valuePublisher: AnyPublisher<Float, Never> {
controlPublisher(for: .valueChanged)
.compactMap { $0 as? UISlider }
.map { $0.value }
.eraseToAnyPublisher()
}
}
// UIView Gesture 처리
// 참고: https://jllnmercier.medium.com/combine-handling-uikits-gestures-with-a-publisher-c9374de5a478
extension UIView {
func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher {
.init(view: self, gestureType: gestureType)
}
struct GesturePublisher: Publisher {
typealias Output = GestureType
typealias Failure = Never
private let view: UIView
private let gestureType: GestureType
init(view: UIView, gestureType: GestureType) {
self.view = view
self.gestureType = gestureType
}
func receive<S>(subscriber: S) where S: Subscriber,
GesturePublisher.Failure == S.Failure, GesturePublisher.Output
== S.Input {
let subscription = GestureSubscription(
subscriber: subscriber,
view: view,
gestureType: gestureType
)
subscriber.receive(subscription: subscription)
}
}
enum GestureType {
case tap(UITapGestureRecognizer = .init())
case swipe(UISwipeGestureRecognizer = .init())
case longPress(UILongPressGestureRecognizer = .init())
case pan(UIPanGestureRecognizer = .init())
case pinch(UIPinchGestureRecognizer = .init())
case edge(UIScreenEdgePanGestureRecognizer = .init())
func get() -> UIGestureRecognizer {
switch self {
case let .tap(tapGesture):
return tapGesture
case let .swipe(swipeGesture):
return swipeGesture
case let .longPress(longPressGesture):
return longPressGesture
case let .pan(panGesture):
return panGesture
case let .pinch(pinchGesture):
return pinchGesture
case let .edge(edgePanGesture):
return edgePanGesture
}
}
}
class GestureSubscription<S: Subscriber>: Subscription where S.Input == GestureType, S.Failure == Never {
private var subscriber: S?
private var gestureType: GestureType
private var view: UIView
init(subscriber: S, view: UIView, gestureType: GestureType) {
self.subscriber = subscriber
self.view = view
self.gestureType = gestureType
configureGesture(gestureType)
}
private func configureGesture(_ gestureType: GestureType) {
let gesture = gestureType.get()
gesture.addTarget(self, action: #selector(handler))
view.addGestureRecognizer(gesture)
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
subscriber = nil
view.removeGestureRecognizer(gestureType.get())
}
@objc
private func handler() {
_ = subscriber?.receive(gestureType)
}
}
}
반응형
'iOS > Combine' 카테고리의 다른 글
PassthroughSubject vs CurrentValueSubject (1) | 2023.12.01 |
---|---|
Throttle과 Debounce의 차이점 (0) | 2023.05.31 |
PassthroughSubject (0) | 2023.05.31 |
사용자 정의(custom) 구독자(subscriber) 만들기 (0) | 2023.05.10 |
Combine with Timer (0) | 2023.05.09 |
Combine과 Property Wrapper를 이용해서 UserDefault 쉽게 사용하기 (0) | 2023.05.08 |
MVVM with Combine Tutorial for iOS (1) | 2019.09.05 |
RxSwift 와 Combine (0) | 2019.07.26 |