[iOS13]UILabelを長押ししてコピーしたい

## 概要

[UITextField](https://developer.apple.com/documentation/uikit/uitextfield)はフォーカスを当てて長押しすると、コピーやカットなどのメニューが表示されますよね。
しかし[UILabel](https://developer.apple.com/documentation/uikit/uilabel)はこの機能がついてないのでつけたいな、と。
検索してみるとすぐに実装方法がヒット！
その通りにやってみた。
........メニューが全然表示されない泣
3時間半くらい苦戦してようやく解決法を見つけたので備忘録としてまとめます。
また[UIMenuController](https://developer.apple.com/documentation/uikit/uimenucontroller)を調べていて、iOS13から新しく`Deprecated`になっていた要素があったので紹介します。

## 環境

* iOS 13.0
* Xcode 11.0
* Swift5

## 実装

### ① カスタムクラスの作成

UILabel長押しでメニューを表示させるには、まずUILabelを継承したカスタムクラスを作成します。

```swift:CopyableLabel.swift
import UIKit

class CopyableLabel: UILabel
{
    // これからここに書いていく
}
```

### ② 長押しできるように設定

ラベルを長押しして特定のイベントを発動できるように、
タップ検出のプロパティ [isUserInteractionEnabled](https://developer.apple.com/documentation/uikit/uiview/1622577-isuserinteractionenabled) を有効化、
[addGestureRecognizer](https://developer.apple.com/documentation/uikit/uiview/1622496-addgesturerecognizer)でジェスチャーを追加します。
今回は長押ししたいので[UILongPressGestureRecognizer](https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer)を使用します。

```swift:CopyableLabel.swift
class CopyableLabel: UILabel
{
    override init(frame: CGRect)
    {
        super.init(frame: frame)
        setup()
    }

    required init?(coder: NSCoder)
    {
        super.init(coder: coder)
        // Storyboardからの設定を無視したい場合は書く
        setup()
    }

    private func setup()
    {
        isUserInteractionEnabled = true
        // ジェスチャーの追加
        addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:))))
    }

    /// 長押ししたら発動
    ///
    /// - Parameter recognizer: UIGestureRecognizer
    @objc func handleLongPressGesture(_ recognizer: UIGestureRecognizer)
    {
        // あとでここに追記する
    }
}
```

### ③ メニューを開くよう設定

[UIGestureRecognizer](https://developer.apple.com/documentation/uikit/uigesturerecognizer)を引数に受けているので、
ジェスチャーが確認できた状態だけメニューを開くようにguard判定を挟みます。
しなくても大丈夫かもですが←

次にメニューを開く位置を設定します。
はじめメニューが表示されなかった理由はここで、ラベルのboundsを[setTargetRect](https://developer.apple.com/documentation/uikit/uimenucontroller/1622812-settargetrect)に設定していましたが
それだといつまで経っても表示されず...
代わりにジェスチャーを受けたViewの位置を指定するとうまくいきました！
同じように苦戦した方いますかね汗

あとは[setMenuVisible](https://developer.apple.com/documentation/uikit/uimenucontroller/1622819-setmenuvisible)でメニューを表示します。

ここで一番大事なのは、[canBecomeFirstResponder](https://developer.apple.com/documentation/uikit/uiresponder/1621130-canbecomefirstresponder)をoverrideして`true`にすることです。
これをしないとメニューを表示できません。

```swift:CopyableLabel.swift
class CopyableLabel: UILabel
{
    // 省略

    override var canBecomeFirstResponder: Bool {
        return true
    }

    /// 長押ししたら発動
    ///
    /// - Parameter recognizer: UIGestureRecognizer
    @objc func handleLongPressGesture(_ recognizer: UIGestureRecognizer)
    {
        // 状態判定
        guard recognizer.state == .recognized else { return }

        if let recognizerView = recognizer.view, let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
            let menuController = UIMenuController.shared
            menuController.setTargetRect(recognizerView.frame, in: recognizerSuperView)
            menuController.setMenuVisible(true, animated: true)
        }
    }
}
```

### ④ メニューに表示する項目の設定

今回は`コピー`だけで良いので[canPerformAction](https://developer.apple.com/documentation/uikit/uiresponder/1621105-canperformaction)でコピーアクションの許可をします。
もし自分でカスタマイズしたアクションを表示したい場合はここで制御可能です。

```swift:CopyableLabel.swift
class CopyableLabel: UILabel
{
    // 省略

    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool
    {
        // コピーアクションだけ許可
        return action == #selector(UIResponderStandardEditActions.copy)
    }
}
```

### ⑤ ラベルのテキストコピー

メニューに表示された[コピー]を押したときに実行したい処理を書きます。
今回は単純にラベルのテキストをコピーしたいだけなので次のような書き方で終了です。

```swift:CopyableLabel.swift
class CopyableLabel: UILabel
{
    // 省略

    override func copy(_ sender: Any?)
    {
        UIPasteboard.general.string = text
    }
}
```

## 出来上がり

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509799/a9449c55-9a6b-5c51-1f1a-8729badec090.gif" width="50%">

## iOS13以降のメニュー開き方

冒頭にも書いたように、iOS13.0からメニューを開くメソッドがDeprecatedになってました。
先ほど③で`setTargetRect`と`setMenuVisible`でメニューの表示位置、いまだ開け！という指示をしたわけですが、
[showMenu](https://developer.apple.com/documentation/uikit/uimenucontroller/3044217-showmenu)の1つでその2つを賄えるようになりました。
うん、簡単。ありがたい！

```swift:CopyableLabel.swift
class CopyableLabel: UILabel
{
    // 省略

    /// 長押し検知
    ///
    /// - Parameter recognizer: UIGestureRecognizer
    @objc func handleLongPressGesture(_ recognizer: UIGestureRecognizer)
    {
        guard recognizer.state == .recognized else { return }

        if let recognizerView = recognizer.view, let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
            let menuController = UIMenuController.shared
            if #available(iOS 13.0, *) {
                menuController.showMenu(from: recognizerSuperView, rect: recognizerView.frame)
            } else {
                menuController.setTargetRect(recognizerView.frame, in: recognizerSuperView)
                menuController.setMenuVisible(true, animated: true)
            }
        }
    }
}
```

## 参考

* [ios - Show iPhone cut copy paste menu on UILabel - Stack Overflow](https://stackoverflow.com/questions/1246198/show-iphone-cut-copy-paste-menu-on-uilabel)