Support AnimatedImage on watchOS - Using WatchKit bridge (#22)

* Temp for watchOS AnimatedImage support, using massive private API, still contains small issues

* Update the hack for wacthKit experienment, fix the retain cycle issue that cause WKInterfaceImage not dealloc

* Solve the merge conflict and try again

* Add support for contentMode

* Fix the SDAnimatedImageInterface first appear shows empty issues

* Fix the scale factor support for SDAniamtedImageInterface

* Fix the compile issue on other platforms

* Stop animtiong when dismantle for watchOS AnimatedImage

* Fix the issue that stopAnimating does not stop :)

* Fix the warning because of Apple's bug

* Use macro to integrate the watchOS Animation solution

* Refactory code to fix that calling sd_setImage(with:) multiple times issues

* Support to custom loop count on watchOS AnimatedImage

* Fix the CocoaPods issues which does not have umbrella headers

* Update some of the documentations

* Try to solve the SwiftPM issue because it does not support mixed Objective-C and Swift code, really suck, Apple :)

* Fix travis CI script to only build Carthage. Swift cli build can not works on Objective-C code import syntax, but works on Xcode
This commit is contained in:
DreamPiggy 2019-10-22 01:09:57 +08:00 committed by GitHub
parent f056456f15
commit 7e21926830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 474 additions and 59 deletions

View File

@ -33,4 +33,5 @@ script:
- pod install --project-directory=Example
- xcodebuild build -workspace Example/SDWebImageSwiftUI.xcworkspace -scheme SDWebImageSwiftUIDemo -sdk iphonesimulator -destination 'name=iPhone 8' ONLY_ACTIVE_ARCH=NO | xcpretty -c
- swift build
- carthage update --platform iOS
- xcodebuild build -project SDWebImageSwiftUI.xcodeproj -scheme 'SDWebImageSwiftUI' -sdk iphonesimulator -configuration Debug | xcpretty -c

View File

@ -1 +1 @@
github "SDWebImage/SDWebImage" "5.1.0"
github "SDWebImage/SDWebImage" "5.2.3"

View File

@ -11,7 +11,7 @@ PODS:
- SDWebImage (5.2.3):
- SDWebImage/Core (= 5.2.3)
- SDWebImage/Core (5.2.3)
- SDWebImageSwiftUI (0.3.2):
- SDWebImageSwiftUI (0.3.3):
- SDWebImage (~> 5.1)
- SDWebImageWebPCoder (0.2.5):
- libwebp (~> 1.0)
@ -34,7 +34,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
libwebp: 057912d6d0abfb6357d8bb05c0ea470301f5d61e
SDWebImage: 46a7f73228f84ce80990c786e4372cf4db5875ce
SDWebImageSwiftUI: a8a03ef596dde2e9668a76794f6c59d194289bb0
SDWebImageSwiftUI: 2284857313ca5085ab7b5310d372420d23c0817f
SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987
PODFILE CHECKSUM: 3fb06a5173225e197f3a4bf2be7e5586a693257a

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -67,6 +67,9 @@ struct ContentView: View {
Button(action: { self.reloadCache() }) {
Text("Reload")
}
Button(action: { self.switchView() }) {
Text("Switch")
}
}
#endif
}
@ -76,7 +79,6 @@ struct ContentView: View {
ForEach(imageURLs) { url in
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
HStack {
#if os(iOS) || os(tvOS) || os(macOS)
if self.animated {
AnimatedImage(url: URL(string:url))
.resizable()
@ -88,12 +90,6 @@ struct ContentView: View {
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
}
#else
WebImage(url: URL(string:url))
.resizable()
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
#endif
Text((url as NSString).lastPathComponent)
}
}

View File

@ -33,7 +33,7 @@ struct DetailView: View {
contentView()
}
#endif
#if os(macOS)
#if os(macOS) || os(watchOS)
if animated {
contentView()
.contextMenu {
@ -45,16 +45,12 @@ struct DetailView: View {
contentView()
}
#endif
#if os(watchOS)
contentView()
#endif
Spacer()
}
}
func contentView() -> some View {
HStack {
#if os(iOS) || os(tvOS) || os(macOS)
if animated {
AnimatedImage(url: URL(string:url), options: [.progressiveLoad], isAnimating: $isAnimating)
.onProgress(perform: { (receivedSize, expectedSize) in
@ -79,18 +75,6 @@ struct DetailView: View {
.resizable()
.scaledToFit()
}
#else
WebImage(url: URL(string:url), options: [.progressiveLoad])
.onProgress(perform: { (receivedSize, expectedSize) in
if (expectedSize >= 0) {
self.progress = CGFloat(receivedSize) / CGFloat(expectedSize)
} else {
self.progress = 1
}
})
.resizable()
.scaledToFit()
#endif
}
}
}

View File

@ -24,7 +24,16 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "SDWebImageSwiftUI",
dependencies: ["SDWebImage", "SDWebImageSwiftUIObjC"],
path: "SDWebImageSwiftUI/Classes",
exclude: ["ObjC"]
),
// This is implementation detail because SwiftPM does not support mixed Objective-C/Swift code, don't dependent this target
.target(
name: "SDWebImageSwiftUIObjC",
dependencies: ["SDWebImage"],
path: "SDWebImageSwiftUI/Classes"),
path: "SDWebImageSwiftUI/Classes/ObjC",
publicHeadersPath: "."
)
]
)

