Comment pouvons-nous implémenter 3D touch pour vérifier si l'utilisateur tape sur UIView
ou force le toucher sur UIView
?
Existe-t-il un moyen de le faire avec UIGestureRecognize
ou uniquement avec UITouch
?
Vous pouvez le faire sans un identificateur de geste désigné. Vous n'avez pas besoin d'ajuster les méthodes touchesEnded et touchesBegan, mais simplement les touchesMoved pour obtenir les valeurs correctes. obtenir la force d'un uitouch à partir du début/de la fin renverra des valeurs étranges.
UITouch *touch = [touches anyObject];
CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;
ensuite, définissez un seuil de force et comparez la force normalisée à ce seuil (0,75 me semble bien).
Les propriétés 3D Touch sont disponibles sur les objets UITouch
.
Vous pouvez obtenir ces touches en remplaçant le UIView
touchesBegan:
et touchesMoved:
méthodes. Je ne sais pas ce que vous voyez dans touchesEnded:
encore.
Si vous souhaitez créer de nouveaux identificateurs de gestes, vous avez un accès complet aux UITouch
es comme exposé dans UIGestureRecognizerSubclass
.
Je ne sais pas comment vous pourriez utiliser les propriétés tactiles 3D dans un UIGestureRecognizer
traditionnel. Peut-être via le protocole UIGestureRecognizerDelegate
gestureRecognizer:shouldReceiveTouch:
méthode.
Avec Swift 4.2 et iOS 12, un moyen possible de résoudre votre problème consiste à créer une sous-classe personnalisée de UIGestureRecognizer
qui gère Force Touch et l'ajouter à votre vue à côté d'un UITapGestureRecognizer
. Le code complet suivant montre comment l'implémenter:
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
}
}
}
Sources:
J'ai créé un UIGestureRecognizer qui émule le comportement de l'application Apple Mail. Au toucher 3D, il commence par une courte vibration Pulse unique, puis une action secondaire facultative (hardTarget) et Pulse appelée par appui fort peu de temps après la presse initiale.
Adapté de https://github.com/FlexMonkey/DeepPressGestureRecognizer
Changements:
Remarque: J'ai ajouté le son système non documenté k_PeakSoundID, mais n'hésitez pas à le désactiver si vous n'êtes pas à l'aise avec une constante au-delà de la plage documentée . J'utilise des sons système avec des constantes non divulguées depuis des années, mais vous êtes invités à désactiver les impulsions de vibration à l'aide de la propriété 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 façon dont je fais cela est d'utiliser une combinaison d'un ITapGestureRecognizer (fourni par Apple) et un DFContinuousForceTouchGestureRecognizer (fourni par moi).
Le DFContinuousForceTouchGestureRecognizer
est agréable car il fournit des mises à jour continues sur les changements de pression afin que vous puissiez faire des choses comme augmenter la vue lorsque l'utilisateur fait varier sa pression sur lui, par opposition à un seul événement. Si vous ne voulez qu'un seul événement, vous pouvez ignorer tout dans le rappel DFContinuousForceTouchDelegate
sauf le rappel - (void) forceTouchRecognized
.
https://github.com/foggzilla/DFContinuousForceTouchGestureRecognizer
Vous pouvez le télécharger et exécuter l'exemple d'application sur un appareil qui prend en charge la pression forcée pour voir comment il se sent.
Dans votre UIViewController
implémentez les éléments suivants:
- (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];
}
mettre en œuvre le sélecteur pour le geste de robinet
#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];
});
}
Mettre en œuvre le protocole délégué pour le toucher de force:
#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];
}
Notez que cela ne sera utile que sur un appareil qui prend en charge le toucher forcé.
De plus, vous ne devez pas ajouter le DFContinuousForceTouchGestureRecognizer
à une vue si vous exécutez sur iOS 8 ou sous car il utilise la nouvelle propriété force
sur UITouch
uniquement disponible dans iOS 9.
Si vous ajoutez cela sur iOS 8, il se bloquera, alors ajoutez conditionnellement ce module de reconnaissance en fonction de la version iOS sur laquelle vous exécutez si vous prenez en charge des versions antérieures à iOS 9.