UIKit 기반 앱에서도 Combine을 활용한 선언적 이벤트 처리가 가능합니다.
이 문서에서는 UIButton, UISwitch, UITextField 등 UIControl 기반 컴포넌트의 이벤트를
Combine Publisher로 래핑하여 사용하는 방법을 소개합니다.
아래와 같이 UIKit 컨트롤의 이벤트를 Combine으로 선언적으로 처리하는 게 목표입니다.
button
.publisher(for: .touchUpInside)
.sink {
print("버튼이 눌렸습니다!")
}
.store(in: &cancellables)
UIKit은 Combine을 기본적으로 지원하지 않기 때문에, 커스텀 Publisher를 직접 구현해야 합니다.
import Combine
import UIKit
extension UIControl {
func publisher(for event: UIControl.Event) -> UIControl.EventPublisher {
return UIControl.EventPublisher(control: self, event: event)
}
struct EventPublisher: Publisher {
typealias Output = Void
typealias Failure = Never
let control: UIControl
let event: UIControl.Event
func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = EventSubscription(subscriber: subscriber, control: control, event: event)
subscriber.receive(subscription: subscription)
}
private final class EventSubscription<S: Subscriber>: Subscription where S.Input == Void {
private var subscriber: S?
weak private var control: UIControl?
init(subscriber: S, control: UIControl, event: UIControl.Event) {
self.subscriber = subscriber
self.control = control
control.addTarget(self, action: #selector(eventHandler), for: event)
}
func request(_ demand: Subscribers.Demand) {
// 수요 관리 없음 (Void 이벤트 처리이므로)
}
func cancel() {
subscriber = nil
}
@objc private func eventHandler() {
_ = subscriber?.receive(())
}
}
}
}
import UIKit
import Combine
final class ExampleViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let button = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
button.setTitle("눌러보세요", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 18)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.frame = CGRect(x: 100, y: 200, width: 160, height: 50)
view.addSubview(button)
button
.publisher(for: .touchUpInside)
.sink {
print("✅ 버튼이 눌렸습니다")
}
.store(in: &cancellables)
}
}
| 구성 요소 | 설명 |
|---|---|
publisher(for:) | UIControl의 이벤트를 Combine Publisher로 래핑 |
EventPublisher | Combine의 Publisher 프로토콜을 구현한 커스텀 퍼블리셔 |
EventSubscription | 실제 UI 이벤트 발생 시 subscriber에게 이벤트를 전달 |
@objc eventHandler() | UIControl 이벤트가 발생하면 호출되는 메서드 |
| 용어 | 설명 |
|---|---|
Publisher | 데이터를 발행하는 주체 예: Just, URLSession, 배열 등 |
Subscriber | 데이터를 수신하는 주체 예: sink, assign |
Operator | 데이터를 중간에 변형/필터링하는 도구 예: map, filter, debounce |
💡 Combine의 기본 흐름:
Publisher → Operator → Subscriber
| 항목 | 설명 |
|---|---|
커스텀 Publisher | UIKit 이벤트에 특화됨send() 호출 없이 이벤트 감지 가능 |
PassthroughSubject | 외부에서 수동으로 .send() 호출 필요 |
CurrentValueSubject | 항상 최신 상태를 유지하며 구독 시 즉시 값 전달 가능 |
| 컨트롤 | 사용 가능한 이벤트 예시 |
|---|---|
UIButton | .touchUpInside, .touchDown |
UISwitch | .valueChanged |
UITextField | .editingChanged, .editingDidEnd |
UISlider | .valueChanged, .touchUpInside 등 |
UITextField와 Combine을 이용한 양방향 바인딩 구현 UISwitch 상태 변화 감지하여 뷰 상태 동기화 및 UI 업데이트| 항목 | 설명 |
|---|---|
| Combine 도입 이유 | 선언적 UI 이벤트 처리 |
| 커스텀 Publisher 구현 이유 | UIKit ↔ Combine 연결 |
| 핵심 클래스 | Publisher / Subscription / UIControl |
| 활용 가능 이벤트 | UIButton, UISwitch, UITextField 등 |
| 사용 예시 | .publisher(for: .touchUpInside) → .sink {} 처리 |
UIKit에서도 Combine을 통해 구조적이고 선언적인 방식으로 UI 이벤트를 처리할 수 있습니다.
위 확장을 프로젝트에 도입하면, MVVM + Combine 구조에 매우 유용하게 활용할 수 있습니다.