web-dev-qa-db-fra.com

Manipulation/nettoyage/etc corrects de CADisplayLink dans l'animation personnalisée Swift?

Considérez cette animation de synchronisation triviale en utilisant CADisplayLink,

var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4

private func yourAnim()
    {
    if ( link != nil )
        {
        link!.paused = true
        //A:
        link!.removeFromRunLoop(
          NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
        link = nil
        }

    link = CADisplayLink(target: self, selector: #selector(doorStep) )
    startTime = CACurrentMediaTime()
    link!.addToRunLoop(
      NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
    }

func doorStep()
    {
    let elapsed = CACurrentMediaTime() - startTime

    var ping = elapsed
    if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}

    let frac = ping / (animTime / 2.0)
    yourAnimFunction(CGFloat(frac) * animMaxVal)

    if (elapsed > animTime)
        {
        //B:
        link!.paused = true
        link!.removeFromRunLoop(
          NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
        link = nil
        yourAnimFunction(0.0)
        }
    }

func killAnimation()
    {
    // for example if the cell disappears or is reused
    //C:
    ????!!!!
    }

Il semble y avoir divers problèmes.

Dans (A :), même si link n'est pas null, il peut ne pas être possible de le supprimer d'une boucle d'exécution. (Par exemple, quelqu'un peut l'avoir initialisé avec link = link:CADisplayLink() - essayez-le en cas de plantage.)

Deuxièmement, à (B :), cela semble être un gâchis ... il y a sûrement une meilleure (et plus rapide) façon, et si c'était nul même si le temps venait à expiration?

Enfin en (C :) si vous voulez casser l’animation ... Je me suis sentie déprimée et je n’ai aucune idée de ce qui est préférable.

Et vraiment, le code en A: et B: devrait être le même appel, une sorte d’appel de nettoyage.

17
Fattie

Voici un exemple simple montrant comment je pourrais implémenter une CADisplayLink (dans Swift 3):

class C { // your view class or whatever

  private var displayLink: CADisplayLink?
  private var startTime = 0.0
  private let animLength = 5.0

  func startDisplayLink() {

    stopDisplayLink() // make sure to stop a previous running display link
    startTime = CACurrentMediaTime() // reset start time

    // create displayLink & add it to the run-loop
    let displayLink = CADisplayLink(
      target: self, selector: #selector(displayLinkDidFire)
    )
    displayLink.add(to: .main, forMode: .commonModes)
    self.displayLink = displayLink
  }

  @objc func displayLinkDidFire(_ displayLink: CADisplayLink) {

    var elapsed = CACurrentMediaTime() - startTime

    if elapsed > animLength {
      stopDisplayLink()
      elapsed = animLength // clamp the elapsed time to the anim length
    }

    // do your animation logic here
  }

  // invalidate display link if it's non-nil, then set to nil
  func stopDisplayLink() {
    displayLink?.invalidate()
    displayLink = nil
  }
}

Points à noter:

  • Nous utilisons nil ici pour représenter l’état dans lequel le lien d’affichage ne fonctionne pas - car il n’existe pas de moyen simple d’obtenir ces informations à partir d’un lien d’affichage invalidé.
  • Au lieu d'utiliser removeFromRunLoop(), nous utilisons invalidate(), qui ne plantera pas si le lien d'affichage n'a pas déjà été ajouté à une boucle d'exécution. Toutefois, cette situation ne devrait jamais se produire - nous ajoutons toujours immédiatement le lien d’affichage à la boucle d’exécution après sa création.
  • Nous avons rendu la variable displayLink privée afin d'empêcher les classes extérieures de la placer dans un état inattendu (par exemple, l'invalider mais pas la définir sur nil).
  • Nous avons une seule méthode stopDisplayLink() qui invalide le lien d'affichage (s'il est non nul) et le définit sur nil - plutôt que de copier et coller cette logique.
  • Nous n’avons pas défini paused sur true avant d’invalider le lien d’affichage, car il est redondant.
  • Au lieu de forcer le décompressage de displayLink après la vérification de non-nil, nous utilisons un chaînage facultatif, par exemple displayLink?.invalidate() (qui appellera invalidate() si le lien d’affichage est non nul). Bien que le déroulage forcé puisse être «sans danger» dans votre situation donnée (alors que vous vérifiez zéro), il est potentiellement dangereux pour la refactorisation future, car vous pouvez restructurer votre logique sans considérer son impact sur le déploiement de la force. .
  • Nous fixons la durée elapsed à la durée de l'animation afin de nous assurer que la logique d'animation ultérieure ne produira pas une valeur hors de la plage attendue.
  • Notre méthode de mise à jour displayLinkDidFire(_:) prend un seul argument de type CADisplayLink, comme requis par la documentation .
31
Hamish

Je me rends compte que cette question a déjà une bonne réponse, mais voici une autre approche légèrement différente qui aide à mettre en œuvre des animations fluides, indépendamment de la fréquence d'images de la liaison d'affichage. 

** (Lien vers le projet de démonstration disponible au bas de cette réponse - UPDATE: le code source du projet de démonstration est maintenant mis à jour vers Swift 4) 

Pour ma mise en œuvre, j'ai choisi d'envelopper le lien d'affichage dans sa propre classe et de configurer une référence de délégué qui sera appelée avec le temps delta (le temps entre le dernier appel du lien d'affichage et l'appel en cours) afin que nous puissions exécuter un peu plus nos animations. doucement.

J'utilise actuellement cette méthode pour animer environ 60 vues sur l'écran simultanément dans un jeu.

Nous allons d’abord définir le protocole de délégation que notre encapsuleur appellera pour notifier les événements de mise à jour.

// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
    func displayWillUpdate(deltaTime: CFTimeInterval)
}

