Help us understand the problem. What is going on with this article?

UIViewControllerに用意された3D Touchを利用する

More than 3 years have passed since last update.

「集まれ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は,大きく分けて三つ利用する方法がある.

  1. ホーム画面でアプリのアイコンを強く押すときのメニューを実装する
  2. UIViewControllerに用意されたAPIを使う
  3. 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)

そして,処理の流れは,下のようになる.

  1. 3D Touchイベントが発生すると,previewingContext(_. location)が呼ばれる.
  2. このcallbackの中で,UIViewControllerの配下のビューに,3D Touchで表示させるデータとそのビューコントローラがあるかを問い合わせる.適当なデータがない場合(変な場所を3D Touchした場合も)には,previewingContext(_. location)はnilを返せば,3D Touchによって何も表示されないようになる.この時.表示されるビューをpeek viewと呼ぶ.また,この強く押して,プレビューすることをpeekと呼ぶみたいだ(peekは「覗く」の意).
  3. 次にユーザが3D Touchで開いたビューをさらに強く押し込んだ場合,previewingContext(_, commit:)が呼ばれる.このcommitには,previewingContext(_. location)で生成したview controllerが入っている.もし,表示されたビューがUINavigationControllerで管理されるdrill downのビューであれば,UINavigationControllerのpushを実行したり,あるいあ表示されたビューはmodalで表示されるべきであれば,presentViewControllerをつかって,modal view controllerを表示すればよい.

これが大まかな処理の流れとなる.よくデザインされているように見える.

flow.001.jpeg

コード

実際の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
}

さらに,ユーザが強く押し込んだ時は,上のコードで作成したWebViewControllerUINavigationControllerのルートに設定し,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)の中で,UIViewControllerPreviewingsourceRectに正しい領域を設定する必要がある.このとき,UIViewのメソッドであるconvertを使って,ぼかす領域をセットする.UIViewControllerPreviewing.sourceRectは,registerForPreviewingでセットしたviewの座標系で表さなければならない.convertメソッドの動作イメージは,下図を例にするとb.frame(200, 50, 50, 60)の値であるとき,convertを使って,b.frameをビューaからみた時の値にconvertを使って変換することができる.

convert_sample.jpg

// 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の肝は,
1. 準備
2. 強く押した時のpeek表示,ぼかさない領域の設定
3. さらに押し込んだ時の対応
4. メニューを用意する時は,previewActionItemsを使う
となる.

タップされた場所に対応するコンテンツを用意に用意できる(例えばUIImageViewとか,リンクとか)ならば,サポートするのは簡単なので,実装することをオススメする.

テスト環境

テストするためには,やはり3D touchをサポートするiPhone6s以降のiPhoneの実機を使うことが望ましいが,感圧タッチトラックパッドを備えるMacBook,あるいあMagicTrackPad2があれば,それをつかってシミュレータでテストすることもできる.設定はメニューから,Hardware→Touch pressure→Use Trackpad Forceにチェックをつければ良い.

simulator.jpg

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away