ios - haptic - Aplicación 3D touch/Force touch
ios developer library (5)
¿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)
}
}
}
La forma en que lo hago es usar una combinación de un UITapGestureRecognizer (provisto por Apple) y un DFContinuousForceTouchGestureRecognizer (provisto por mí).
El DFContinuousForceTouchGestureRecognizer
es bueno porque proporciona actualizaciones continuas sobre los cambios de presión para que pueda hacer cosas como aumentar la vista a medida que el usuario varía su presión sobre ella, a diferencia de un solo evento. Si solo quieres un evento único, puedes ignorar todo en el DFContinuousForceTouchDelegate
excepto el - (void) forceTouchRecognized
callback.
DFContinuousForceTouchGestureRecognizer
Puede descargar esto y ejecutar la aplicación de muestra en un dispositivo que admita fuerza presione para ver cómo se siente.
En su UIViewController
implemente lo siguiente:
- (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];
}
implementar selector para gesto de toque
#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];
});
}
Implementar el protocolo delegado para force touch:
#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];
}
Tenga en cuenta que esto solo será útil en un dispositivo que admita fuerza táctil.
Además, no debe agregar DFContinuousForceTouchGestureRecognizer
a una vista si se está ejecutando en iOS 8 o menos, ya que usa la nueva propiedad force
en UITouch
solo está disponible en iOS 9.
Si agrega esto en iOS 8, se bloqueará, por lo tanto, agregue este reconocedor de manera condicional en función de la versión de iOS en la que esté ejecutando si admite versiones anteriores a iOS 9.
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í).