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.
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:
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é.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.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
).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.paused
sur true
avant d’invalider le lien d’affichage, car il est redondant.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. .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.displayLinkDidFire(_:)
prend un seul argument de type CADisplayLink
, comme requis par la documentation .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
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.