diff --git a/Gifu.xcodeproj/project.pbxproj b/Gifu.xcodeproj/project.pbxproj index 960bee3..7a0d47b 100644 --- a/Gifu.xcodeproj/project.pbxproj +++ b/Gifu.xcodeproj/project.pbxproj @@ -233,7 +233,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Kaishin & Co"; TargetAttributes = { 009BD1351BBC7F6500FC982B = { @@ -335,7 +335,7 @@ DEVELOPMENT_TEAM = 5G38N4D8G2; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -363,7 +363,7 @@ DEVELOPMENT_TEAM = 5G38N4D8G2; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -386,6 +386,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -447,6 +448,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -511,7 +513,7 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -527,6 +529,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Debug; }; @@ -545,7 +548,7 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -561,6 +564,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Release; }; diff --git a/Package.swift b/Package.swift index 324436c..9fa3522 100644 --- a/Package.swift +++ b/Package.swift @@ -5,8 +5,8 @@ import PackageDescription let package = Package( name: "Gifu", platforms: [ - .iOS(.v12), - .tvOS(.v12), + .iOS(.v14), + .tvOS(.v14), .visionOS(.v1), ], products: [ diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 44d947c..28d0d27 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -24,3 +24,10 @@ let package = Package( ], swiftLanguageModes: [.v6] ) + +for target in package.targets { + target.swiftSettings = target.swiftSettings ?? [] + target.swiftSettings?.append(contentsOf: [ + .enableUpcomingFeature("ExistentialAny") + ]) +} diff --git a/Sources/Gifu/Classes/Animator.swift b/Sources/Gifu/Classes/Animator.swift index 3a8d032..761343f 100644 --- a/Sources/Gifu/Classes/Animator.swift +++ b/Sources/Gifu/Classes/Animator.swift @@ -1,6 +1,7 @@ import UIKit /// Responsible for parsing GIF data and decoding the individual frames. +@MainActor public class Animator { /// Total duration of one animation loop var loopDuration: TimeInterval { @@ -20,7 +21,7 @@ public class Animator { private var displayLinkInitialized: Bool = false /// A delegate responsible for displaying the GIF frames. - private weak var delegate: (any GIFAnimatable)! + private weak var delegate: (any GIFAnimatable)? /// Callback for when all the loops of the animation are done (never called for infinite loops) private var animationBlock: (() -> Void)? = nil @@ -31,6 +32,7 @@ public class Animator { /// Responsible for starting and stopping the animation. private lazy var displayLink: CADisplayLink = { [unowned self] in self.displayLinkInitialized = true + let display = CADisplayLink( target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.onScreenUpdate) @@ -71,7 +73,7 @@ public class Animator { store.shouldChangeFrame(with: displayLink.duration) { if $0 { - delegate.animatorHasNewFrame() + delegate?.animatorHasNewFrame() if store.isLoopFinished, let loopBlock { loopBlock() } @@ -88,8 +90,12 @@ public class Animator { /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. /// - parameter completionHandler: Completion callback function func prepareForAnimation( - withGIFNamed imageName: String, inBundle bundle: Bundle = .main, size: CGSize, - contentMode: UIView.ContentMode, loopCount: Int = 0, completionHandler: (() -> Void)? = nil + withGIFNamed imageName: String, + inBundle bundle: Bundle = .main, + size: CGSize, + contentMode: UIView.ContentMode, + loopCount: Int = 0, + completionHandler: (@Sendable () -> Void)? = nil ) { guard let extensionRemoved = imageName.components(separatedBy: ".")[safe: 0], let imagePath = bundle.url(forResource: extensionRemoved, withExtension: "gif"), @@ -101,7 +107,8 @@ public class Animator { size: size, contentMode: contentMode, loopCount: loopCount, - completionHandler: completionHandler) + completionHandler: completionHandler + ) } /// Prepares the animator instance for animation. @@ -116,7 +123,7 @@ public class Animator { size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, - completionHandler: (() -> Void)? = nil + completionHandler: (@Sendable () -> Void)? = nil ) { frameStore = FrameStore( data: imageData, @@ -136,12 +143,18 @@ public class Animator { displayLink.add(to: .main, forMode: RunLoop.Mode.common) } - deinit { - if displayLinkInitialized { + private func invalidateDisplayLink() { + Task { [displayLink] in displayLink.invalidate() } } + deinit { + MainActor.assumeIsolated { + invalidateDisplayLink() + } + } + /// Start animating. func startAnimating() { if frameStore?.isAnimatable ?? false { @@ -164,9 +177,13 @@ public class Animator { /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) func animate( - withGIFNamed imageName: String, size: CGSize, contentMode: UIView.ContentMode, - loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, - loopBlock: (() -> Void)? = nil + withGIFNamed imageName: String, + size: CGSize, + contentMode: UIView.ContentMode, + loopCount: Int = 0, + preparationBlock: (@Sendable () -> Void)? = nil, + animationBlock: (@Sendable () -> Void)? = nil, + loopBlock: (@Sendable () -> Void)? = nil ) { self.animationBlock = animationBlock self.loopBlock = loopBlock @@ -175,7 +192,8 @@ public class Animator { size: size, contentMode: contentMode, loopCount: loopCount, - completionHandler: preparationBlock) + completionHandler: preparationBlock + ) startAnimating() } @@ -189,9 +207,13 @@ public class Animator { /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) func animate( - withGIFData imageData: Data, size: CGSize, contentMode: UIView.ContentMode, loopCount: Int = 0, - preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, - loopBlock: (() -> Void)? = nil + withGIFData imageData: Data, + size: CGSize, + contentMode: UIView.ContentMode, + loopCount: Int = 0, + preparationBlock: (@Sendable () -> Void)? = nil, + animationBlock: (@Sendable () -> Void)? = nil, + loopBlock: (@Sendable () -> Void)? = nil ) { self.animationBlock = animationBlock self.loopBlock = loopBlock @@ -200,7 +222,8 @@ public class Animator { size: size, contentMode: contentMode, loopCount: loopCount, - completionHandler: preparationBlock) + completionHandler: preparationBlock + ) startAnimating() } @@ -232,5 +255,5 @@ private class DisplayLinkProxy { init(target: Animator) { self.target = target } /// Lets the target update the frame if needed. - @objc func onScreenUpdate() { target?.updateFrameIfNeeded() } + @MainActor @objc func onScreenUpdate() { target?.updateFrameIfNeeded() } } diff --git a/Sources/Gifu/Classes/FrameStore.swift b/Sources/Gifu/Classes/FrameStore.swift index bfe3900..1d5a16e 100644 --- a/Sources/Gifu/Classes/FrameStore.swift +++ b/Sources/Gifu/Classes/FrameStore.swift @@ -2,9 +2,9 @@ import ImageIO import UIKit /// Responsible for storing and updating the frames of a single GIF. -class FrameStore { +final class FrameStore: @unchecked Sendable { /// The strategy to use for frame cache. - enum FrameCachingStrategy: Equatable { + enum FrameCachingStrategy: Equatable, Sendable { // Cache only a given number of upcoming frames. case cacheUpcoming(Int) @@ -152,7 +152,7 @@ class FrameStore { // MARK: - Frames /// Loads the frames from an image source, resizes them, then caches them in `animatedFrames`. - func prepareFrames(_ completionHandler: (() -> Void)? = nil) { + func prepareFrames(_ completionHandler: (@Sendable () -> Void)? = nil) { frameCount = Int(CGImageSourceGetCount(imageSource)) lock.lock() animatedFrames.reserveCapacity(frameCount) @@ -228,8 +228,7 @@ extension FrameStore { /// Updates the frames by preloading new ones and replacing the previous frame with a placeholder. private func updateFrameCache() { if case let .cacheUpcoming(size) = cachingStrategy, - size < frameCount - 1 - { + size < frameCount - 1 { deleteCachedFrame(at: previousFrameIndex) } diff --git a/Sources/Gifu/Classes/GIFAnimatable.swift b/Sources/Gifu/Classes/GIFAnimatable.swift index 3426cd1..162383d 100644 --- a/Sources/Gifu/Classes/GIFAnimatable.swift +++ b/Sources/Gifu/Classes/GIFAnimatable.swift @@ -2,7 +2,8 @@ import Foundation import UIKit /// The protocol that view classes need to conform to to enable animated GIF support. -public protocol GIFAnimatable: AnyObject { +@MainActor +public protocol GIFAnimatable: AnyObject, Sendable { /// Responsible for managing the animation frames. var animator: Animator? { get set } @@ -16,11 +17,10 @@ public protocol GIFAnimatable: AnyObject { var contentMode: UIView.ContentMode { get set } } - /// A single-property protocol that animatable classes can optionally conform to. public protocol ImageContainer { /// Used for displaying the animation frames. - var image: UIImage? { get set } + @MainActor var image: UIImage? { get set } } extension GIFAnimatable where Self: ImageContainer { @@ -58,14 +58,22 @@ extension GIFAnimatable { /// - parameter preparationBlock: Callback for when preparation is done /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) - public func animate(withGIFNamed imageName: String, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { - animator?.animate(withGIFNamed: imageName, - size: frame.size, - contentMode: contentMode, - loopCount: loopCount, - preparationBlock: preparationBlock, - animationBlock: animationBlock, - loopBlock: loopBlock) + public func animate( + withGIFNamed imageName: String, + loopCount: Int = 0, + preparationBlock: (@Sendable () -> Void)? = nil, + animationBlock: (@Sendable () -> Void)? = nil, + loopBlock: (@Sendable () -> Void)? = nil + ) { + animator?.animate( + withGIFNamed: imageName, + size: frame.size, + contentMode: contentMode, + loopCount: loopCount, + preparationBlock: preparationBlock, + animationBlock: animationBlock, + loopBlock: loopBlock + ) } /// Prepare for animation and start animating immediately. @@ -75,14 +83,22 @@ extension GIFAnimatable { /// - parameter preparationBlock: Callback for when preparation is done /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) - public func animate(withGIFData imageData: Data, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { - animator?.animate(withGIFData: imageData, - size: frame.size, - contentMode: contentMode, - loopCount: loopCount, - preparationBlock: preparationBlock, - animationBlock: animationBlock, - loopBlock: loopBlock) + public func animate( + withGIFData imageData: Data, + loopCount: Int = 0, + preparationBlock: (@Sendable () -> Void)? = nil, + animationBlock: (@Sendable () -> Void)? = nil, + loopBlock: (@Sendable () -> Void)? = nil + ) { + animator?.animate( + withGIFData: imageData, + size: frame.size, + contentMode: contentMode, + loopCount: loopCount, + preparationBlock: preparationBlock, + animationBlock: animationBlock, + loopBlock: loopBlock + ) } /// Prepare for animation and start animating immediately. @@ -92,16 +108,29 @@ extension GIFAnimatable { /// - parameter preparationBlock: Callback for when preparation is done /// - parameter animationBlock: Callback for when all the loops of the animation are done (never called for infinite loops) /// - parameter loopBlock: Callback for when a loop is done (at the end of each loop) - public func animate(withGIFURL imageURL: URL, loopCount: Int = 0, preparationBlock: (() -> Void)? = nil, animationBlock: (() -> Void)? = nil, loopBlock: (() -> Void)? = nil) { + public func animate( + withGIFURL imageURL: URL, + loopCount: Int = 0, + preparationBlock: (@Sendable () -> Void)? = nil, + animationBlock: (@Sendable () -> Void)? = nil, + loopBlock: (@Sendable () -> Void)? = nil + ) { let session = URLSession.shared let task = session.dataTask(with: imageURL) { (data, response, error) in switch (data, response, error) { case (.none, _, let error?): - print("Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString) + print( + "Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString) case (let data?, _, _): DispatchQueue.main.async { - self.animate(withGIFData: data, loopCount: loopCount, preparationBlock: preparationBlock, animationBlock: animationBlock, loopBlock: loopBlock) + self.animate( + withGIFData: data, + loopCount: loopCount, + preparationBlock: preparationBlock, + animationBlock: animationBlock, + loopBlock: loopBlock + ) } default: () } @@ -115,14 +144,18 @@ extension GIFAnimatable { /// - parameter imageName: The file name of the GIF in the main bundle. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. /// - parameter completionHandler: Callback for when preparation is done - public func prepareForAnimation(withGIFNamed imageName: String, - loopCount: Int = 0, - completionHandler: (() -> Void)? = nil) { - animator?.prepareForAnimation(withGIFNamed: imageName, - size: frame.size, - contentMode: contentMode, - loopCount: loopCount, - completionHandler: completionHandler) + public func prepareForAnimation( + withGIFNamed imageName: String, + loopCount: Int = 0, + completionHandler: (@Sendable () -> Void)? = nil + ) { + animator?.prepareForAnimation( + withGIFNamed: imageName, + size: frame.size, + contentMode: contentMode, + loopCount: loopCount, + completionHandler: completionHandler + ) } /// Prepares the animator instance for animation. @@ -130,18 +163,22 @@ extension GIFAnimatable { /// - parameter imageData: GIF image data. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. /// - parameter completionHandler: Callback for when preparation is done - public func prepareForAnimation(withGIFData imageData: Data, - loopCount: Int = 0, - completionHandler: (() -> Void)? = nil) { - if var imageContainer = self as? any ImageContainer { + public func prepareForAnimation( + withGIFData imageData: Data, + loopCount: Int = 0, + completionHandler: (@Sendable () -> Void)? = nil + ) { + if var imageContainer = self as? (any ImageContainer) { imageContainer.image = UIImage(data: imageData) } - animator?.prepareForAnimation(withGIFData: imageData, - size: frame.size, - contentMode: contentMode, - loopCount: loopCount, - completionHandler: completionHandler) + animator?.prepareForAnimation( + withGIFData: imageData, + size: frame.size, + contentMode: contentMode, + loopCount: loopCount, + completionHandler: completionHandler + ) } /// Prepares the animator instance for animation. @@ -149,19 +186,24 @@ extension GIFAnimatable { /// - parameter imageURL: GIF image url. /// - parameter loopCount: Desired number of loops, <= 0 for infinite loop. /// - parameter completionHandler: Callback for when preparation is done - public func prepareForAnimation(withGIFURL imageURL: URL, - loopCount: Int = 0, - completionHandler: (() -> Void)? = nil) { + public func prepareForAnimation( + withGIFURL imageURL: URL, + loopCount: Int = 0, + completionHandler: (@Sendable () -> Void)? = nil + ) { let session = URLSession.shared let task = session.dataTask(with: imageURL) { (data, response, error) in switch (data, response, error) { case (.none, _, let error?): - print("Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString) + print( + "Error downloading gif:", error.localizedDescription, "at url:", imageURL.absoluteString) case (let data?, _, _): DispatchQueue.main.async { - self.prepareForAnimation(withGIFData: data, - loopCount: loopCount, - completionHandler: completionHandler) + self.prepareForAnimation( + withGIFData: data, + loopCount: loopCount, + completionHandler: completionHandler + ) } default: () } @@ -209,7 +251,7 @@ extension GIFAnimatable { /// Updates the image with a new frame if necessary. public func updateImageIfNeeded() { - if var imageContainer = self as? any ImageContainer { + if var imageContainer = self as? (any ImageContainer) { let container = imageContainer imageContainer.image = activeFrame ?? container.image } else { diff --git a/Sources/Gifu/Classes/GIFImageView.swift b/Sources/Gifu/Classes/GIFImageView.swift index 683b702..bf09926 100644 --- a/Sources/Gifu/Classes/GIFImageView.swift +++ b/Sources/Gifu/Classes/GIFImageView.swift @@ -4,7 +4,7 @@ import UIKit public class GIFImageView: UIImageView, GIFAnimatable { /// A lazy animator. public lazy var animator: Animator? = { - return Animator(withDelegate: self) + Animator(withDelegate: self) }() /// Layer delegate method called periodically by the layer. **Should not** be called manually. diff --git a/Sources/Gifu/Helpers/ImageSourceHelpers.swift b/Sources/Gifu/Helpers/ImageSourceHelpers.swift index 16a1dfa..5f79637 100755 --- a/Sources/Gifu/Helpers/ImageSourceHelpers.swift +++ b/Sources/Gifu/Helpers/ImageSourceHelpers.swift @@ -1,6 +1,7 @@ import ImageIO import MobileCoreServices import UIKit +import UniformTypeIdentifiers typealias GIFProperties = [String: Double] @@ -69,7 +70,8 @@ extension CGImageSource { /// /// - returns: A boolean value that is `true` if the image source contains animated GIF data. var isAnimatedGIF: Bool { - let isTypeGIF = UTTypeConformsTo(CGImageSourceGetType(self) ?? "" as CFString, kUTTypeGIF) + let type = (CGImageSourceGetType(self) as? String) ?? "" + let isTypeGIF = UTType(type)?.conforms(to: .gif) let imageCount = CGImageSourceGetCount(self) return isTypeGIF != false && imageCount > 1 }