CombineとUIKitを連携させるいい感じのサンプルを作りました。
Combineを使うことで各コンポーネントのアクションの検知をスッキリ書くかことができて最高に気持ちいいです。
参考にしてもらえると嬉しいです。
サンプルコード
Combine+UIControl
UIControl
には以下のコンポーネントがあります。
これらのアクションをCombineでsink
できるようにします。
- UIButton
- UITextField
- UIPageControl
- UISwitch etc...
よく使うUIButtonとUITextFieldを紹介します。
CombineのExtensionを作成する
UIControlのExtensionを作成します。実装はこちらを拝借しました。
Combine+UIControl.swift
import Combine
import UIKit
public protocol CombineCompatible {}
public extension UIControl {
final class Subscription<SubscriberType: Subscriber, Control: UIControl>:
Combine.Subscription where SubscriberType.Input == Control {
private var subscriber: SubscriberType?
private let input: Control
public init(subscriber: SubscriberType, input: Control, event: UIControl.Event) {
self.subscriber = subscriber
self.input = input
input.addTarget(self, action: #selector(eventHandler), for: event)
}
public func request(_ demand: Subscribers.Demand) {}
public func cancel() {
subscriber = nil
}
@objc private func eventHandler() {
_ = subscriber?.receive(input)
}
}
struct Publisher<Output: UIControl>: Combine.Publisher {
public typealias Output = Output
public typealias Failure = Never
let output: Output
let event: UIControl.Event
public init(output: Output, event: UIControl.Event) {
self.output = output
self.event = event
}
public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = Subscription(subscriber: subscriber, input: output, event: event)
subscriber.receive(subscription: subscription)
}
}
}
extension UIControl: CombineCompatible {}
public extension CombineCompatible where Self: UIControl {
func publisher(for event: UIControl.Event) -> UIControl.Publisher<UIControl> {
.init(output: self, event: event)
}
}
UIButton+Combine
-
UIButton
のタップを検知する
UIButton.swift
var cancellables = Set<AnyCancellable>()
let uibutton = UIButton()
uibutton.publisher(for: .touchUpInside).sink(receiveValue: { _ in
print("button tapped!!")
}).store(in: &cancellables)
UITextField+Combine
-
UITextField
のテキストの変更を検知する
UITextField.swift
// テキストの変更を検知してLabelにassignする
var cancellables = Set<AnyCancellable>()
let label = UILabel()
let uitextfield = UITextField()
uitextfield.publisher(for: .allEditingEvents)
.map{ _ in self.uitextfield.text }
.assign(to: \.text, on: label)
.store(in: &cancellables)
Combine+UIViewのGesture
UIView
のGesture
をCombineでsink
できるようにします。
CombineのExtensionを作成する
実装はこちらを拝借しました。
Combine+TapGesture.swift
import UIKit
import Combine
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
}
}
}
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)
}
}
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
}
@objc
private func handler() {
_ = subscriber?.receive(gestureType)
}
}
extension UIView {
func gesture(_ gestureType: GestureType = .tap()) -> GesturePublisher {
.init(view: self, gestureType: gestureType)
}
}
UIView+Combine
- UIViewのタップを検知する
UIView.swift
var cancellables = Set<AnyCancellable>()
let uiview = UIView()
uiview.gesture().sink(receiveValue: { _ in
print("view tapped!!")
}).store(in: &cancellables)
- UIVIewのスワイプを検知する
UIView.swift
var cancellables = Set<AnyCancellable>()
let uiview = UIView()
uiview.gesture(.swipe()).sink { _ in
print("view swiped!!")
}.store(in: &cancellables)
UITextView+Combine
UITextFieldはUIControlを継承していますがUITextViewは違うので、こちらは別途作成が必要です。
今回はテキストの変更の検知だけ紹介します。
CombineのExtensionを作成する
Combine+UITextView.swift
import UIKit
import Combine
extension UITextView {
func textPublisher() -> AnyPublisher<String, Never> {
NotificationCenter.default
.publisher(for: UITextView.textDidChangeNotification, object: self)
.map { ($0.object as? UITextView)?.text ?? "" }
.eraseToAnyPublisher()
}
}
UITextView+Combine
- テキストの変更を検知する
UITextViw.swift
textView.textPublisher().sink(receiveValue: { text in
// 変更後のテキストはtext
print("text canged!!")
}).store(in: &cancellables)