【Swift】CombineとUIKitを連携させる（サンプルコードあり）

CombineとUIKitを連携させるいい感じのサンプルを作りました。
Combineを使うことで各コンポーネントのアクションの検知をスッキリ書くかことができて最高に気持ちいいです。
参考にしてもらえると嬉しいです。

## サンプルコード
[asa08/combine-sample](https://github.com/asa08/combine-sample)

## Combine+UIControl
`UIControl`には以下のコンポーネントがあります。
これらのアクションをCombineで`sink`できるようにします。

- UIButton
- UITextField
- UIPageControl
- UISwitch etc...

よく使うUIButtonとUITextFieldを紹介します。

### CombineのExtensionを作成する
UIControlのExtensionを作成します。実装は[こちら](https://gist.github.com/d-date/735fa126e37ed88ff9d0011f6dc0510b)を拝借しました。

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/main/combines/Combine/Combine%2BUIControl.swift)

```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`のタップを検知する

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/dc16e4c4239e013e6e00942017ba1fc814df6602/combines/Screen/Feed/View/Module/FeedButtonView.swift#L42)

```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`のテキストの変更を検知する

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/main/combines/Screen/Feed/View/Module/FeedTextFieldView.swift)

```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を作成する
実装は[こちら](https://jllnmercier.medium.com/combine-handling-uikits-gestures-with-a-publisher-c9374de5a478)を拝借しました。

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/main/combines/Combine/Combine%2BTapGesture.swift)

```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のタップを検知する

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/dc16e4c4239e013e6e00942017ba1fc814df6602/combines/Screen/Feed/View/Module/FeedSimpleView.swift#L39)

```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を作成する

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/main/combines/Combine/Combine%2BUITextView.swift.swift)

```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
- テキストの変更を検知する

[サンプルコードの箇所](https://github.com/asa08/combine-sample/blob/dc16e4c4239e013e6e00942017ba1fc814df6602/combines/Screen/Feed/View/Module/FeedTextView.swift#L41)

```UITextViw.swift
textView.textPublisher().sink(receiveValue: { text in
    // 変更後のテキストはtext
    print("text canged!!")
}).store(in: &cancellables)
```