ios - library - xcode developer




Aplicación 3D touch/Force touch (4)

¿Cómo podemos implementar 3D touch para verificar si el usuario toca en UIView o forzar el toque en UIView ?

¿Hay alguna manera de hacer esto con UIGestureRecognize o solo con UITouch ?


Con Swift 4.2 y iOS 12, una posible forma de resolver su problema es crear una subclase personalizada de UIGestureRecognizer que maneje Force Touch y agregarla a su vista junto a un UITapGestureRecognizer . El siguiente código completo muestra cómo implementarlo:

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

}

Fuentes:


Creé un UIGestureRecognizer que emula el comportamiento de la aplicación Apple Mail. Con el toque tridimensional, comienza con una breve vibración de pulso simple y luego una acción secundaria opcional (hardTarget) y un pulso llamado al presionar con fuerza poco después de la presión inicial.

Adaptado de https://github.com/FlexMonkey/DeepPressGestureRecognizer

Cambios:

  • 3D táctil vibra pulsos como el comportamiento del sistema iOS
  • debe tocarse para que finalice, como la aplicación de correo de Apple
  • el umbral predeterminado es el nivel predeterminado del sistema
  • el toque duro activa la llamada de hardAction como aplicación de correo

Nota: Agregué el sonido del sistema indocumentado k_PeakSoundID, pero no dude en apagarlo si no se siente cómodo usando una constante más allá del rango documentado . He estado usando sonidos del sistema con constantes no reveladas durante años, pero se te invita a que apagues los pulsos de vibración usando la propiedad 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)
        }
    }
}

Las propiedades 3D Touch están disponibles en objetos UITouch .

Puede obtener estos toques anulando los toques UIView un UIView de touchesBegan: y los métodos touchesMoved: . No estoy seguro de lo que ve en touchesEnded: todavía.

Si está dispuesto a crear nuevos reconocedores de gestos, tiene acceso completo a UITouch como se expone en UIGestureRecognizerSubclass .

No estoy seguro de cómo podría usar las propiedades táctiles 3D en un UIGestureRecognizer tradicional. Tal vez a través del UIGestureRecognizerDelegate gestureRecognizer:shouldReceiveTouch: del protocolo gestureRecognizer:shouldReceiveTouch: .


Puede hacerlo sin un reconocedor de gestos designado. No necesita ajustar el método touchesEnded y touchesBegan, sino simplemente los toquesMoved para obtener los valores correctos. obtener la fuerza de un uitouch desde el principio / terminado devolverá valores extraños.

UITouch *touch = [touches anyObject];

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

luego, establezca un umbral de fuerza y ​​compare la Fuerza normalizada con este umbral (0,75 parece estar bien para mí).





3dtouch