41
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

iPadOS 13.4のポインタに対応する

2020年3月にiPadOS 13.4がリリースされ、ポインタが導入されました。iPadにマウスやトラックパッドを接続(有線もしくは無線)して利用できます。

対応端末

すべてのiPad Proモデル
iPad Air 2以降
iPad(第5世代)以降
iPad mini 4以降

4~5年前のモデルでも対応しており、幅広い端末で利用できます。

Magnetic Effect

ボタンの上にポインタを移動させると、ボタンに飛びつくようなアニメーションとともに、ポインタがボタンの形に変化します。またポインタが離れる際には、ボタンにしがみつくようなアニメーションがあり、これらは「Magnetic Effect」と呼ばれています。
78849069-70661f00-7a4e-11ea-8e03-ada6cdf77bea.gif

エフェクトは3種類

ポインタがボタンの上に移動した時のエフェクトは3種類用意されています。

Highlight: ポインタがボタンの形に変わります。ツールバーのアイコンやメニューなどで使われています。
78849152-9d1a3680-7a4e-11ea-9150-a4bc9296e97e.gif

Lift: ボタンが拡大され、ポインタは非表示になります。ホーム画面のアイコンで使われています。
78849165-a3a8ae00-7a4e-11ea-839d-661a3a44ab22.gif

Hover: オーバーレイ(もしくはアンダーレイ)が表示されます。他にもオプションでボタンを拡大したり、シャドーをつけたり、ポインタの形を変えたりする事も可能です。カスタマイズすることで様々なエフェクトを実装できます。
78849173-aacfbc00-7a4e-11ea-93ad-8f3cae3cc417.gif

ポインタの形は自由

ポインタの形はUIBezierPathで表現できる形であれば対応しています。例えばKeynoteではテキストをリサイズする時にポインタの形が変わります。
78849227-d2bf1f80-7a4e-11ea-989b-e476c3a26791.gif

UIPointerInteraction API

ポインタ対応にはUIPointerInteraction APIを使います。

ただし、UIKitではUIButtonUIBarButtonItemUISegmentedControlUIMenuControllerなどがすでにポインタに対応しています。例えばUIButtonをポインタに対応させるためには、以下のコードを実行します。

if #available(iOS 13.4, *) {
    button.isPointerInteractionEnabled = true
}

デフォルトではHighlightエフェクトが適用されますが、別のエフェクトやポインタの形を適用させたい場合はpointerStyleProviderを使います。

if #available(iOS 13.4, *) {
    button.pointerStyleProvider = { button, effect, shape in
        if case let .roundedRect(frame, radius) = shape {
            // デフォルトよりもハイライトエリアを3ptsずつ広げる
            let rect = CGRect(x:frame.origin.x-3, 
                              y:frame.origin.y-3, 
                              width:frame.width+6, 
                              height:frame.height+6)
            return UIPointerStyle(effect: effect, shape: .roundedRect(rect, radius: radius))
        }
        return nil
    }
} 

その他のビューでポインタに対応するには、UIPointerInteractionを登録します。使い方はドラッグ&ドロップを実装するためのUIDragInteraction/UIDropInteractionや、コンテキストメニューを実装するためのUIContextMenuInteractionと同様です。

class TitleView: UIView {

    private func setupUI() {
        ...
        if #available(iOS 13.4, *) {
            enablePointer()
        }
    }

    ....
}

@available(iOS 13.4, *)
extension TitleView: UIPointerInteractionDelegate {

    func enablePointer() {
        isUserInteractionEnabled = true
        addInteraction(UIPointerInteraction(delegate: self))
    }

