コピー可能な UILabel
編集不可な文字列を UILabel
で表示させつつユーザにコピー操作だけはさせたいという事があったので実装を試みてみた。
コピーの方法は UITextView
や UITextField
でコピー操作を行う時と同様にポップアップのメニューを表示するようにする。
メニューは UIMenuController
を使う事で標準UIのポップアップメニューを表示できる。
まず、ラベルをタップした時にメニューが表示される様にする。
UITapGestureRecognizer
を使う方法もあるけど UILabel
のサブクラスを作るので簡易に
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
このあたりをオーバーライドしてタッチ操作の終わりを検出する事にする。
初期化
まず UILabel
はデフォルトでは userInteractionEnabled = NO
でタッチ操作を受け付けないので initWithFrame:
をオーバーライドして初期化時にデフォルトで userInteractionEnabled = NO
にしてタッチ操作を受け付ける様にしておく。
- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.userInteractionEnabled = YES; } return self; }
この時、別のイニシャライザの initWithCoder:
はあえてオーバーライドしなくて良いと思う。
このラベルクラスを InterfaceBuilder (以下IB) で配置した際には initWithFrame:
ではなくて initWithCoder:
が呼ばれるが、ビュークラスはIB上で userInteractionEnabled
のオンオフを切り替える事が出来るので initWithCoder:
はあえてオーバーライドしない事でIBで設定した userInteractionEnabled
の設定を引き継ぐ(変えない)様にする事が出来る。
IBで userInteractionEnabled = NO
にすればコピーメニューが表示されなく YES にすればコピーメニューが表示されるラベルクラスになる。
タッチ操作のオーバーライドと firstResponder
UITextView
UITextField
の挙動を見ているとタップ操作でも長押し操作でも完了時にメニューを表示し、ビュー内でタッチを開始したらビューの外でタッチを離してもメニュー表示が発動するという挙動だったので特にタッチ位置の判定などはせずに以下の様にタッチの終わりだけを検出する。
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self becomeFirstResponder]; }
タッチを検出したら becomeFirstResponder
を呼んで firstResponder になる様にして UIMenuController
を表示する準備をする。
また UILabel
はデフォルトでは firstResponder になる事が出来ないので canBecomeFirstResponder
をオーバーライドして firstResponder になれる様にしておく。
- (BOOL)canBecomeFirstResponder { return YES; }
メニューの表示
メニューは表示位置を指定してから setMenuVisible:YES
を呼ぶ。
UIMenuController *menuController = [UIMenuController sharedMenuController]; [menuController setTargetRect:self.bounds inView:self]; [menuController setMenuVisible:YES animated:YES];
また、
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
メソッドをオーバーライドしてどのメニュー項目に対応できるのかを実装しておく。
UILabel
の場合はもともとメニュー表示に対応していないのでこのメソッドのオーバーライドを忘れるとメニューが表示されない。
今回はコピーメニューを表示したいので copy:
メソッドを有効にして、他は super に投げておく。super には投げずに return NO;
でも良い。
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(copy:)) { return YES; } return [super canPerformAction:action withSender:sender]; // return NO; }
ここまででひとまずラベルをタッチしてコピーメニューを表示する事ができる。
メニューの動作
今のままではコピーメニューを選択した時に unrecognized selector sent to instance
でクラッシュしてしまうのでコピーメソッドを実装する。
コピーは単にペーストボードにラベルのテキストを渡してやればいい。
- (void)copy:(id)sender { [UIPasteboard generalPasteboard].string = self.text; }
一応ここまでで最低限は完成。
以下ではもうちょっと動きを良くしていく。
メニューの表示を改善
UITextView
UITextField
の挙動を見るとタッチ完了でメニューを表示、メニューが表示された状態でタッチ完了だとメニューを非表示という動きになっているのでこれを真似る。
UIMenuController *menuController = [UIMenuController sharedMenuController]; if (menuController.menuVisible) { [menuController setMenuVisible:NO animated:YES]; } else { [menuController setTargetRect:self.bounds inView:self]; [menuController setMenuVisible:YES animated:YES]; }
これで同じ箇所をタッチする度にメニューが表示/非表示となり標準UIの動作に近づく。
メニュー表示位置の調整
今メニューに設定している targetRect だとラベルの全体の中心位置にメニューが表示されてしまう。
ラベルに背景色が付いていれば違和感は少ないかも知れないけど、ラベルの背景色がラベルの乗っているビューと同じ背景色だと変な箇所にメニューが表示されているように見える。
これは targetRect を以下のように計算してやる事でテキストの表示位置に合わせてメニューを表示する事が出来るようになる。
CGRect rect = [self textRectForBounds:self.bounds limitedToNumberOfLines:self.numberOfLines]; [menuController setTargetRect:rect inView:self];
中央寄せや右寄せの alignment の設定を行った時にもバッチリ文字部分にメニューが表示される。
共有メニュー
コピーできるんだったら共有メニューもついでに表示させたいと思うかもしれない。
共有メニューは _share:
という Private メソッドの扱いのようだけど、一応対応する事は出来る。
まず対応するアクションの指定を以下のように変更
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(copy:) || action == @selector(_share:)) { return YES; } return [super canPerformAction:action withSender:sender]; }
そして _share:
メソッドを実装する。
共有のアクションは UIActivityViewController
を使ってシステムの挙動を呼び出すだけでOK。
UIActivityViewController
は表示する ViewController が必要になるので self.window.rootViewController
で辿ったり、はたまた UIApplicationDelegate
経由で辿ったりして ViewController を探して使うなどする。
- (void)_share:(id)sender { UIActivityViewController *viewController = [[UIActivityViewController alloc] initWithActivityItems:@[self.text] applicationActivities:nil]; [self.window.rootViewController presentViewController:viewController animated:YES completion:nil]; // [[[UIApplication sharedApplication].delegate window].rootViewController presentViewController:viewController animated:YES completion:nil]; }
これで以下のようにシステムの共有メニューの表示と実行が可能になる。
まとめ
コピー可能な UILabel