Use a shared CADisplayLink whenever possible

This commit is contained in:
Ali 2022-12-26 21:04:22 +04:00
parent 654f61a06b
commit b80eace071
11 changed files with 181 additions and 262 deletions

View File

@ -34,55 +34,6 @@ private let completionKey = "CAAnimationUtils_completion"
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve" public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve"
private final class FrameRangeContext {
private var animationCount: Int = 0
private var displayLink: CADisplayLink?
init() {
}
func add() {
self.animationCount += 1
self.update()
}
func remove() {
self.animationCount -= 1
if self.animationCount < 0 {
self.animationCount = 0
assertionFailure()
}
self.update()
}
@objc func displayEvent() {
}
private func update() {
if self.animationCount != 0 {
if self.displayLink == nil {
let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent))
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
}
}
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
displayLink.isPaused = false
}
} else if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
private let frameRangeContext = FrameRangeContext()
public extension CAAnimation { public extension CAAnimation {
var completion: ((Bool) -> Void)? { var completion: ((Bool) -> Void)? {
get { get {
@ -103,18 +54,16 @@ public extension CAAnimation {
private func adjustFrameRate(animation: CAAnimation) { private func adjustFrameRate(animation: CAAnimation) {
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
if let animation = animation as? CABasicAnimation {
if animation.keyPath == "opacity" {
return
}
}
let maxFps = Float(UIScreen.main.maximumFramesPerSecond) let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 { if maxFps > 61.0 {
#if DEBUG var preferredFps: Float = maxFps
//let _ = frameRangeContext.add() if let animation = animation as? CABasicAnimation {
#endif if animation.keyPath == "opacity" {
preferredFps = 60.0
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps) return
}
}
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: preferredFps, preferred: maxFps)
} }
} }
} }

View File

@ -1,6 +1,153 @@
import Foundation import Foundation
import UIKit import UIKit
public final class SharedDisplayLinkDriver {
public static let shared = SharedDisplayLinkDriver()
public final class Link {
private let driver: SharedDisplayLinkDriver
public let needsHighestFramerate: Bool
let update: () -> Void
var isValid: Bool = true
public var isPaused: Bool = false {
didSet {
if self.isPaused != oldValue {
driver.requestUpdate()
}
}
}
init(driver: SharedDisplayLinkDriver, needsHighestFramerate: Bool, update: @escaping () -> Void) {
self.driver = driver
self.needsHighestFramerate = needsHighestFramerate
self.update = update
}
public func invalidate() {
self.isValid = false
}
}
private final class RequestContext {
weak var link: Link?
init(link: Link) {
self.link = link
}
}
private var displayLink: CADisplayLink?
private var hasRequestedHighestFramerate: Bool = false
private var requests: [RequestContext] = []
private var isInForeground: Bool = false
private init() {
let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = true
self.update()
})
let _ = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let self else {
return
}
self.isInForeground = false
self.update()
})
switch UIApplication.shared.applicationState {
case .active:
self.isInForeground = true
default:
self.isInForeground = false
}
self.update()
}
private func requestUpdate() {
self.update()
}
private func update() {
var hasActiveItems = false
var needHighestFramerate = false
for request in self.requests {
if let link = request.link {
needHighestFramerate = link.needsHighestFramerate
if link.isValid && !link.isPaused {
hasActiveItems = true
break
}
}
}
if self.isInForeground && hasActiveItems {
let displayLink: CADisplayLink
if let current = self.displayLink {
displayLink = current
} else {
displayLink = CADisplayLink(target: self, selector: #selector(self.displayLinkEvent))
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
}
if #available(iOS 15.0, *) {
let frameRateRange: CAFrameRateRange
if needHighestFramerate {
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
} else {
frameRateRange = .default
}
if displayLink.preferredFrameRateRange != frameRateRange {
displayLink.preferredFrameRateRange = frameRateRange
}
}
displayLink.isPaused = false
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
@objc private func displayLinkEvent() {
var removeIndices: [Int]?
for i in 0 ..< self.requests.count {
if let link = self.requests[i].link, link.isValid {
link.update()
} else {
if removeIndices == nil {
removeIndices = [i]
} else {
removeIndices?.append(i)
}
}
}
if let removeIndices = removeIndices {
for index in removeIndices.reversed() {
self.requests.remove(at: index)
}
if self.requests.isEmpty {
self.update()
}
}
}
public func add(needsHighestFramerate: Bool = true, _ update: @escaping () -> Void) -> Link {
let link = Link(driver: self, needsHighestFramerate: needsHighestFramerate, update: update)
self.requests.append(RequestContext(link: link))
self.update()
return link
}
}
public final class DisplayLinkTarget: NSObject { public final class DisplayLinkTarget: NSObject {
private let f: () -> Void private let f: () -> Void
@ -14,7 +161,7 @@ public final class DisplayLinkTarget: NSObject {
} }
public final class DisplayLinkAnimator { public final class DisplayLinkAnimator {
private var displayLink: CADisplayLink! private var displayLink: SharedDisplayLinkDriver.Link?
private let duration: Double private let duration: Double
private let fromValue: CGFloat private let fromValue: CGFloat
private let toValue: CGFloat private let toValue: CGFloat
@ -32,21 +179,20 @@ public final class DisplayLinkAnimator {
self.startTime = CACurrentMediaTime() self.startTime = CACurrentMediaTime()
self.displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
self?.tick() self?.tick()
}), selector: #selector(DisplayLinkTarget.event)) }
self.displayLink.isPaused = false self.displayLink?.isPaused = false
self.displayLink.add(to: RunLoop.main, forMode: .common)
} }
deinit { deinit {
self.displayLink.isPaused = true self.displayLink?.isPaused = true
self.displayLink.invalidate() self.displayLink?.invalidate()
} }
public func invalidate() { public func invalidate() {
self.displayLink.isPaused = true self.displayLink?.isPaused = true
self.displayLink.invalidate() self.displayLink?.invalidate()
} }
@objc private func tick() { @objc private func tick() {
@ -60,14 +206,14 @@ public final class DisplayLinkAnimator {
self.update(self.fromValue * CGFloat(1 - t) + self.toValue * CGFloat(t)) self.update(self.fromValue * CGFloat(1 - t) + self.toValue * CGFloat(t))
if abs(t - 1.0) < Double.ulpOfOne { if abs(t - 1.0) < Double.ulpOfOne {
self.completed = true self.completed = true
self.displayLink.isPaused = true self.displayLink?.isPaused = true
self.completion() self.completion()
} }
} }
} }
public final class ConstantDisplayLinkAnimator { public final class ConstantDisplayLinkAnimator {
private var displayLink: CADisplayLink? private var displayLink: SharedDisplayLinkDriver.Link?
private let update: () -> Void private let update: () -> Void
private var completed = false private var completed = false
@ -81,26 +227,16 @@ public final class ConstantDisplayLinkAnimator {
guard let displayLink = self.displayLink else { guard let displayLink = self.displayLink else {
return return
} }
if self.frameInterval == 1 { let _ = displayLink
if #available(iOS 15.0, *) {
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
}
} else {
displayLink.preferredFramesPerSecond = 30
}
} }
public var isPaused: Bool = true { public var isPaused: Bool = true {
didSet { didSet {
if self.isPaused != oldValue { if self.isPaused != oldValue {
if !self.isPaused && self.displayLink == nil { if !self.isPaused && self.displayLink == nil {
let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
self?.tick() self?.tick()
}), selector: #selector(DisplayLinkTarget.event)) }
/*if #available(iOS 15.0, *) {
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
}*/
displayLink.add(to: RunLoop.main, forMode: .common)
self.displayLink = displayLink self.displayLink = displayLink
self.updateDisplayLink() self.updateDisplayLink()
} }

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
public class DisplayLinkDispatcher: NSObject { public class DisplayLinkDispatcher: NSObject {
private var displayLink: CADisplayLink!
private var blocksToDispatch: [() -> Void] = [] private var blocksToDispatch: [() -> Void] = []
private let limit: Int private let limit: Int
@ -10,38 +9,13 @@ public class DisplayLinkDispatcher: NSObject {
self.limit = limit self.limit = limit
super.init() super.init()
if #available(iOS 10.0, *) {
//self.displayLink.preferredFramesPerSecond = 60
} else {
self.displayLink = CADisplayLink(target: self, selector: #selector(self.run))
self.displayLink.isPaused = true
self.displayLink.add(to: RunLoop.main, forMode: .common)
}
} }
public func dispatch(f: @escaping () -> Void) { public func dispatch(f: @escaping () -> Void) {
if self.displayLink == nil { if Thread.isMainThread {
if Thread.isMainThread { f()
f()
} else {
DispatchQueue.main.async(execute: f)
}
} else { } else {
self.blocksToDispatch.append(f) DispatchQueue.main.async(execute: f)
self.displayLink.isPaused = false
}
}
@objc func run() {
for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) {
if self.blocksToDispatch.count == 0 {
self.displayLink.isPaused = true
break
} else {
let f = self.blocksToDispatch.removeFirst()
f()
}
} }
} }
} }

View File

@ -814,12 +814,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
} }
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if #available(iOS 15.0, *) {
if let scrollDisplayLink = self.scroller.value(forKey: "_scrollHeartbeat") as? CADisplayLink {
let _ = scrollDisplayLink
}
}
self.isDragging = false self.isDragging = false
if decelerate { if decelerate {
self.lastContentOffsetTimestamp = CACurrentMediaTime() self.lastContentOffsetTimestamp = CACurrentMediaTime()

View File

@ -36,44 +36,6 @@ class DisplayLinkService {
} }
} }
// private init() {
// displayLink.add(to: .main, forMode: .common)
// displayLink.preferredFramesPerSecond = 60
// displayLink.isPaused = true
// }
//
// // MARK: - Display Link
// private lazy var displayLink: CADisplayLink! = { CADisplayLink(target: self, selector: #selector(displayLinkDidFire)) } ()
// private var previousTickTime = 0.0
//
// private func startDisplayLink() {
// guard displayLink.isPaused else {
// return
// }
// previousTickTime = CACurrentMediaTime()
// displayLink.isPaused = false
// }
//
// @objc private func displayLinkDidFire(_ displayLink: CADisplayLink) {
// let currentTime = CACurrentMediaTime()
// let delta = currentTime - previousTickTime
// previousTickTime = currentTime
// let allListners = listners.allObjects
// var hasListners = false
// for listner in allListners {
// (listner as! DisplayLinkListner).update(delta: delta)
// hasListners = true
// }
//
// if !hasListners {
// stopDisplayLink()
// }
// }
//
// private func stopDisplayLink() {
// displayLink.isPaused = true
// }
private init() { private init() {
dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60) dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60)
dispatchSourceTimer.setEventHandler { dispatchSourceTimer.setEventHandler {

View File

@ -145,7 +145,7 @@ open class ManagedAnimationNode: ASDisplayNode {
public let intrinsicSize: CGSize public let intrinsicSize: CGSize
private let imageNode: ASImageNode private let imageNode: ASImageNode
private let displayLink: CADisplayLink private let displayLink: SharedDisplayLinkDriver.Link
public var imageUpdated: ((UIImage) -> Void)? public var imageUpdated: ((UIImage) -> Void)?
public var image: UIImage? { public var image: UIImage? {
@ -179,19 +179,14 @@ open class ManagedAnimationNode: ASDisplayNode {
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
var displayLinkUpdate: (() -> Void)? var displayLinkUpdate: (() -> Void)?
self.displayLink = CADisplayLink(target: DisplayLinkTarget { self.displayLink = SharedDisplayLinkDriver.shared.add {
displayLinkUpdate?() displayLinkUpdate?()
}, selector: #selector(DisplayLinkTarget.event))
if #available(iOS 10.0, *) {
self.displayLink.preferredFramesPerSecond = 60
} }
super.init() super.init()
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
self.displayLink.add(to: RunLoop.main, forMode: .common)
displayLinkUpdate = { [weak self] in displayLinkUpdate = { [weak self] in
self?.updateAnimation() self?.updateAnimation()
} }
@ -199,6 +194,7 @@ open class ManagedAnimationNode: ASDisplayNode {
open func advanceState() { open func advanceState() {
guard !self.trackStack.isEmpty else { guard !self.trackStack.isEmpty else {
self.displayLink.isPaused = true
return return
} }
@ -211,6 +207,7 @@ open class ManagedAnimationNode: ASDisplayNode {
} }
self.didTryAdvancingState = false self.didTryAdvancingState = false
self.displayLink.isPaused = false
} }
public func updateAnimation() { public func updateAnimation() {
@ -219,6 +216,7 @@ open class ManagedAnimationNode: ASDisplayNode {
} }
guard let state = self.state else { guard let state = self.state else {
self.displayLink.isPaused = true
return return
} }

View File

@ -300,7 +300,7 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode {
public final class MediaPlayerScrubbingNode: ASDisplayNode { public final class MediaPlayerScrubbingNode: ASDisplayNode {
private var contentNodes: MediaPlayerScrubbingNodeContentNodes private var contentNodes: MediaPlayerScrubbingNodeContentNodes
private var displayLink: CADisplayLink? private var displayLink: SharedDisplayLinkDriver.Link?
private var isInHierarchyValue: Bool = false private var isInHierarchyValue: Bool = false
private var playbackStatusValue: MediaPlayerPlaybackStatus? private var playbackStatusValue: MediaPlayerPlaybackStatus?
@ -798,20 +798,9 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
if needsAnimation { if needsAnimation {
if self.displayLink == nil { if self.displayLink == nil {
class DisplayLinkProxy: NSObject { let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
var f: () -> Void
init(_ f: @escaping () -> Void) {
self.f = f
}
@objc func displayLinkEvent() {
self.f()
}
}
let displayLink = CADisplayLink(target: DisplayLinkProxy({ [weak self] in
self?.updateProgress() self?.updateProgress()
}), selector: #selector(DisplayLinkProxy.displayLinkEvent)) }
displayLink.add(to: .main, forMode: RunLoop.Mode.common)
self.displayLink = displayLink self.displayLink = displayLink
} }
self.displayLink?.isPaused = false self.displayLink?.isPaused = false

View File

@ -209,64 +209,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
) )
} }
private final class AnimationSupportContext {
private let window: UIWindow
private let testView: UIView
private var animationCount: Int = 0
private var displayLink: CADisplayLink?
init(window: UIWindow) {
self.window = window
self.testView = UIView()
window.addSubview(self.testView)
self.testView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0))
self.testView.backgroundColor = .black
}
func add() {
self.animationCount += 1
self.update()
}
func remove() {
self.animationCount -= 1
if self.animationCount < 0 {
self.animationCount = 0
assertionFailure()
}
self.update()
}
@objc func displayEvent() {
self.testView.frame = CGRect(origin: CGPoint(x: self.testView.frame.minX == 0.0 ? 1.0 : 0.0, y: 0.0), size: self.testView.bounds.size)
}
private func update() {
if self.animationCount != 0 {
if self.displayLink == nil {
let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent))
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
}
}
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
displayLink.isPaused = false
}
} else if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate { @objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate {
@objc var window: UIWindow? @objc var window: UIWindow?
private var animationSupportContext: AnimationSupportContext?
var nativeWindow: (UIWindow & WindowHost)? var nativeWindow: (UIWindow & WindowHost)?
var mainWindow: Window1! var mainWindow: Window1!
private var dataImportSplash: LegacyDataImportSplash? private var dataImportSplash: LegacyDataImportSplash?
@ -362,9 +306,6 @@ private final class AnimationSupportContext {
self.window = window self.window = window
self.nativeWindow = window self.nativeWindow = window
//self.animationSupportContext = AnimationSupportContext(window: window)
//self.animationSupportContext?.add()
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in

View File

@ -78,12 +78,6 @@ extension CALayer {
let animation = CABasicAnimation(keyPath: property.caLayerKeypath) let animation = CABasicAnimation(keyPath: property.caLayerKeypath)
animation.fromValue = keyframeValue animation.fromValue = keyframeValue
animation.toValue = keyframeValue animation.toValue = keyframeValue
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
}
}
return animation return animation
} }
@ -141,12 +135,6 @@ extension CALayer {
let calculationMode = try self.calculationMode(for: keyframes, context: context) let calculationMode = try self.calculationMode(for: keyframes, context: context)
let animation = CAKeyframeAnimation(keyPath: property.caLayerKeypath) let animation = CAKeyframeAnimation(keyPath: property.caLayerKeypath)
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
}
}
// Position animations define a `CGPath` curve that should be followed, // Position animations define a `CGPath` curve that should be followed,
// instead of animating directly between keyframe point values. // instead of animating directly between keyframe point values.

View File

@ -113,12 +113,6 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
let animation = CABasicAnimation(keyPath: event) let animation = CABasicAnimation(keyPath: event)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.fromValue = presentation()?.currentFrame animation.fromValue = presentation()?.currentFrame
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
}
}
return animation return animation
} }
return super.action(forKey: event) return super.action(forKey: event)