Ensuite, nous allons définir notre classe wrapper de liens d’affichage. Cette classe prendra une référence de délégué à l'initialisation. Une fois initialisé, il démarrera automatiquement notre lien d’affichage et le nettoiera sur deinit.

import UIKit

class DisplayUpdateNotifier {

    // **********************************************
    //  MARK: Variables
    // **********************************************

    /// A weak reference to the delegate/listener that will be notified/called on display updates
    weak var listener: DisplayUpdateReceiver?

    /// The display link that will be initiating our updates
    internal var displayLink: CADisplayLink? = nil

    /// Tracks the timestamp from the previous displayLink call
    internal var lastTime: CFTimeInterval = 0.0

    // **********************************************
    //  MARK: Setup & Tear Down
    // **********************************************

    deinit {
        stopDisplayLink()
    }

    init(listener: DisplayUpdateReceiver) {
        // setup our delegate listener reference
        self.listener = listener

        // setup & kick off the display link
        startDisplayLink()
    }

    // **********************************************
    //  MARK: CADisplay Link
    // **********************************************

    /// Creates a new display link if one is not already running
    private func startDisplayLink() {
        guard displayLink == nil else {
            return
        }

        displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
        displayLink?.add(to: .main, forMode: .commonModes)
        lastTime = 0.0
    }

    /// Invalidates and destroys the current display link. Resets timestamp var to zero
    private func stopDisplayLink() {
        displayLink?.invalidate()
        displayLink = nil
        lastTime = 0.0
    }

    /// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
    @objc private func linkUpdate() {
        // bail if our display link is no longer valid
        guard let displayLink = displayLink else {
            return
        }

        // get the current time
        let currentTime = displayLink.timestamp

        // calculate delta (
        let delta: CFTimeInterval = currentTime - lastTime

        // store as previous
        lastTime = currentTime

        // call delegate
        listener?.displayWillUpdate(deltaTime: delta)
    }
}

Pour l'utiliser, il vous suffit d'initialiser une instance de l'encapsuleur, en lui transmettant la référence du programme d'écoute délégué, puis de mettre à jour vos animations en fonction du temps delta. Dans cet exemple, le délégué transmet l'appel de mise à jour à la vue pouvant être animée (de cette manière, vous pouvez suivre plusieurs vues d'animation et demander à chacune de mettre à jour leurs positions via cet appel).

class ViewController: UIViewController, DisplayUpdateReceiver {

    var displayLinker: DisplayUpdateNotifier?
    var animView: MoveableView?

    override func viewDidLoad() {
        super.viewDidLoad()

        // setup our animatable view and add as subview
        animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
        animView?.configureMovement()
        animView?.backgroundColor = .blue
        view.addSubview(animView!)

        // setup our display link notifier wrapper class
        displayLinker = DisplayUpdateNotifier.init(listener: self)
    }

    // implement DisplayUpdateReceiver function to receive updates from display link wrapper class
    func displayWillUpdate(deltaTime: CFTimeInterval) {
        // pass the update call off to our animating view or views
        _ = animView?.update(deltaTime: deltaTime)

        // in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
        // that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
        // our view again
        if animView?.isReadyForReuse == true {
            animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
            view.addSubview(animView!)
        }
    }
}

Notre fonction de mise à jour des vues mobiles ressemble à ceci:

