ユーザーがUIView
をタップしたか、UIView
を強制的にタッチしたかを確認するために、3Dタッチを実装するにはどうすればよいですか?
UIGestureRecognize
でこれを行う方法はありますか、それともUITouch
でのみですか?
指定されたジェスチャレコグナイザーなしで実行できます。 touchesEndedメソッドとtouchesBeganメソッドを調整する必要はありませんが、正しい値を取得するにはtouchesMovedを調整するだけです。 begin/endedからuitouchの力を取得すると、奇妙な値が返されます。
UITouch *touch = [touches anyObject];
CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;
次に、力のしきい値を設定し、normalizedForceをこのしきい値と比較します(0.75は問題ないようです)。
3D Touchプロパティ UITouch
オブジェクトで利用可能 。
UIView
のtouchesBegan:
およびtouchesMoved:
メソッドをオーバーライドすることにより、これらのタッチを取得できます。 touchesEnded:
に何が表示されるかまだわかりません。
新しいジェスチャレコグナイザーを作成する場合は、UITouch
で公開されているUIGestureRecognizerSubclass
esに完全にアクセスできます。
従来のUIGestureRecognizer
で3Dタッチプロパティをどのように使用できるかわかりません。たぶんUIGestureRecognizerDelegate
プロトコルのgestureRecognizer:shouldReceiveTouch:
メソッドを介して。
Swift 4.2およびiOS 12の場合、問題を解決するための可能な方法は、Force Touchを処理するUIGestureRecognizer
のカスタムサブクラスを作成し、UITapGestureRecognizer
。次の完全なコードは、その実装方法を示しています。
ViewController.Swift
import UIKit
class ViewController: UIViewController {
let redView = UIView()
lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler))
lazy var forceTouchGestureRecognizer = ForceTouchGestureRecognizer(target: self, action: #selector(forceTouchHandler))
override func viewDidLoad() {
super.viewDidLoad()
redView.backgroundColor = .red
redView.addGestureRecognizer(tapGestureRecognizer)
view.addSubview(redView)
redView.translatesAutoresizingMaskIntoConstraints = false
redView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.forceTouchCapability == UIForceTouchCapability.available {
redView.addGestureRecognizer(forceTouchGestureRecognizer)
} else {
// When force touch is not available, remove force touch gesture recognizer.
// Also implement a fallback if necessary (e.g. a long press gesture recognizer)
redView.removeGestureRecognizer(forceTouchGestureRecognizer)
}
}
@objc func tapHandler(_ sender: UITapGestureRecognizer) {
print("Tap triggered")
}
@objc func forceTouchHandler(_ sender: ForceTouchGestureRecognizer) {
UINotificationFeedbackGenerator().notificationOccurred(.success)
print("Force touch triggered")
}
}
ForceTouchGestureRecognizer.Swift
import UIKit.UIGestureRecognizerSubclass
@available(iOS 9.0, *)
final class ForceTouchGestureRecognizer: UIGestureRecognizer {
private let threshold: CGFloat = 0.75
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
handleTouch(touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
handleTouch(touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = UIGestureRecognizer.State.failed
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = UIGestureRecognizer.State.failed
}
private func handleTouch(_ touch: UITouch) {
guard touch.force != 0 && touch.maximumPossibleForce != 0 else { return }
if touch.force / touch.maximumPossibleForce >= threshold {
state = UIGestureRecognizer.State.recognized
}
}
}
ソース:
Apple Mailアプリの動作をエミュレートするUIGestureRecognizerを作成しました。3Dタッチすると、短い単一のPulseバイブレーションで開始し、オプションのセカンダリアクション(hardTarget)とハードプレスで呼び出されるPulse最初のプレスの直後。
https://github.com/FlexMonkey/DeepPressGestureRecognizer から適応
変更点:
注:文書化されていないシステムサウンドk_PeakSoundIDを追加しましたが、文書化された範囲を超える定数を使用して不快な場合は、気軽にオフにしてください。私は何年もの間、非公開の定数を持つシステムサウンドを使用していますが、vibrateOnDeepPressプロパティを使用して振動パルスをオフにすることを歓迎します。
import UIKit
import UIKit.UIGestureRecognizerSubclass
import AudioToolbox
class DeepPressGestureRecognizer: UIGestureRecognizer {
var vibrateOnDeepPress = true
var threshold: CGFloat = 0.75
var hardTriggerMinTime: TimeInterval = 0.5
var onDeepPress: (() -> Void)?
private var deepPressed: Bool = false {
didSet {
if (deepPressed && deepPressed != oldValue) {
onDeepPress?()
}
}
}
private var deepPressedAt: TimeInterval = 0
private var k_PeakSoundID: UInt32 = 1519
private var hardAction: Selector?
private var target: AnyObject?
required init(target: AnyObject?, action: Selector, hardAction: Selector? = nil, threshold: CGFloat = 0.75) {
self.target = target
self.hardAction = hardAction
self.threshold = threshold
super.init(target: target, action: action)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if let touch = touches.first {
handle(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if let touch = touches.first {
handle(touch: touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = deepPressed ? UIGestureRecognizerState.ended : UIGestureRecognizerState.failed
deepPressed = false
}
private func handle(touch: UITouch) {
guard let _ = view, touch.force != 0 && touch.maximumPossibleForce != 0 else {
return
}
let forcePercentage = (touch.force / touch.maximumPossibleForce)
let currentTime = Date.timeIntervalSinceReferenceDate
if !deepPressed && forcePercentage >= threshold {
state = UIGestureRecognizerState.began
if vibrateOnDeepPress {
AudioServicesPlaySystemSound(k_PeakSoundID)
}
deepPressedAt = Date.timeIntervalSinceReferenceDate
deepPressed = true
} else if deepPressed && forcePercentage <= 0 {
endGesture()
} else if deepPressed && currentTime - deepPressedAt > hardTriggerMinTime && forcePercentage == 1.0 {
endGesture()
if vibrateOnDeepPress {
AudioServicesPlaySystemSound(k_PeakSoundID)
}
//fire hard press
if let hardAction = self.hardAction, let target = self.target {
_ = target.perform(hardAction, with: self)
}
}
}
func endGesture() {
state = UIGestureRecognizerState.ended
deepPressed = false
}
}
// MARK: DeepPressable protocol extension
protocol DeepPressable {
var gestureRecognizers: [UIGestureRecognizer]? {get set}
func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
func removeGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
func setDeepPressAction(target: AnyObject, action: Selector)
func removeDeepPressAction()
}
extension DeepPressable {
func setDeepPressAction(target: AnyObject, action: Selector) {
let deepPressGestureRecognizer = DeepPressGestureRecognizer(target: target, action: action, threshold: 0.75)
self.addGestureRecognizer(gestureRecognizer: deepPressGestureRecognizer)
}
func removeDeepPressAction() {
guard let gestureRecognizers = gestureRecognizers else { return }
for recogniser in gestureRecognizers where recogniser is DeepPressGestureRecognizer {
removeGestureRecognizer(gestureRecognizer: recogniser)
}
}
}
これを行う方法は、 ITapGestureRecognizer (Apple提供)と DFContinuousForceTouchGestureRecognizer (私提供)の組み合わせを使用することです。
DFContinuousForceTouchGestureRecognizer
は、単一のイベントとは対照的に、圧力の変化に関する継続的な更新を提供するため、ユーザーが圧力を変化させるときにビューを拡大するなどのことができるので便利です。単一のイベントだけが必要な場合は、- (void) forceTouchRecognized
コールバックを除くDFContinuousForceTouchDelegate
のeveythingを無視できます。
https://github.com/foggzilla/DFContinuousForceTouchGestureRecognizer
これをダウンロードして、強制プレスをサポートしているデバイスでサンプルアプリを実行し、感じ方を確認できます。
UIViewController
に次を実装します。
- (void)viewDidLoad {
[super viewDidLoad];
_forceTouchRecognizer = [[DFContinuousForceTouchGestureRecognizer alloc] init];
_forceTouchRecognizer.forceTouchDelegate = self;
//here to demonstrate how this works alonside a tap gesture recognizer
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
[self.imageView addGestureRecognizer:_tapGestureRecognizer];
[self.imageView addGestureRecognizer:_forceTouchRecognizer];
}
タップジェスチャのセレクターを実装する
#pragma UITapGestureRecognizer selector
- (void)tapped:(id)sender {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:@"Tap" message:@"YEAH!!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
});
}
強制タッチのデリゲートプロトコルを実装します。
#pragma DFContinuousForceTouchDelegate
- (void)forceTouchRecognized:(DFContinuousForceTouchGestureRecognizer *)recognizer {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:@"Force Touch" message:@"YEAH!!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
});
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didStartWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
[self.imageView setNeedsDisplay];
}
- (void) forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didMoveWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
CGFloat transformDelta = 1.0f + ((force/maxForce) / 3.0f);
self.imageView.transform = CGAffineTransformMakeScale(transformDelta, transformDelta);
[self.imageView setNeedsDisplay];
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didCancelWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
- (void)forceTouchRecognizer:(DFContinuousForceTouchGestureRecognizer *)recognizer didEndWithForce:(CGFloat)force maxForce:(CGFloat)maxForce {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
- (void)forceTouchDidTimeout:(DFContinuousForceTouchGestureRecognizer *)recognizer {
self.imageView.transform = CGAffineTransformIdentity;
[self.imageView setNeedsDisplay];
}
これは、強制タッチをサポートするデバイスでのみ役立つことに注意してください。
また、iOS 8以下で実行している場合は、DFContinuousForceTouchGestureRecognizer
で新しいforce
プロパティを使用するため、iOS 9でのみ使用可能な場合、UITouch
をビューに追加しないでください。
これをiOS 8に追加するとクラッシュするため、iOS 9よりも古いバージョンをサポートしている場合は、実行しているiOSバージョンに基づいてこの認識機能を条件付きで追加します。