본문 바로가기
Swift/UIKit

UIControl 이벤트를 Combine 퍼블리셔로 만들기

by songmoro 2025. 8. 20.

 

문제 상황

처음에는 UITextField().publisher(for:) 같은 방식으로 Combine과 연결하려 했지만, 실제로는 값이 한 번만 방출되고 실시간으로 텍스트 변화가 반영되지 않음

 

이를 해결하기 위해 NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification)와 같은 방법을 쓸 수도 있지만, 데이터 흐름이 분산되어 추적하기 어렵다고 판단

 

결국, UIControl 자체를 Combine 퍼블리셔로 래핑하는 방법을 선택

 

참고 링크의 코드를 참고해 사용했지만, 실 사용시 아래와 같이 UIControl 타입을 UIButton, UITextField로 타입 캐스팅해줘야 하는 불편함이 존재

nicknameTextField.publisher(.editingChanged)
    .compactMap { $0 as? UITextField }
    .map(\.text)
    .sink { [weak self] in
        self?.viewModel.input.nickname = $0
    }
    .store(in: &cancellables)

 

UIControl + Combine

UIButton, UITextField, UISwitch 등 UIControl은 이벤트를 감지할 수 있으니, 이를 Combine Publisher로 변환


UIControl이 AnyUIControl을 채택하게 해 UIControl에서 publisher(_:) 메서드를 사용할 수 있도록 확장

import UIKit
import Combine

protocol AnyUIControl {}

extension AnyUIControl where Self: UIControl {
    func publisher(_ event: Self.Event) -> UIControl.EventPublisher<Self> {
        return EventPublisher(control: self, event: event)
    }
}

extension UIControl: AnyUIControl {}

 

EventPublisher

extension UIControl {
    struct EventPublisher<Output: UIControl>: Publisher {
        typealias Failure = Never

        private let control: Output
        private let event: UIControl.Event

        init(control: Output, event: UIControl.Event) {
            self.control = control
            self.event = event
        }

        func receive<S>(subscriber: S)
        where S: Subscriber, S.Input == Output, S.Failure == Never {
            let subscription = EventSubscription(control: control, subscrier: subscriber, event: event)
            subscriber.receive(subscription: subscription)
        }
    }
}

 

Subscription

private final class EventSubscription<
    EventSubscriber: Subscriber,
    Input: UIControl
>: Subscription where EventSubscriber.Input == Input, EventSubscriber.Failure == Never {

    private let control: Input
    private var subscriber: EventSubscriber?

    init(control: Input, subscrier: EventSubscriber, event: UIControl.Event) {
        self.control = control
        self.subscriber = subscrier
        control.addTarget(self, action: #selector(handler), for: event)
    }

    func request(_ demand: Subscribers.Demand) { }

    func cancel() {
        subscriber = nil
    }

    @objc private func handler() {
        _ = subscriber?.receive(control)
    }
}

 

사용 예시

final class ViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()
    private let textField = UITextField()
    private let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.publisher(.editingChanged)
            .sink { tf in
                print("텍스트 변경됨:", tf.text ?? "")
            }
            .store(in: &cancellables)

        button.publisher(.touchUpInside)
            .sink { _ in
                print("버튼 클릭됨")
            }
            .store(in: &cancellables)
    }
}

 

참고

https://kka7.tistory.com/391