From 81dc7996c60846038534315c40c9f5c1af824757 Mon Sep 17 00:00:00 2001 From: noppe Date: Wed, 4 Dec 2024 01:36:46 +0900 Subject: [PATCH] Use UIUpdateLink in iOS18 --- Package.swift | 6 +++ .../BaseClass/AnimatableCGImageView.swift | 42 +++++++++------ Sources/UpdateLink/BackportUpdateLink.swift | 54 +++++++++++++++++++ .../DisplayLinkProxy.swift | 0 Sources/UpdateLink/UIUpdateLink+.swift | 15 ++++++ Sources/UpdateLink/UpdateLink.swift | 15 ++++++ 6 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 Sources/UpdateLink/BackportUpdateLink.swift rename Sources/{AnimatedImage/Entity => UpdateLink}/DisplayLinkProxy.swift (100%) create mode 100644 Sources/UpdateLink/UIUpdateLink+.swift create mode 100644 Sources/UpdateLink/UpdateLink.swift diff --git a/Package.swift b/Package.swift index 397467d..001bd05 100644 --- a/Package.swift +++ b/Package.swift @@ -19,8 +19,14 @@ let package = Package( targets: [ .target( name: "AnimatedImage", + dependencies: [ + "UpdateLink" + ], resources: [.copy("Resources/PrivacyInfo.xcprivacy")] ), + .target( + name: "UpdateLink" + ), .target( name: "AnimatedImageSwiftUI", dependencies: [ diff --git a/Sources/AnimatedImage/BaseClass/AnimatableCGImageView.swift b/Sources/AnimatedImage/BaseClass/AnimatableCGImageView.swift index f1fb0a2..459ae4d 100644 --- a/Sources/AnimatedImage/BaseClass/AnimatableCGImageView.swift +++ b/Sources/AnimatedImage/BaseClass/AnimatableCGImageView.swift @@ -1,28 +1,40 @@ public import UIKit +import UpdateLink -open class AnimatableCGImageView: CGImageView, DisplayLinkTarget { - private var displayLink: CADisplayLink? = nil +open class AnimatableCGImageView: CGImageView { + var updateLink: (any UpdateLink)! - open func startAnimating() { - stopAnimating() - displayLink = CADisplayLink( - target: DisplayLinkProxy(target: self), - selector: #selector(DisplayLinkProxy.updateContents) + public override init(frame: CGRect) { + super.init(frame: frame) + if #available(iOS 18.0, *) { + updateLink = UIUpdateLink(view: self) + } else { + updateLink = BackportUpdateLink(view: self) + } + updateLink.isEnabled = true + updateLink.preferredFrameRateRange = CAFrameRateRange( + minimum: 1, + maximum: 60 ) - displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 1, maximum: 60) - displayLink?.add(to: .main, forMode: .common) + updateLink.addAction(handler: { [unowned self] _, info in + willUpdateContents(&contents, for: info.modelTime) + }) + updateLink.requiresContinuousUpdates = true } - open func stopAnimating() { - displayLink?.invalidate() - displayLink = nil + @MainActor public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - func updateContents(_ displayLink: CADisplayLink) { - willUpdateContents(&contents, for: displayLink.targetTimestamp) + open func startAnimating() { + updateLink.isEnabled = true + } + + open func stopAnimating() { + updateLink.isEnabled = false } open func willUpdateContents(_ contents: inout CGImage?, for targetTimestamp: TimeInterval) { - } } + diff --git a/Sources/UpdateLink/BackportUpdateLink.swift b/Sources/UpdateLink/BackportUpdateLink.swift new file mode 100644 index 0000000..2402787 --- /dev/null +++ b/Sources/UpdateLink/BackportUpdateLink.swift @@ -0,0 +1,54 @@ +import UIKit + +public final class BackportUpdateLink: UpdateLink, DisplayLinkTarget { + private var displayLink: CADisplayLink! + + public init(view: UIView) { + displayLink = CADisplayLink( + target: DisplayLinkProxy(target: self), + selector: #selector(DisplayLinkProxy.updateContents) + ) + } + + public var isEnabled: Bool { + get { !displayLink.isPaused } + set { displayLink.isPaused = !newValue } + } + + public var requiresContinuousUpdates: Bool = false { + didSet { + if requiresContinuousUpdates { + displayLink.add(to: .main, forMode: .common) + } else { + displayLink.remove(from: .main, forMode: .common) + } + } + } + + public var preferredFrameRateRange: CAFrameRateRange { + get { displayLink?.preferredFrameRateRange ?? .default } + set { displayLink?.preferredFrameRateRange = newValue } + } + + private var handlers: [(any UpdateLink, any UpdateInfo) -> Void] = [] + public func addAction( + handler: @escaping (any UpdateLink, any UpdateInfo) -> Void + ) { + handlers.append(handler) + } + + func updateContents(_ displayLink: CADisplayLink) { + let info = BackportUpdateInfo(modelTime: displayLink.targetTimestamp) + handlers.forEach { action in + action(self, info) + } + } +} + +final class BackportUpdateInfo: UpdateInfo { + var modelTime: TimeInterval + + init(modelTime: TimeInterval) { + self.modelTime = modelTime + } +} diff --git a/Sources/AnimatedImage/Entity/DisplayLinkProxy.swift b/Sources/UpdateLink/DisplayLinkProxy.swift similarity index 100% rename from Sources/AnimatedImage/Entity/DisplayLinkProxy.swift rename to Sources/UpdateLink/DisplayLinkProxy.swift diff --git a/Sources/UpdateLink/UIUpdateLink+.swift b/Sources/UpdateLink/UIUpdateLink+.swift new file mode 100644 index 0000000..7856f8e --- /dev/null +++ b/Sources/UpdateLink/UIUpdateLink+.swift @@ -0,0 +1,15 @@ +import UIKit + +@available(iOS 18.0, *) +extension UIUpdateInfo: UpdateInfo {} + +@available(iOS 18.0, *) +extension UIUpdateLink: UpdateLink { + public func addAction( + handler: @escaping (any UpdateLink, any UpdateInfo) -> Void + ) { + addAction { (link: UIUpdateLink, info: UIUpdateInfo) in + handler(link, info) + } + } +} diff --git a/Sources/UpdateLink/UpdateLink.swift b/Sources/UpdateLink/UpdateLink.swift new file mode 100644 index 0000000..e30bf28 --- /dev/null +++ b/Sources/UpdateLink/UpdateLink.swift @@ -0,0 +1,15 @@ +import UIKit + +@MainActor +public protocol UpdateLink: AnyObject { + var isEnabled: Bool { get set } + var requiresContinuousUpdates: Bool { get set } + var preferredFrameRateRange: CAFrameRateRange { get set } + func addAction(handler: @escaping (any UpdateLink, any UpdateInfo) -> Void) +} + +@MainActor +public protocol UpdateInfo: AnyObject { + var modelTime: TimeInterval { get } +} +