「集まれSwift好き!Swift愛好会 vol12」http://love-swift.connpass.com/event/41463/
「集まれSwift好き!Swift愛好会 vol11」http://love-swift.connpass.com/event/39087/
上の二つの会で話した内容のまとめ.
説明に使うサンプルコード.
https://github.com/sonsongithub/UZTextView/tree/master/Sample3DTouch
※Sample3DTouchプロジェクトが以下の説明のアプリケーションのソースコードです.選択したり,リンクをタップできるビューUZTextViewのリポジトリに入れてあります.
3D Touchの実装方法
3D Touchは,大きく分けて三つ利用する方法がある.
- ホーム画面でアプリのアイコンを強く押すときのメニューを実装する
-
UIViewController
に用意されたAPIを使う -
UITouch
から値をダイレクトに取得して自分ですべて実装する
特に(1)は特段難しいことはない(気がする).一方で,(3)は気軽に実装すると,未来にAPIの仕様がかわって泣く,あるいは,実装しているうちに何を作ってるのかわからなくなるパターンなので手を出さない(ことにする).となると,UIViewController
に実装された(2)を利用するのがよさそうだ.
というわけで,今回は(2)を説明してみる.
処理の流れ
UIViewController
で3D Touchを使いたい場合は,まず,反応するビューを以下のUIViewController
のメソッドで登録する.
func registerForPreviewing(with delegate: UIViewControllerPreviewingDelegate,
sourceView: UIView) -> UIViewControllerPreviewing
このメソッドは,viewDidLoad
のイベントで実装するのが良さそうである.
override func viewDidLoad() {
super.viewDidLoad()
// other process
self.registerForPreviewing(with: self, sourceView: self.view)
}
これでview controllerのview
が3D Touchに反応するようになる.ただし,このdelegate先となるオブジェクトは,下に示す二つのUIViewControllerPreviewingDelegate
プロトコルを実装する必要がある.
// 表示するview controllerを作成して,ランタイムに返す
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
// 表示されているview controllerが強くおされて,peekが終了するときに呼ばれる.
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
そして,処理の流れは,下のようになる.
- 3D Touchイベントが発生すると,
previewingContext(_. location)
が呼ばれる. - このcallbackの中で,
UIViewController
の配下のビューに,3D Touchで表示させるデータとそのビューコントローラがあるかを問い合わせる.適当なデータがない場合(変な場所を3D Touchした場合も)には,previewingContext(_. location)
はnilを返せば,3D Touchによって何も表示されないようになる.この時.表示されるビューをpeek viewと呼ぶ.また,この強く押して,プレビューすることをpeekと呼ぶみたいだ(peekは「覗く」の意). - 次にユーザが3D Touchで開いたビューをさらに強く押し込んだ場合,
previewingContext(_, commit:)
が呼ばれる.このcommit
には,previewingContext(_. location)
で生成したview controllerが入っている.もし,表示されたビューがUINavigationController
で管理されるdrill downのビューであれば,UINavigationController
のpushを実行したり,あるいあ表示されたビューはmodalで表示されるべきであれば,presentViewControllerをつかって,modal view controllerを表示すればよい.
これが大まかな処理の流れとなる.よくデザインされているように見える.
コード
実際のUIViewControllerPreviewingDelegate
プロトコルで実装するコードを見てみる.
まず,3D Touchが押された場所で表示する情報があるかを判定する.
サンプルコードでは,getInfo
というメソッドは,その場所に含まれるURLとその領域を返し,その戻り値がnil
でないとき,そのURLをWebViewController
に設定し,ランタイムに返すようになっている.
func previewingContext(_ previewingContext: UIViewControllerPreviewing,
viewControllerForLocation location: CGPoint) -> UIViewController? {
let locationInTextView = self.view.convert(location, to: textView)
if let (url, rect) = getInfo(locationInTextView: locationInTextView) {
previewingContext.sourceRect = self.view.convert(rect, from: textView)
let controller = WebViewController(nibName: nil, bundle: nil)
controller.url = url
return controller
}
return nil
}
さらに,ユーザが強く押し込んだ時は,上のコードで作成したWebViewController
をUINavigationController
のルートに設定し,modalで表示するようにしている.
このようにして,peek状態の時と,最終的にちゃんと開いた時のview controllerの状態を分けてコーディングすることができる.
これは,なかなかよくできている.
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
let nav = UINavigationController(rootViewController: viewControllerToCommit)
self.present(nav, animated: true, completion: nil)
}
以上で基本的な3D touchの実装は終わりだが,よりかっこいいUIを実現するために突っ込んだところをみていこう.
ぼかす領域
上の動画の例でわかるように,3D touchは押し込んだ時に反応した部分だけをぼかさないようにして,次のpeek状態へアニメーションさせることができる.上の例では,強く押し込んだ"Let's Enctypt"のリンクのところだけがぼかされず,拡大されるようアニメーションすることがわかる.この効果を得るためには,previewingContext(_. location)
の中で,UIViewControllerPreviewing
のsourceRect
に正しい領域を設定する必要がある.このとき,UIView
のメソッドであるconvert
を使って,ぼかす領域をセットする.UIViewControllerPreviewing.sourceRect
は,registerForPreviewing
でセットしたviewの座標系で表さなければならない.convert
メソッドの動作イメージは,下図を例にするとb.frame
が(200, 50, 50, 60)
の値であるとき,convert
を使って,b.frame
をビューa
からみた時の値にconvert
を使って変換することができる.
// fromの場合
let f_from = a.convert(b.frame, from: c)
print(f_from) => (400, 70 50, 60)
// toの場合
let f_to = c.convert(b.frame, to: a)
print(f_to) => (400, 70 50, 60)
このconvert
メソッドは,アニメショーンさせる時に役立つ.
今回の例では,
previewingContext.sourceRect = self.view.convert(rect, from: textView)
の部分では,ぼかす領域をtextView
の座標から,self.view
の座標に変換している.
サブメニュー
3D touchをした後に表示させるUIViewController
に,メニューアイテムを実装しておくと,強く押して表示されるpeek viewを上にドラッグした時にメニューを表示させることができる.UIViewController
は,3D touch用にpreviewActionItems
というプロパティが追加されており,このプロパティが,UIPreviewActionItem
の配列を返すと,このサブメニューが表示されるようになる.UIPreviewActionItem
は,階層構造を作ることもできる,実際のコードを見てみる.
override var previewActionItems : [UIPreviewActionItem] {
get {
func previewActionForTitle(_ title: String, style: UIPreviewActionStyle = .default) -> UIPreviewAction {
return UIPreviewAction(title: title, style: style) { previewAction, viewController in
print(title)
}
}
let action1 = previewActionForTitle("Action1")
let action2 = previewActionForTitle("Destructive Action", style: .destructive)
let subAction1 = previewActionForTitle("Sub1")
let subAction2 = previewActionForTitle("Sub2")
let groupedActions = UIPreviewActionGroup(title: "Sub Actions", style: .default, actions: [subAction1, subAction2] )
return [action1, action2, groupedActions]
}
}
各アイテムを選んだ時のアクションは,ブロックで実装する.
まとめ
3D touchの肝は,
- 準備
- 強く押した時のpeek表示,ぼかさない領域の設定
- さらに押し込んだ時の対応
- メニューを用意する時は,
previewActionItems
を使う
となる.
タップされた場所に対応するコンテンツを用意に用意できる(例えばUIImageViewとか,リンクとか)ならば,サポートするのは簡単なので,実装することをオススメする.
テスト環境
テストするためには,やはり3D touchをサポートするiPhone6s以降のiPhoneの実機を使うことが望ましいが,感圧タッチトラックパッドを備えるMacBook,あるいあMagicTrackPad2があれば,それをつかってシミュレータでテストすることもできる.設定はメニューから,Hardware→Touch pressure→Use Trackpad Forceにチェックをつければ良い.