3D Touch/Force Touch 实现

38

如何实现3D Touch检测用户是在UIView上轻敲还是强力触摸?

是否可以使用UIGestureRecognizer或仅使用UITouch完成此操作?

5个回答

21

你可以不使用指定的手势识别器完成它。你不需要调整 touchesEnded 和 touchesBegan 方法,只需调整 touchesMoved 来获取正确的值即可。从 began/ended 获取 uitouch 的力度将返回奇怪的值。

UITouch *touch = [touches anyObject];

CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;

然后,设置一个力阈值,并将normalizedForce与该阈值进行比较(0.75对我来说似乎不错)。


9

3D Touch属性可用于UITouch对象

您可以通过重写UIViewtouchesBegan:touchesMoved:方法来获取这些触摸。还不确定在touchesEnded:中看到了什么。

如果您愿意创建新的手势识别器,您完全可以访问在UIGestureRecognizerSubclass中公开的UITouches。

我不确定如何在传统的UIGestureRecognizer中使用3D touch属性。也许可以通过UIGestureRecognizerDelegate协议的gestureRecognizer:shouldReceiveTouch:方法实现。


我想要检查用户是在UIView上轻点还是在UIView上使用了3D Touch。 - Avi Rok
UITapGestureRecognizer 没有针对 3D Touch 进行更新,因此您需要创建自己的 UIGestureRecognizer 子类或子类化视图并处理 touchesBegantouchesMoved - Rhythmic Fistman
2
如果触摸不移动但力量发生变化,touchesMoved:方法会被调用吗? - Warpling

9
使用Swift 4.2和iOS 12,解决问题的一种可能方法是创建一个自定义的子类UIGestureRecognizer来处理Force Touch,并将其添加到您的视图中,旁边还有一个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
        }
    }

}

参考资料:


7
我创建了一个UIGestureRecognizer,它模仿了苹果邮件应用的行为。在 3D Touch 操作时,它会开始进行短暂的单脉冲振动,然后是可选的第二个操作(hardTarget)和由初始按压后不久的硬按触发的脉冲。
改编自 https://github.com/FlexMonkey/DeepPressGestureRecognizer 更改如下:
  • 3D Touch 操作时振动的脉冲与 iOS 系统行为类似
  • 与 Apple 邮件应用一样,必须松开触摸才能结束
  • 阈值默认为系统默认级别
  • 硬触摸触发 hardAction 调用,类似于邮件应用
注意:我添加了未记录的系统声音 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)
        }
    }
}

1
把振动代码放在识别器中可能是一个架构上的错误。 - kelin
仅适用于 iOS 15,不适用于之前的 iOS 版本,例如 14、13、12 等。 - Haseeb Javed

6
我正在使用Apple提供的UITapGestureRecognizer和我提供的DFContinuousForceTouchGestureRecognizer组合来完成此操作。

DFContinuousForceTouchGestureRecognizer非常好用,因为它会持续更新压力变化,所以您可以在用户变化压力时对视图进行增强,而不是仅在单个事件中进行。如果您只需要单个事件,则可以忽略DFContinuousForceTouchDelegate中除了- (void) forceTouchRecognized回调之外的所有内容。

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];
    });
}

实现力触(delegate protocol)的协议:

#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添加到视图中,因为它使用UITouch上仅在iOS 9中可用的新的force属性。
如果您在iOS 8上添加此属性,应用程序将崩溃,因此请根据您正在运行的iOS版本有条件地添加此识别器,如果您支持早于iOS 9的版本。

哇,谢谢!你能把你的答案转换成Swift 2.0吗?我的应用支持iOS 8-9,所以我不需要在视图中添加DFContinuousForceTouchGestureRecognizer。 - Avi Rok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接