2020年3月にiPadOS 13.4がリリースされ、ポインタが導入されました。iPadにマウスやトラックパッドを接続(有線もしくは無線)して利用できます。
対応端末
すべてのiPad Proモデル
iPad Air 2以降
iPad(第5世代)以降
iPad mini 4以降
4~5年前のモデルでも対応しており、幅広い端末で利用できます。
Magnetic Effect
ボタンの上にポインタを移動させると、ボタンに飛びつくようなアニメーションとともに、ポインタがボタンの形に変化します。またポインタが離れる際には、ボタンにしがみつくようなアニメーションがあり、これらは「Magnetic Effect」と呼ばれています。
エフェクトは3種類
ポインタがボタンの上に移動した時のエフェクトは3種類用意されています。
Highlight:
ポインタがボタンの形に変わります。ツールバーのアイコンやメニューなどで使われています。
Lift:
ボタンが拡大され、ポインタは非表示になります。ホーム画面のアイコンで使われています。
Hover:
オーバーレイ(もしくはアンダーレイ)が表示されます。他にもオプションでボタンを拡大したり、シャドーをつけたり、ポインタの形を変えたりする事も可能です。カスタマイズすることで様々なエフェクトを実装できます。
ポインタの形は自由
ポインタの形はUIBezierPath
で表現できる形であれば対応しています。例えばKeynoteではテキストをリサイズする時にポインタの形が変わります。
UIPointerInteraction API
ポインタ対応にはUIPointerInteraction API
を使います。
ただし、UIKitではUIButton
、UIBarButtonItem
、UISegmentedControl
、UIMenuController
などがすでにポインタに対応しています。例えば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
でもちゃんとポインタが反応するようになりました。
上記の例では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も適用されません。
もしHighlight
, Lift
, Hover
以外のまったく違うエフェクトを適用したい場合は、UIPointerInteractionDelegate
のpointerInteraction(_: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)
}
}
}
拙作カレンダーアプリでは予定にポインタを移動させるとハンドラーが表示され、それをドラッグする事で予定の長さを変更する、という機能の実装に利用しています。
最後に、指とポインタによる入力を区別したい場合があります。例えば上記の「予定の端をドラッグして、予定の長さを変える」という機能ですが、ポインタを利用している時には便利な機能ですが、指を使って予定の端をドラッグするというのは至難の技です。また、そもそも指で操作している時にはリサイズのためのハンドラーが表示されないという問題もあります。
このような場合、Info.plist
にUIApplicationSupportsIndirectInputEvents
キーを追加することで、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:)
でクラッシュが発生するなどの副作用するため、アプリを再度テストする必要があります。詳しくはこちらのドキュメントをご覧ください。
参考
-
Pointer Interactions
https://developer.apple.com/documentation/uikit/pointer_interactions -
Human Interface Guideline: Pointers (iPadOS)
https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/pointers/ -
UIApplicationSupportsIndirectInputEvents
https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents -
Supporting Pointer Interactions
https://pspdfkit.com/blog/2020/supporting-pointer-interactions/