func update(deltaTime: CFTimeInterval) -> Bool {
    guard canAnimate == true, isReadyForReuse == false else {
        return false
    }

    // by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
    let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
    let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))

    // update velocity with smoothed acceleration
    velocity.adding(point: smoothAccel)

    // update center with smoothed velocity
    center.adding(point: smoothVel)

    currentTime += 0.01
    if currentTime >= timeLimit {
        canAnimate = false
        endAnimation()
        return false
    }

    return true
}

Si vous souhaitez parcourir un projet de démonstration complet, vous pouvez le télécharger à partir de GitHub ici: Projet de démonstration CADisplayLink

4
digitalHound

Ce qui précède est le meilleur exemple d’utilisation efficace de CADisplayLink. Merci à @Fattie et @digitalHound 

Je ne pouvais pas m'empêcher d'ajouter mon utilisation des classes CADisplayLink et DisplayUpdater par 'digitalHound' dans PdfViewer à l'aide de WKWebView.

Peut-être que la réponse ici n’est pas le bon endroit, mais j’ai l’intention de montrer l’utilisation de CADisplayLink ici. (pour d'autres comme moi, qui peuvent mettre en œuvre leurs exigences.)

//
//  PdfViewController.Swift
//

import UIKit
import WebKit

class PdfViewController: UIViewController, DisplayUpdateReceiver {

    @IBOutlet var mySpeedScrollSlider: UISlider!    // UISlider in storyboard

    var displayLinker: DisplayUpdateNotifier?

    var myPdfFileName = ""                          
    var myPdfFolderPath = ""
    var myViewTitle = "Pdf View"
    var myCanAnimate = false
    var mySlowSkip = 0.0

    // 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
    var cuScrollSpeed = 0.25

    fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        self.title = myViewTitle
        let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
        navigationItem.leftBarButtonItem = leftItem

        self.view.backgroundColor = UIColor.white.cgColor
        mySpeedScrollSlider.minimumValue = 0.05
        mySpeedScrollSlider.maximumValue = 4.0
        mySpeedScrollSlider.isContinuous = true
        mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged]) 
        mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
        mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor

        self.configureWebView()
        let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
        let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
        myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
    }

    //MARK: - Button Action

    @objc func PdfBackClick()
    {
        _ = self.navigationController?.popViewController(animated: true)
    }

    @objc func updateSlider()
    {
        if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
            myCanAnimate = false
        } else {
            myCanAnimate = true
        }
        cuScrollSpeed = Double(mySpeedScrollSlider.value)
    }

    fileprivate func configureWebView() {
        myPdfWKWebView.frame = view.bounds
        myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
        myPdfWKWebView.navigationDelegate = self
        myPdfWKWebView.isMultipleTouchEnabled = true
        myPdfWKWebView.scrollView.alwaysBounceVertical = true
        myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
        view.addSubview(myPdfWKWebView)
        myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
        myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
    }

    //MARK: - DisplayUpdateReceiver delegate

    func displayWillUpdate(deltaTime: CFTimeInterval) {

        guard myCanAnimate == true else {
            return
        }

        var maxSpeed = 0.0

        if cuScrollSpeed < 0.5 {
            if mySlowSkip > 0.25 {
                mySlowSkip = 0.0
            } else {
                mySlowSkip += cuScrollSpeed
                return
            }
            maxSpeed = 0.5
        } else {
            maxSpeed = cuScrollSpeed
        }

        let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
        let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
        let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
        let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x

        if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
        {
            return
        }

        let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
        self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
    }

}

extension PdfViewController: WKNavigationDelegate {
    // MARK: - WKNavigationDelegate
    public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        //print("didStartProvisionalNavigation")
    }

    public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        //print("didFinish")
        displayLinker = DisplayUpdateNotifier.init(listener: self)
        myCanAnimate = true
    }

    public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        //print("didFailProvisionalNavigation error:\(error)")
    }

    public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        //print("didFail")
    }

}

Exemple L'appel depuis une autre vue est comme sous.

Pour charger le fichier PDF à partir du dossier Document.

func callPdfViewController( theFileName:String, theFileParentPath:String){
    if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
        let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
        pdfViewController?.myPdfFileName = theFileName
        pdfViewController?.myPdfFolderPath = theFileParentPath
        self.navigationController!.pushViewController(pdfViewController!, animated: true)
    } else {
        // Show error.
    }
}

Cet exemple peut être "modifié" pour charger une page Web et le faire défiler automatiquement à la vitesse sélectionnée par l'utilisateur. 

Cordialement

Sanjay.

0
SHS