【问题标题】:3D touch/Force touch implementation3D 触摸/强制触摸实现
【发布时间】:2015-09-24 22:27:44
【问题描述】:

我们如何实现 3D 触摸来检查用户是否点击UIView 或强制触摸UIView

有没有办法使用UIGestureRecognize 或仅使用UITouch 来做到这一点?

【问题讨论】:

    标签: ios swift 3dtouch


    【解决方案1】:

    您可以在没有指定手势识别器的情况下执行此操作。您无需调整 touchesEnded 和 touchesBegan 方法,只需调整 touchesMoved 即可获得正确的值。从开始/结束获取 uitouch 的力量将返回奇怪的值。

    UITouch *touch = [touches anyObject];
    
    CGFloat maximumPossibleForce = touch.maximumPossibleForce;
    CGFloat force = touch.force;
    CGFloat normalizedForce = force/maximumPossibleForce;
    

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

    【讨论】:

      【解决方案2】:

      3D Touch 属性are available on UITouch objects

      您可以通过覆盖UIViewtouchesBegan:touchesMoved: 方法来实现这些功能。不确定你在touchesEnded: 看到了什么。

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

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

      【讨论】:

      • 我想检查用户是否点击 UIView 或强制触摸 UIView
      • UITapGestureRecognizer 尚未针对 3D Touch 进​​行更新,因此您必须创建自己的 UIGestureRecognizer 子类或子类视图并处理 touchesBegantouchesMoved
      • 如果触摸不动但力度发生变化,touchesMoved: 会被调用吗?
      【解决方案3】:

      在 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
              }
          }
      
      }
      

      来源:

      【讨论】:

        【解决方案4】:

        我创建了一个模拟 Apple Mail 应用程序行为的 UIGestureRecognizer。在 3D 触摸时,它以一个短的单脉冲振动开始,然后是一个可选的辅助动作 (hardTarget),并在初始按下后不久通过硬按下调用脉冲。

        改编自https://github.com/FlexMonkey/DeepPressGestureRecognizer

        变化:

        • 3D 触摸振动脉冲类似于 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)
                }
            }
        }
        

        【讨论】:

        • 将振动代码放入识别器可能是架构错误。
        • 仅适用于 iOS 15,不适用于以前的 iOS 版本,如 14、13、12. ....
        【解决方案5】:

        我这样做的方式是结合使用UITapGestureRecognizer(Apple 提供)和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];
            });
        }
        

        实现强制触摸的委托协议:

        #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 上的新 force 属性,仅在 iOS 9 中可用。

        如果您在 iOS 8 上添加它会崩溃,因此如果您支持的版本早于 iOS 9,请根据您运行的 iOS 版本有条件地添加此识别器。

        【讨论】:

        • 哇,谢谢!你能把你的答案转换成 Swift 2.0 吗?而且我的应用支持 iOS 8 - 9 所以我不需要添加 DFContinuousForceTouchGestureRecognizer 来查看
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-18
        相关资源
        最近更新 更多