swift animation cadisplaylink

¿Manejo correcto/limpieza/etc de CADisplayLink en animación personalizada Swift?



animation (3)

Considera esta animación de sincronización trivial usando 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: ????!!!! }

Parece que hay varios problemas.

En (A :), aunque el link no es nulo, puede que no sea posible eliminarlo de un bucle de ejecución. (Por ejemplo, alguien puede haberlo inicializado con link = link:CADisplayLink() - inténtalo para un fallo).

En segundo lugar, en (B :) parece ser un desastre ... seguramente hay una forma mejor (y más rápida), y ¿qué pasa si es nula a pesar de que el tiempo acaba de expirar?

Finalmente en (C :) si quieres romper la animación ... Me deprimí y no tengo idea de qué es lo mejor.

Y realmente el código en A: y B: debería ser el mismo derecho de llamada, una especie de llamada de limpieza.


Aquí hay un ejemplo simple que muestra cómo me gustaría implementar un CADisplayLink (en 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 } }

Puntos a tener en cuenta:

  • Estamos utilizando nil aquí para representar el estado en el que no se está ejecutando el enlace de visualización, ya que no hay una manera fácil de obtener esta información de un enlace de visualización invalidado.
  • En lugar de usar removeFromRunLoop() , estamos usando invalidate() , que no se bloqueará si el enlace de visualización no se ha agregado a un ciclo de ejecución. Sin embargo, esta situación nunca debería surgir en primer lugar, ya que siempre estamos agregando inmediatamente el enlace de visualización al ciclo de ejecución después de crearlo.
  • Hemos hecho que el displayLink privado para evitar que las clases externas lo pongan en un estado inesperado (p. Ej., Anularlo pero no configurarlo en nil ).
  • Tenemos un solo método stopDisplayLink() que invalida el enlace de visualización (si no es nulo) y lo establece en nil , en lugar de copiar y pegar esta lógica.
  • No estamos configurando paused como true antes de invalidar el enlace de pantalla, ya que esto es redundante.
  • En lugar de forzar el desenvolvimiento del displayLink después de verificar que no sea nulo, estamos usando un encadenamiento opcional, por ejemplo, displayLink?.invalidate() (que llamará invalidate() si el enlace del display no es nulo). Si bien el desenvolvimiento de fuerza puede ser ''seguro'' en su situación dada (ya que no está comprobando), es potencialmente peligroso cuando se trata de refactorizar en el futuro, ya que puede reestructurar su lógica sin tener en cuenta el impacto que esto tiene en la fuerza .
  • Estamos limitando el tiempo elapsed a la duración de la animación para garantizar que la lógica de la animación posterior no genere un valor fuera del rango esperado.
  • Nuestro método de actualización displayLinkDidFire(_:) toma un solo argumento de tipo CADisplayLink , como lo requiere la documentación .

El ejemplo anterior es el mejor ejemplo de cómo usar CADisplayLink con eficiencia. Gracias a @Fattie y @digitalHound

No pude resistirme a agregar mi uso de las clases CADisplayLink y DisplayUpdater mediante ''digitalHound'' en PdfViewer usando WKWebView. Mi requisito era continuar con el desplazamiento automático del pdf a la velocidad seleccionable por el usuario.

Puede que la respuesta aquí no sea el lugar correcto, pero intento mostrar el uso de CADisplayLink aquí. (para otros como yo, que pueden implementar sus requisitos).

// // 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") } }

Ejemplo Llamar desde otra vista es como debajo.

Para cargar el archivo PDF desde la carpeta de documentos.

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

Este ejemplo puede ser ''modificado'' para cargar la página web y desplazarse automáticamente a la velocidad seleccionada por el usuario.

Saludos

Sanjay


Me doy cuenta de que esta pregunta ya tiene una buena respuesta, pero aquí hay otro enfoque ligeramente diferente que ayuda a implementar animaciones suaves independientemente de la velocidad de cuadros del enlace de pantalla.

** (Enlace al proyecto de demostración disponible en la parte inferior de esta respuesta - ACTUALIZACIÓN: el código fuente del proyecto de demostración ahora se ha actualizado a Swift 4)

Para mi implementación, opté por envolver el enlace de visualización en su propia clase y configurar una referencia de delegado que se llamará con el tiempo delta (el tiempo entre la última llamada del enlace de visualización y la llamada actual) para que podamos realizar nuestras animaciones un poco más suavemente.

Actualmente estoy usando este método para animar ~ 60 vistas en la pantalla simultáneamente en un juego.

Primero vamos a definir el protocolo de delegado al que llamará nuestro contenedor para notificar los eventos de actualización.

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

A continuación vamos a definir nuestra clase de contenedor de enlace de visualización. Esta clase tomará una referencia de delegado en la inicialización. Cuando se inicie, iniciará automáticamente nuestro enlace de pantalla y lo limpiará en 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) } }

Para usarlo, simplemente inicialice una instancia del contenedor, pasando la referencia del oyente delegado, luego actualice sus animaciones en función del tiempo delta. En este ejemplo, el delegado pasa la llamada de actualización a la vista animable (de esta manera usted podría rastrear múltiples vistas animadas y hacer que cada una actualice sus posiciones a través de esta llamada).

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

Nuestra función de actualización de vistas móviles se ve así:

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 desea ver un proyecto de demostración completo, puede descargarlo desde GitHub aquí: Proyecto de demostración CADisplayLink