    func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
        // ポインタが反応すべきframeを返す
        return UIPointerRegion(rect: rectForTitle())
    }

    private func rectForTitle() -> CGRect {
        // AttributedTextが表示されているrectを返す
        return ...
    }

    func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
        // ポインタのエフェクト(UIPointerEffect)や形(UIPointerShape)を返す
        let targetedPreview = UITargetedPreview(view: self)
        let effect: UIPointerEffect = .highlight(targetedPreview)
        let shape: UIPointerShape = .roundedRect(rectForTitle(), radius: UIPointerShape.defaultCornerRadius)
        let pointerStyle = UIPointerStyle(effect: effect, shape: shape)
        return pointerStyle
    }
}

実際の動きは以下のようになります。UIViewでもちゃんとポインタが反応するようになりました。
ezgif-6-a28da0dbee9e.gif

上記の例ではHighlightエフェクトを利用していますが、例えばHoverエフェクトでオーバーレイを表示させたい場合はpointerInteraction(_:styleFor:)を次のように実装します。

@available(iOS 13.4, *)
extension TitleView: UIPointerInteractionDelegate {

    func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
        let targetedPreview = UITargetedPreview(view: self)
        targetedPreview.parameters.visiblePath = UIBezierPath(roundedRect: rectForTitle(), cornerRadius: 10.0)
        // Hoverエフェクトを利用する
        let effect: UIPointerEffect = .hover(targetedPreview, preferredTintMode: .overlay, prefersShadow: false, prefersScaledContent: false)
        let pointerStyle = UIPointerStyle(effect: effect)
        return pointerStyle
    }
}

動きはこのようになります。Highlightと違って、Hoverではポインタの形は変わらず、オーバーレイのみが表示されます。Magnetic Effectも適用されません。
ezgif-6-a9d7c3f33eae.gif
もしHighlight, Lift, Hover以外のまったく違うエフェクトを適用したい場合は、UIPointerInteractionDelegatepointerInteraction(_:willEnter:animator:)/pointerInteraction(_:willExit:animator:)を使って独自のアニメーションを実装できます。

@available(iOS 13.4, *)
extension TitleView: UIPointerInteractionDelegate {

    func pointerInteraction(_ interaction: UIPointerInteraction, willEnter region: UIPointerRegion, animator: UIPointerInteractionAnimating) {
        // ポインタがビューの上に移動した時に呼ばれる
        showEventResizingHandler(true)
    }

    func pointerInteraction(_ interaction: UIPointerInteraction, willExit region: UIPointerRegion, animator: UIPointerInteractionAnimating) {
        // ポインタがビューの外に移動した時に呼ばれる
        animator.addAnimations {
            self.showEventResizingHandler(false)
        }
    }
}

拙作カレンダーアプリでは予定にポインタを移動させるとハンドラーが表示され、それをドラッグする事で予定の長さを変更する、という機能の実装に利用しています。
ezgif-6-49bffc550953.gif
最後に、指とポインタによる入力を区別したい場合があります。例えば上記の「予定の端をドラッグして、予定の長さを変える」という機能ですが、ポインタを利用している時には便利な機能ですが、指を使って予定の端をドラッグするというのは至難の技です。また、そもそも指で操作している時にはリサイズのためのハンドラーが表示されないという問題もあります。

このような場合、Info.plistUIApplicationSupportsIndirectInputEventsキーを追加することで、UITouch.typeの値が指の場合はUITouch.TouchType.direct、ポインタの場合はUITouch.TouchType.indirectとなり、指とポインタを区別する事ができます。

class CalendarView: UIView {

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if #available(iOS 13.4, *), touches.first?.type == .some(.indirectPointer) {
            // ポインタによる操作
            ...
        } else {
            // 指による操作
            ...
        }
    }
}

ただしこのキーを追加すると、例えばカスタムのUIGestureRecognizerを実装している場合、UIGestureRecognizer.numberOfTouchesが0になるケースがあり、その結果UIGestureRecognizer.location(ofTouch:in:)でクラッシュが発生するなどの副作用するため、アプリを再度テストする必要があります。詳しくはこちらのドキュメントをご覧ください。

参考

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
Sign upLogin
41
Help us understand the problem. What are the problem?