web-dev-qa-db-fra.com

Implémentation 3D touch / Force touch

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?

37
Avi Rok

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).

21
random

Les propriétés 3D Touch sont disponibles sur les objets UITouch .

Vous pouvez obtenir ces touches en remplaçant le UIViewtouchesBegan: 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 UITouches 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 UIGestureRecognizerDelegategestureRecognizer:shouldReceiveTouch: méthode.

9
Rhythmic Fistman

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:

6
Imanou Petit

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:

  • Le toucher tactile 3D vibre comme le comportement du système iOS
  • le toucher doit apparaître pour que cela se termine, comme Apple mail app
  • le seuil par défaut est le niveau par défaut du système
  • le toucher dur déclenche l'appel hardAction comme l'application de messagerie

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)
        }
    }
}
5
Joel Teply

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.

5
foggzilla