View File

@ -412,14 +412,14 @@ final public class AnimationView: AnimationViewBase {
self.f() self.f()
} }
} }
self.workaroundDisplayLink = CADisplayLink(target: WorkaroundDisplayLinkTarget { [weak self] in /*self.workaroundDisplayLink = CADisplayLink(target: WorkaroundDisplayLinkTarget { [weak self] in
let _ = self?.realtimeAnimationProgress let _ = self?.realtimeAnimationProgress
}, selector: #selector(WorkaroundDisplayLinkTarget.update)) }, selector: #selector(WorkaroundDisplayLinkTarget.update))
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond) let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
self.workaroundDisplayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps) self.workaroundDisplayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
} }
self.workaroundDisplayLink?.add(to: .main, forMode: .common) self.workaroundDisplayLink?.add(to: .main, forMode: .common)*/
} }
} else { } else {
if let workaroundDisplayLink = self.workaroundDisplayLink { if let workaroundDisplayLink = self.workaroundDisplayLink {
@ -1305,12 +1305,6 @@ final public class AnimationView: AnimationViewBase {
layerAnimation.fillMode = CAMediaTimingFillMode.both layerAnimation.fillMode = CAMediaTimingFillMode.both
layerAnimation.repeatCount = loopMode.caAnimationConfiguration.repeatCount layerAnimation.repeatCount = loopMode.caAnimationConfiguration.repeatCount
layerAnimation.autoreverses = loopMode.caAnimationConfiguration.autoreverses layerAnimation.autoreverses = loopMode.caAnimationConfiguration.autoreverses
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
layerAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
}
}
layerAnimation.isRemovedOnCompletion = false layerAnimation.isRemovedOnCompletion = false
if timeOffset != 0 { if timeOffset != 0 {