View File

@ -26,7 +26,7 @@ Which aims to provide a better support for SwiftUI users.
s.tvos.deployment_target = '13.0'
s.watchos.deployment_target = '6.0'
s.source_files = 'SDWebImageSwiftUI/Classes/**/*'
s.source_files = 'SDWebImageSwiftUI/Classes/**/*', 'SDWebImageSwiftUI/Module/*.h'
s.frameworks = 'SwiftUI'
s.dependency 'SDWebImage', '~> 5.1'

View File

@ -7,6 +7,14 @@
objects = {
/* Begin PBXBuildFile section */
324F61C7235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */; settings = {ATTRIBUTES = (Public, ); }; };
324F61C8235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */; settings = {ATTRIBUTES = (Public, ); }; };
324F61C9235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */; settings = {ATTRIBUTES = (Public, ); }; };
324F61CA235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */; settings = {ATTRIBUTES = (Public, ); }; };
324F61CB235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; };
324F61CC235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; };
324F61CD235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; };
324F61CE235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */; };
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; };
@ -89,6 +97,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDAnimatedImageInterface.h; sourceTree = "<group>"; };
324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAnimatedImageInterface.m; sourceTree = "<group>"; };
326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = "<group>"; };
32C43DCC22FD540D00BE87F5 /* SDWebImageSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDWebImageSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
32C43DDC22FD54C600BE87F5 /* ImageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = "<group>"; };
@ -142,6 +152,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
324F61C4235E07EC003973B8 /* ObjC */ = {
isa = PBXGroup;
children = (
324F61C5235E07EC003973B8 /* SDAnimatedImageInterface.h */,
324F61C6235E07EC003973B8 /* SDAnimatedImageInterface.m */,
);
path = ObjC;
sourceTree = "<group>";
};
32C43DC222FD540D00BE87F5 = {
isa = PBXGroup;
children = (
@ -175,6 +194,7 @@
32C43DDB22FD54C600BE87F5 /* Classes */ = {
isa = PBXGroup;
children = (
324F61C4235E07EC003973B8 /* ObjC */,
32C43DDC22FD54C600BE87F5 /* ImageManager.swift */,
32C43DDE22FD54C600BE87F5 /* WebImage.swift */,
32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */,
@ -202,6 +222,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
324F61C7235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */,
32C43DE622FD54CD00BE87F5 /* SDWebImageSwiftUI.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -210,6 +231,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
324F61C8235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */,
32C43E2222FD583A00BE87F5 /* SDWebImageSwiftUI.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -218,6 +240,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
324F61C9235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */,
32C43E2322FD583B00BE87F5 /* SDWebImageSwiftUI.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -226,6 +249,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
324F61CA235E07EC003973B8 /* SDAnimatedImageInterface.h in Headers */,
32C43E2422FD583C00BE87F5 /* SDWebImageSwiftUI.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -320,15 +344,19 @@
TargetAttributes = {
32C43DCB22FD540D00BE87F5 = {
CreatedOnToolsVersion = 11.0;
LastSwiftMigration = 1100;
};
32C43DF322FD57FD00BE87F5 = {
CreatedOnToolsVersion = 11.0;
LastSwiftMigration = 1100;
};
32C43E0022FD581400BE87F5 = {
CreatedOnToolsVersion = 11.0;
LastSwiftMigration = 1100;
};
32C43E0D22FD581C00BE87F5 = {
CreatedOnToolsVersion = 11.0;
LastSwiftMigration = 1100;
};
};
};
@ -394,6 +422,7 @@
326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */,
32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */,
324F61CB235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -406,6 +435,7 @@
326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */,
32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */,
324F61CC235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -418,6 +448,7 @@
326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */,
32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */,
324F61CD235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -430,6 +461,7 @@
326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */,
32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */,
32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */,
324F61CE235E07EC003973B8 /* SDAnimatedImageInterface.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -565,6 +597,7 @@
32C43DD522FD540D00BE87F5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
@ -585,6 +618,7 @@
PRODUCT_NAME = SDWebImageSwiftUI;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -593,6 +627,7 @@
32C43DD622FD540D00BE87F5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
@ -621,6 +656,7 @@
32C43DFA22FD57FD00BE87F5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
@ -643,6 +679,7 @@
PRODUCT_NAME = SDWebImageSwiftUI;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
@ -650,6 +687,7 @@
32C43DFB22FD57FD00BE87F5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
@ -679,6 +717,7 @@
32C43E0722FD581400BE87F5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
@ -699,6 +738,7 @@
PRODUCT_NAME = SDWebImageSwiftUI;
SDKROOT = appletvos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
};
@ -707,6 +747,7 @@
32C43E0822FD581400BE87F5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
@ -736,6 +777,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
@ -756,6 +798,7 @@
PRODUCT_NAME = SDWebImageSwiftUI;
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
};
@ -765,6 +808,7 @@
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@ -8,13 +8,13 @@
import SwiftUI
import SDWebImage
#if !os(watchOS)
#if canImport(SDWebImageSwiftUIObjC)
import SDWebImageSwiftUIObjC
#endif
// Data Binding Object
final class AnimatedImageModel : ObservableObject {
@Published var image: PlatformImage?
@Published var url: URL?
@Published var successBlock: ((PlatformImage, SDImageCacheType) -> Void)?
@Published var failureBlock: ((Error) -> Void)?
@Published var progressBlock: ((Int, Int) -> Void)?
@ -38,12 +38,23 @@ final class AnimatedImageConfiguration: ObservableObject {
@Published var customLoopCount: Int?
}
// Convenient
#if os(watchOS)
public typealias AnimatedImageViewWrapper = SDAnimatedImageInterface
extension SDAnimatedImageInterface {
var wrapped: SDAnimatedImageInterface {
return self
}
}
#endif
// View
public struct AnimatedImage : PlatformViewRepresentable {
@ObservedObject var imageModel = AnimatedImageModel()
@ObservedObject var imageLayout = AnimatedImageLayout()
@ObservedObject var imageConfiguration = AnimatedImageConfiguration()
var url: URL?
var placeholder: PlatformImage?
var webOptions: SDWebImageOptions = []
var webContext: [SDWebImageContextOption : Any]? = nil
@ -72,7 +83,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
self.placeholder = placeholder
self.webOptions = options
self.webContext = context
self.imageModel.url = url
self.url = url
}
/// Create an animated image with name and bundle.
@ -90,7 +101,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
/// - Parameter isAnimating: The binding for animation control
public init(name: String, bundle: Bundle? = nil, isAnimating: Binding<Bool>) {
self._isAnimating = isAnimating
#if os(macOS)
#if os(macOS) || os(watchOS)
let image = SDAnimatedImage(named: name, in: bundle)
#else
let image = SDAnimatedImage(named: name, in: bundle, compatibleWith: nil)
@ -117,8 +128,10 @@ public struct AnimatedImage : PlatformViewRepresentable {
#if os(macOS)
public typealias NSViewType = AnimatedImageViewWrapper
#else
#elseif os(iOS) || os(tvOS)
public typealias UIViewType = AnimatedImageViewWrapper
#elseif os(watchOS)
public typealias WKInterfaceObjectType = AnimatedImageViewWrapper
#endif
#if os(macOS)
@ -133,7 +146,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
public static func dismantleNSView(_ nsView: AnimatedImageViewWrapper, coordinator: ()) {
dismantleView(nsView, coordinator: coordinator)
}
#else
#elseif os(iOS) || os(tvOS)
public func makeUIView(context: UIViewRepresentableContext<AnimatedImage>) -> AnimatedImageViewWrapper {
makeView(context: context)
}
@ -145,6 +158,18 @@ public struct AnimatedImage : PlatformViewRepresentable {
public static func dismantleUIView(_ uiView: AnimatedImageViewWrapper, coordinator: ()) {
dismantleView(uiView, coordinator: coordinator)
}
#elseif os(watchOS)
public func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<AnimatedImage>) -> AnimatedImageViewWrapper {
makeView(context: context)
}
public func updateWKInterfaceObject(_ wkInterfaceObject: AnimatedImageViewWrapper, context: WKInterfaceObjectRepresentableContext<AnimatedImage>) {
updateView(wkInterfaceObject, context: context)
}
public static func dismantleWKInterfaceObject(_ wkInterfaceObject: AnimatedImageViewWrapper, coordinator: ()) {
dismantleView(wkInterfaceObject, coordinator: coordinator)
}
#endif
func makeView(context: PlatformViewRepresentableContext<AnimatedImage>) -> AnimatedImageViewWrapper {
@ -152,15 +177,23 @@ public struct AnimatedImage : PlatformViewRepresentable {
}
func updateView(_ view: AnimatedImageViewWrapper, context: PlatformViewRepresentableContext<AnimatedImage>) {
view.wrapped.image = imageModel.image
if let url = imageModel.url {
view.wrapped.sd_setImage(with: url, placeholderImage: placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
self.imageModel.progressBlock?(receivedSize, expectedSize)
}) { (image, error, cacheType, _) in
if let image = image {
self.imageModel.successBlock?(image, cacheType)
} else {
self.imageModel.failureBlock?(error ?? NSError())
if let image = imageModel.image {
#if os(watchOS)
view.wrapped.setImage(image)
#else
view.wrapped.image = image
#endif
} else {
if let url = url {
view.wrapped.sd_setImage(with: url, placeholderImage: placeholder, options: webOptions, context: webContext, progress: { (receivedSize, expectedSize, _) in
self.imageModel.progressBlock?(receivedSize, expectedSize)
}) { (image, error, cacheType, _) in
self.imageModel.image = image
if let image = image {
self.imageModel.successBlock?(image, cacheType)
} else {
self.imageModel.failureBlock?(error ?? NSError())
}
}
}
}
@ -169,7 +202,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
if self.isAnimating != view.wrapped.animates {
view.wrapped.animates = self.isAnimating
}
#else
#elseif os(iOS) || os(tvOS)
if self.isAnimating != view.wrapped.isAnimating {
if self.isAnimating {
view.wrapped.startAnimating()
@ -177,6 +210,12 @@ public struct AnimatedImage : PlatformViewRepresentable {
view.wrapped.stopAnimating()
}
}
#elseif os(watchOS)
if self.isAnimating {
view.wrapped.startAnimating()
} else {
view.wrapped.stopAnimating()
}
#endif
configureView(view, context: context)
@ -202,19 +241,24 @@ public struct AnimatedImage : PlatformViewRepresentable {
case .fit:
#if os(macOS)
view.wrapped.imageScaling = .scaleProportionallyUpOrDown
#else
#elseif os(iOS) || os(tvOS)
view.wrapped.contentMode = .scaleAspectFit
#elseif os(watchOS)
view.wrapped.setContentMode(.aspectFit)
#endif
case .fill:
#if os(macOS)
view.wrapped.imageScaling = .scaleAxesIndependently
#else
#elseif os(iOS) || os(tvOS)
view.wrapped.contentMode = .scaleToFill
#elseif os(watchOS)
view.wrapped.setContentMode(.fill)
#endif
}
// Animated Image does not support resizing mode and rendering mode
if let image = view.wrapped.image, !image.sd_isAnimated, !image.conforms(to: SDAnimatedImageProtocol.self) {
if let image = imageModel.image, !image.sd_isAnimated, !image.conforms(to: SDAnimatedImageProtocol.self) {
var image = image
// ResizingMode
if let resizingMode = imageLayout.resizingMode {
#if os(macOS)
@ -228,14 +272,24 @@ public struct AnimatedImage : PlatformViewRepresentable {
view.wrapped.image?.resizingMode = .stretch
view.wrapped.image?.capInsets = capInsets
#else
view.wrapped.image = view.wrapped.image?.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
image = image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
#if os(iOS) || os(tvOS)
view.wrapped.image = image
#elseif os(watchOS)
view.wrapped.setImage(image)
#endif
#endif
case .tile:
#if os(macOS)
view.wrapped.image?.resizingMode = .tile
view.wrapped.image?.capInsets = capInsets
#else
view.wrapped.image = view.wrapped.image?.resizableImage(withCapInsets: capInsets, resizingMode: .tile)
image = image.resizableImage(withCapInsets: capInsets, resizingMode: .tile)
#if os(iOS) || os(tvOS)
view.wrapped.image = image
#elseif os(watchOS)
view.wrapped.setImage(image)
#endif
#endif
@unknown default:
// Future cases, not implements
@ -250,13 +304,23 @@ public struct AnimatedImage : PlatformViewRepresentable {
#if os(macOS)
view.wrapped.image?.isTemplate = true
#else
view.wrapped.image = view.wrapped.image?.withRenderingMode(.alwaysTemplate)
image = image.withRenderingMode(.alwaysTemplate)
#if os(iOS) || os(tvOS)
view.wrapped.image = image
#elseif os(watchOS)
view.wrapped.setImage(image)
#endif
#endif
case .original:
#if os(macOS)
view.wrapped.image?.isTemplate = false
#else
view.wrapped.image = view.wrapped.image?.withRenderingMode(.alwaysOriginal)
image = image.withRenderingMode(.alwaysOriginal)
#if os(iOS) || os(tvOS)
view.wrapped.image = image
#elseif os(watchOS)
view.wrapped.setImage(image)
#endif
#endif
@unknown default:
// Future cases, not implements
@ -265,6 +329,7 @@ public struct AnimatedImage : PlatformViewRepresentable {
}
}
#if os(macOS) || os(iOS) || os(tvOS)
// Interpolation
if let interpolation = imageLayout.interpolation {
switch interpolation {
@ -295,9 +360,11 @@ public struct AnimatedImage : PlatformViewRepresentable {
view.setNeedsLayout()
view.setNeedsDisplay()
#endif
#endif
}
func configureView(_ view: AnimatedImageViewWrapper, context: PlatformViewRepresentableContext<AnimatedImage>) {
#if os(macOS) || os(iOS) || os(tvOS)
// IncrementalLoad
if let incrementalLoad = imageConfiguration.incrementalLoad {
view.wrapped.shouldIncrementalLoad = incrementalLoad
@ -319,6 +386,14 @@ public struct AnimatedImage : PlatformViewRepresentable {
// disable custom loop count
view.wrapped.shouldCustomLoopCount = false
}
#elseif os(watchOS)
if let customLoopCount = imageConfiguration.customLoopCount {
view.wrapped.setAnimationRepeatCount(customLoopCount as NSNumber)
} else {
// disable custom loop count
view.wrapped.setAnimationRepeatCount(nil)
}
#endif
}
}
@ -417,9 +492,11 @@ extension AnimatedImage {
}
/// Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is nil.
// `0` or nil means automatically adjust by calculating current memory usage.
// `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU)
// `UInt.max` means cache all the buffer. (Lowest CPU and Highest Memory)
///
/// `0` or nil means automatically adjust by calculating current memory usage.
/// `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU)
/// `UInt.max` means cache all the buffer. (Lowest CPU and Highest Memory)
/// - Warning: watchOS does not implementes.
/// - Parameter bufferSize: The max buffer size
public func maxBufferSize(_ bufferSize: UInt?) -> AnimatedImage {
imageConfiguration.maxBufferSize = bufferSize
@ -429,6 +506,7 @@ extension AnimatedImage {
/// Whehter or not to enable incremental image load for animated image. See `SDAnimatedImageView` for detailed explanation for this.
/// - Note: If you are confused about this description, open Chrome browser to view some large GIF images with low network speed to see the animation behavior.
/// Default is true. Set to false to only render the static poster for incremental animated image.
/// - Warning: watchOS does not implementes.
/// - Parameter incrementalLoad: Whether or not to incremental load
public func incrementalLoad(_ incrementalLoad: Bool) -> AnimatedImage {
imageConfiguration.incrementalLoad = incrementalLoad
@ -479,5 +557,3 @@ struct AnimatedImage_Previews : PreviewProvider {
}
}
#endif
#endif

View File

@ -0,0 +1,24 @@
/*
* This file is part of the SDWebImage package.
* (c) DreamPiggy <lizhuoli1126@126.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
@import SDWebImage;
#if SD_WATCH
NS_ASSUME_NONNULL_BEGIN
/// Do not use this class directly in WatchKit or Storyboard. This class is implementation detail and will be removed in the future.
@interface SDAnimatedImageInterface : WKInterfaceImage
- (instancetype)init WK_AVAILABLE_WATCHOS_ONLY(6.0);
- (void)setContentMode:(SDImageScaleMode)contentMode;
- (void)setAnimationRepeatCount:(nullable NSNumber *)repeatCount;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@ -0,0 +1,272 @@
/*
* This file is part of the SDWebImage package.
* (c) DreamPiggy <lizhuoli1126@126.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDAnimatedImageInterface.h"
#if SD_WATCH
// ImageIO.modulemap does not contains this public header
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-umbrella"
#import <ImageIO/CGImageAnimation.h>
#pragma clang diagnostic pop
#pragma mark - SPI
@protocol CALayerProtocol <NSObject>
@property (nullable, strong) id contents;
@property CGFloat contentsScale;
@end
@protocol UIViewProtocol <NSObject>
@property (nonatomic, strong, readonly) id<CALayerProtocol> layer;
@property (nonatomic, assign) SDImageScaleMode contentMode;
@end
@interface WKInterfaceObject ()
// This is needed for dynamic created WKInterfaceObject, like `WKInterfaceMap`
- (instancetype)_initForDynamicCreationWithInterfaceProperty:(NSString *)property;
// This is remote UIView
@property (nonatomic, strong, readonly) id<UIViewProtocol> _interfaceView;
@end
@interface SDAnimatedImageStatus : NSObject
@property (nonatomic, assign) BOOL shouldAnimate;
@property (nonatomic, assign) CGImageAnimationStatus animationStatus;
@end
@implementation SDAnimatedImageStatus
@end
@interface SDAnimatedImageInterface () {
UIImage *_image;
}
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
@property (nonatomic, assign) NSUInteger totalFrameCount;
@property (nonatomic, assign) NSUInteger totalLoopCount;
@property (nonatomic, strong) UIImage<SDAnimatedImage> *animatedImage;
@property (nonatomic, assign) CGFloat animatedImageScale;
@property (nonatomic, strong) SDAnimatedImageStatus *currentStatus;
@property (nonatomic, strong) NSNumber *animationRepeatCount;
@end
@implementation SDAnimatedImageInterface
- (instancetype)init {
Class cls = [self class];
NSString *UUID = [NSUUID UUID].UUIDString;
NSString *property = [NSString stringWithFormat:@"%@_%@", cls, UUID];
self = [self _initForDynamicCreationWithInterfaceProperty:property];
return self;
}
- (NSDictionary *)interfaceDescriptionForDynamicCreation {
// This is called by WatchKit
return @{
@"type" : @"image",
@"property" : self.interfaceProperty,
@"image" : [self.class sharedEmptyImage]
};
}
+ (UIImage *)sharedEmptyImage {
// This is used for placeholder on `WKInterfaceImage`
// Do not using `[UIImage new]` because WatchKit will ignore it
static dispatch_once_t onceToken;
static UIImage *image;
dispatch_once(&onceToken, ^{
UIColor *color = UIColor.clearColor;
CGRect rect = CGRectMake(0, 0, 1, 1);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
return image;
}
- (void)setImage:(UIImage *)image {
if (_image == image) {
return;
}
_image = image;
// Reset all value
[self resetAnimatedImage];
[super setImage:image];
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
UIImage<SDAnimatedImage> *animatedImage = (UIImage<SDAnimatedImage> *)image;
NSUInteger animatedImageFrameCount = animatedImage.animatedImageFrameCount;
// Check the frame count
if (animatedImageFrameCount <= 1) {
return;
}
self.animatedImage = animatedImage;
self.totalFrameCount = animatedImageFrameCount;
// Get the current frame and loop count.
self.totalLoopCount = self.animatedImage.animatedImageLoopCount;
// Get the scale
self.animatedImageScale = image.scale;
NSData *animatedImageData = animatedImage.animatedImageData;
SDImageFormat format = [NSData sd_imageFormatForImageData:animatedImageData];
if (format == SDImageFormatGIF || format == SDImageFormatPNG) {
[self startBuiltInAnimationWithImage:animatedImage];
}
// Update should animate
[self updateShouldAnimate];
}
}
- (void)startBuiltInAnimationWithImage:(UIImage<SDAnimatedImage> *)animatedImage {
NSData *animatedImageData = animatedImage.animatedImageData;
NSUInteger maxLoopCount;
if (self.animationRepeatCount != nil) {
maxLoopCount = self.animationRepeatCount.unsignedIntegerValue;
} else {
maxLoopCount = animatedImage.animatedImageLoopCount;
}
if (maxLoopCount == 0) {
// The documentation says `kCFNumberPositiveInfinity may be used`, but it actually treat as 1 loop count
// 0 was treated as 1 loop count as well, not the same as Image/IO or UIKit
maxLoopCount = ((__bridge NSNumber *)kCFNumberPositiveInfinity).unsignedIntegerValue - 1;
}
NSDictionary *options = @{(__bridge NSString *)kCGImageAnimationLoopCount : @(maxLoopCount)};
SDAnimatedImageStatus *status = [SDAnimatedImageStatus new];
status.shouldAnimate = YES;
__weak typeof(self) wself = self;
status.animationStatus = CGAnimateImageDataWithBlock((__bridge CFDataRef)animatedImageData, (__bridge CFDictionaryRef)options, ^(size_t index, CGImageRef _Nonnull imageRef, bool * _Nonnull stop) {
__strong typeof(wself) self = wself;
if (!self) {
*stop = YES;
return;
}
if (!status.shouldAnimate) {
*stop = YES;
return;
}
// The CGImageRef provided by this API is GET only, should not call CGImageRelease
self.currentFrame = [[UIImage alloc] initWithCGImage:imageRef scale:self.animatedImageScale orientation:UIImageOrientationUp];
self.currentFrameIndex = index;
// Render the frame
[self displayLayer];
});
self.currentStatus = status;
}
- (void)displayLayer {
if (self.currentFrame) {
id<CALayerProtocol> layer = [self _interfaceView].layer;
layer.contentsScale = self.animatedImageScale;
layer.contents = (__bridge id)self.currentFrame.CGImage;
}
}
- (void)resetAnimatedImage
{
self.animatedImage = nil;
self.totalFrameCount = 0;
self.totalLoopCount = 0;
// reset current state
self.currentStatus.shouldAnimate = NO;
self.currentStatus = nil;
[self resetCurrentFrameIndex];
self.animatedImageScale = 1;
}
- (void)resetCurrentFrameIndex
{
self.currentFrame = nil;
self.currentFrameIndex = 0;
self.currentLoopCount = 0;
}
- (void)updateShouldAnimate
{
self.currentStatus.shouldAnimate = self.animatedImage && self.totalFrameCount > 1;
}
- (void)startAnimating {
if (self.animatedImage) {
self.currentStatus.shouldAnimate = YES;
} else if (_image.images.count > 0) {
[super startAnimating];
}
}
- (void)startAnimatingWithImagesInRange:(NSRange)imageRange duration:(NSTimeInterval)duration repeatCount:(NSInteger)repeatCount {
if (self.animatedImage) {
self.currentStatus.shouldAnimate = YES;
} else if (_image.images.count > 0) {
[super startAnimatingWithImagesInRange:imageRange duration:duration repeatCount:repeatCount];
}
}
- (void)stopAnimating {
if (self.animatedImage) {
self.currentStatus.shouldAnimate = NO;
} else if (_image.images.count > 0) {
[super stopAnimating];
}
}
- (void)setContentMode:(SDImageScaleMode)contentMode {
[self _interfaceView].contentMode = contentMode;
}
@end
#pragma mark - Web Cache
@interface SDAnimatedImageInterface (WebCache)
@end
@implementation SDAnimatedImageInterface (WebCache)
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
Class animatedImageClass = [SDAnimatedImage class];
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextAnimatedImageClass] = animatedImageClass;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
context:mutableContext
setImageBlock:nil
progress:progressBlock
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
if (completedBlock) {
completedBlock(image, error, cacheType, imageURL);
}
}];
}
@end
#endif

View File

@ -7,6 +7,7 @@
*/
#import <Foundation/Foundation.h>
#import <SDWebImageSwiftUI/SDAnimatedImageInterface.h>
//! Project version number for SDWebImageSwiftUI.
FOUNDATION_EXPORT double SDWebImageSwiftUIVersionNumber;