Skip to content

Commit

Permalink
Add more docs
Browse files Browse the repository at this point in the history
  • Loading branch information
onevcat committed Nov 1, 2021
1 parent 33f4b31 commit f217733
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 22 deletions.
68 changes: 66 additions & 2 deletions Source/APNGKit/APNGImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ import Delegate
/// Represents an APNG image object. This class loads an APNG file from disk or from some data object and provides a
/// high level interface for you to set some animation properties. Once you get an `APNGImage` instance, you can set
/// it to an `APNGImageView` to display it on screen.
///
/// ```swift
/// let image = try APNGImage(named: "your_image")
/// let imageView = APNGImageView(image: image)
/// view.addSubview(imageView)
/// ```
///
/// All the initializers throw an `APNGKitError` value if the image cannot be created. Check the error value to know the
/// detail. In some cases, it is possible to render the image as a static one. You can check the error's `normalImage`
/// for this case:
///
/// ```swift
/// do {
/// let image = try APNGImage(named: "my_image")
/// animatedImageView.image = image
/// } catch {
/// if let normalImage = error.apngError?.normalImage {
/// animatedImageView.staticImage = normalImage
/// } else {
/// animatedImageView.staticImage = nil
/// print("Error: \(error)")
/// }
/// }
/// ```
public class APNGImage {

/// The maximum size in memory which determines whether the decoded images should be cached or not.
Expand All @@ -20,10 +44,13 @@ public class APNGImage {

/// The duration of current loaded frames.
public enum Duration {
/// The loaded duration of current image, when the image frames are not yet fully decoded.
case loadedPartial(TimeInterval)
/// The full duration of the current image.
case full(TimeInterval)
}

// Internal decoder. It decodes the image file or data, and render each frame when required.
let decoder: APNGDecoder

/// A delegate called when all the image related information is prepared.
Expand All @@ -50,6 +77,7 @@ public class APNGImage {
/// repeat count and the animation will be played in loop forever.
public var numberOfPlays: Int?

// `numberOfPlays` == 0 also means loop forever.
var playForever: Bool { numberOfPlays == nil || numberOfPlays == 0 }

/// The number of frames in the image instance. It is the expected frame count in the image, which is defined by
Expand Down Expand Up @@ -91,13 +119,31 @@ public class APNGImage {
// same image in different APNG image views, create multiple instance instead.
weak var owner: AnyObject?

/// Creates an APNG image object using the named image file in the main bundle.
/// - Parameters:
/// - name: The name of the image file in the main bundle.
/// - decodingOptions: The decoding options being used while decoding the image data.
/// - Returns: The image object that best matches the given name.
///
/// This method guesses what is the image you want to load based on the given `name`. It searches the possible
/// combinations of file name, extensions and image scales in the bundle.
public convenience init(
named name: String,
decodingOptions: DecodingOptions = []
) throws {
try self.init(named: name, decodingOptions: decodingOptions, in: nil, subdirectory: nil)
}


/// Creates an APNG image object using the named image file in the specified bundle and subdirectory.
/// - Parameters:
/// - name: The name of the image file in the specified bundle.
/// - decodingOptions: The decoding options being used while decoding the image data.
/// - bundle: The bundle in which APNGKit should search in for the image.
/// - subpath: The subdirectory path in the bundle where the image is put.
/// - Returns: The image object that best matches the given name, bundle and subpath.
///
/// This method guesses what is the image you want to load based on the given `name`. It searches the possible
/// combinations of file name, extensions and image scales in the bundle and subpath.
public convenience init(
named name: String,
decodingOptions: DecodingOptions = [],
Expand All @@ -111,6 +157,12 @@ public class APNGImage {
try self.init(fileURL: resource.fileURL, scale: resource.scale, decodingOptions: decodingOptions)
}

/// Creates an APNG image object using the file path.
/// - Parameters:
/// - filePath: The path of APNG file.
/// - scale: The desired image scale. If not set, APNGKit will guess from the file name.
/// - decodingOptions: The decoding options being used while decoding the image data.
/// - Returns: The image object that loaded from the given file path.
public convenience init(
filePath: String,
scale: CGFloat? = nil,
Expand All @@ -119,7 +171,13 @@ public class APNGImage {
let fileURL = URL(fileURLWithPath: filePath)
try self.init(fileURL: fileURL, scale: scale, decodingOptions: decodingOptions)
}


/// Creates an APNG image object using the file URL.
/// - Parameters:
/// - fileURL: The URL of APNG file on disk.
/// - scale: The desired image scale. If not set, APNGKit will guess from the file name.
/// - decodingOptions: The decoding options being used while decoding the image data.
/// - Returns: The image object that loaded from the given file URL.
public init(
fileURL: URL,
scale: CGFloat? = nil,
Expand All @@ -142,6 +200,12 @@ public class APNGImage {
}
}

/// Creates an APNG image object using the give data object.
/// - Parameters:
/// - data: The data containing APNG information and frames.
/// - scale: The desired image scale. If not set, `1.0` is used.
/// - decodingOptions: The decoding options being used while decoding the image data.
/// - Returns: The image object that loaded from the given data.
public init(
data: Data,
scale: CGFloat = 1.0,
Expand Down
45 changes: 27 additions & 18 deletions Source/APNGKit/APNGImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,23 +252,31 @@ open class APNGImageView: PlatformView {
}
nextImage.decoder.renderNext()
case .fallbackToDefault(let defaultImage, let error):
onDecodingFrameError(.init(error: error, canFallbackToDefaultImage: true))
let scale = defaultImage?.recommendedLayerContentsScale(nextImage.scale) ?? screenScale
backingLayer.contentsScale = scale
backingLayer.contents = defaultImage?.layerContents(forContentsScale:scale)
stopAnimating()
onFallBackToDefaultImage()
fallbackTo(defaultImage, referenceScale: nextImage.scale, error: error)
case .defaultDecodingError(let error, let defaultImageError):
onDecodingFrameError(.init(error: error, canFallbackToDefaultImage: false))
backingLayer.contents = nil
stopAnimating()
onFallBackToDefaultImageFailed(defaultImageError)
defaultDecodingErrored(frameError: error, defaultImageError: defaultImageError)
}

invalidateIntrinsicContentSize()
}
}

private func fallbackTo(_ defaultImage: PlatformImage?, referenceScale: CGFloat, error: APNGKitError) {
onDecodingFrameError(.init(error: error, canFallbackToDefaultImage: true))
let scale = defaultImage?.recommendedLayerContentsScale(referenceScale) ?? screenScale
backingLayer.contentsScale = scale
backingLayer.contents = defaultImage?.layerContents(forContentsScale:scale)
stopAnimating()
onFallBackToDefaultImage()
}

private func defaultDecodingErrored(frameError: APNGKitError, defaultImageError: APNGKitError) {
onDecodingFrameError(.init(error: frameError, canFallbackToDefaultImage: false))
backingLayer.contents = nil
stopAnimating()
onFallBackToDefaultImageFailed(defaultImageError)
}

/// Starts the animation. Calling this method does nothing if the animation is already running.
open func startAnimating() {
guard !isAnimating else {
Expand Down Expand Up @@ -381,15 +389,9 @@ open class APNGImageView: PlatformView {
image.decoder.renderNext()

case .fallbackToDefault(let defaultImage, let error):
onDecodingFrameError(.init(error: error, canFallbackToDefaultImage: true))
backingLayer.contents = defaultImage?.layerContents(forContentsScale: image.scale)
stopAnimating()
onFallBackToDefaultImage()
fallbackTo(defaultImage, referenceScale: image.scale, error: error)
case .defaultDecodingError(let error, let defaultImageError):
onDecodingFrameError(.init(error: error, canFallbackToDefaultImage: false))
backingLayer.contents = nil
stopAnimating()
onFallBackToDefaultImageFailed(defaultImageError)
defaultDecodingErrored(frameError: error, defaultImageError: defaultImageError)
}
}

Expand Down Expand Up @@ -447,6 +449,13 @@ extension APNGImageView {
}

extension APNGKitError {

/// Treat the error as a recoverable one. Try to extract the normal version of image and return it is can be created
/// as a normal image.
///
/// When you get an error while initializing an `APNGImage`, you can try to access this property of the `APNGKitError`
/// to check if it is not an APNG image but a normal images supported on the platform. You can choose to set the
/// returned value to `APNGImageView.staticImage` to let the view displays a static normal image as a fallback.
public var normalImage: PlatformImage? {
guard let (data, scale) = self.normalImageData else {
return nil
Expand Down
20 changes: 19 additions & 1 deletion Source/APNGKit/APNGKitError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@
import Foundation
import ImageIO

/// The errors can be thrown or returned by APIs in APNGKit.
///
/// Each member in this type represents a type of reason for error during different image decoding or displaying phase.
/// Check the detail reason to know the details. In most cases, you are only care about one or two types of error, and
/// leave others falling to a default handling.
public enum APNGKitError: Error {
/// Errors happening during decoding the image data.
case decoderError(DecoderError)
/// Errors happening during creating the image.
case imageError(ImageError)

/// Other errors happening inside system and not directly related to APNGKit.
case internalError(Error)
}

extension APNGKitError {

/// Errors happening during decoding the image data.
public enum DecoderError {
case fileHandleCreatingFailed(URL, Error)
case fileHandleOperationFailed(FileHandle, Error)
Expand All @@ -36,13 +45,19 @@ extension APNGKitError {
case multipleAnimationControlChunk
}

/// Errors happening during creating the image.
public enum ImageError {
case resourceNotFound(name: String, bundle: Bundle)
case normalImageDataLoaded(data: Data, scale: CGFloat)
}
}

extension APNGKitError {

/// Returns the image data as a normal image if the error happens during creating image object.
///
/// When the image cannot be loaded as an APNG, but can be represented as a normal image, this returns its data and
/// a scale for the image.
public var normalImageData: (Data, CGFloat)? {
guard case .imageError(.normalImageDataLoaded(let data, let scale)) = self else {
return nil
Expand All @@ -52,6 +67,9 @@ extension APNGKitError {
}

extension Error {
/// Converts `self` to an `APNGKitError` if it is.
///
/// This is identical as `self as? APNGKitError`.
public var apngError: APNGKitError? { self as? APNGKitError }
}

Expand Down
23 changes: 22 additions & 1 deletion Source/APNGKit/DisplayTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,33 @@
import Foundation
import QuartzCore

/// Provides a timer to drive the animation.
///
/// The implementation of this protocol should make sure to not hold the timer's target. This allows the target not to
/// be held longer than it is needed. In other words, it should behave as a "weak timer".
public protocol DrivingTimer {

/// The current timestamp of the timer.
var timestamp: TimeInterval { get }

/// Invalidates the timer to prevent it from being fired again.
func invalidate()

/// The timer pause state. When `isPaused` is `true`, the timer should not fire an event. Setting it to `false`
/// should make the timer be valid again.
var isPaused: Bool { get set }

/// Creates a timer in a certain mode. The timer should call `action` in main thread every time the timer is fired.
/// However, it should not hold the `target` object, so as soon as `target` is released, this timer can be stopped
/// to prevent any retain cycle.
init(mode: RunLoop.Mode?, target: AnyObject, action: @escaping (TimeInterval) -> Void)
}

#if canImport(UIKit)
/// A timer driven by display link.
///
/// This class fires an event synchronized with the display loop. This prevents unnecessary check of animation status
/// and only update the image bounds to the display refreshing.
public class DisplayTimer: DrivingTimer {
// Exposed properties
public var timestamp: TimeInterval { displayLink.timestamp }
Expand All @@ -25,7 +44,7 @@ public class DisplayTimer: DrivingTimer {
set { displayLink.isPaused = newValue }
}

// Holder
// Holder, the underline display link.
private var displayLink: CADisplayLink!
private let action: (TimeInterval) -> Void
private weak var target: AnyObject?
Expand Down Expand Up @@ -82,10 +101,12 @@ public class NormalTimer: DrivingTimer {
}

private func createTimer() -> Timer {
// For macOS, read the refresh rate of display.
#if canImport(AppKit)
let displayMode = CGDisplayCopyDisplayMode(CGMainDisplayID())
let refreshRate = max(displayMode?.refreshRate ?? 60.0, 60.0)
#else
// In other cases, we assume a 60 FPS.
let refreshRate = 60.0
#endif

Expand Down

0 comments on commit f217733

Please sign in to comment.