LoginSignup
5
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-11

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

サンプルコード

asa08/combine-sample

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

UIViewGestureを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)
5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2