diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 70889bb..9a39619 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -2,7 +2,7 @@ PODS: - SDWebImage (5.1.0): - SDWebImage/Core (= 5.1.0) - SDWebImage/Core (5.1.0) - - SDWebImageSwiftUI (0.1.0): + - SDWebImageSwiftUI (0.1.1): - SDWebImage (~> 5.1) DEPENDENCIES: @@ -18,7 +18,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: SDWebImage: fb387001955223213dde14bc08c8b73f371f8d8f - SDWebImageSwiftUI: 22254f3ced4f056602cd8167b64106ab6419c6e6 + SDWebImageSwiftUI: fa0b13b16a92985532cd13931b88aea4ff7efb0b PODFILE CHECKSUM: 146734166216dd8fc1597433cc675999454ed4b2 diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 4e602f3..3bf40fa 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -15,9 +15,11 @@ struct ContentView: View { var body: some View { VStack { WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic")) + .resizable() .scaledToFit() .frame(width: CGFloat(300), height: CGFloat(300), alignment: .center) AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), options: [.progressiveLoad]) + .resizable() .scaledToFill() .frame(width: CGFloat(400), height: CGFloat(300), alignment: .center) } diff --git a/Package.resolved b/Package.resolved index 841e1bd..77db8b3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "0a3cd255a655b73fb3b3437acf2ab506b5c0c9c6", - "version": "5.1.0" + "revision": "9c1682e37bf3486daccd313fcbcd7fd90a2064f4", + "version": "5.2.0" } } ] diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index 7caa8f4..3ec68bb 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; + 326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */; }; 32C43DE622FD54CD00BE87F5 /* SDWebImageSwiftUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 32C43DE422FD54CD00BE87F5 /* SDWebImageSwiftUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 32C43DEA22FD577300BE87F5 /* SDWebImage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32C43DE922FD577300BE87F5 /* SDWebImage.framework */; }; 32C43DEB22FD577300BE87F5 /* SDWebImage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 32C43DE922FD577300BE87F5 /* SDWebImage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -85,6 +89,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = ""; }; 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 = ""; }; 32C43DDE22FD54C600BE87F5 /* WebImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebImage.swift; sourceTree = ""; }; @@ -174,6 +179,7 @@ 32C43DDE22FD54C600BE87F5 /* WebImage.swift */, 32C43DDF22FD54C600BE87F5 /* AnimatedImage.swift */, 32C43E3122FD5DE100BE87F5 /* SDWebImageSwiftUI.swift */, + 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */, ); path = Classes; sourceTree = ""; @@ -385,6 +391,7 @@ files = ( 32C43E1722FD583700BE87F5 /* WebImage.swift in Sources */, 32C43E3222FD5DE100BE87F5 /* SDWebImageSwiftUI.swift in Sources */, + 326E480A23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, 32C43E1622FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1822FD583700BE87F5 /* AnimatedImage.swift in Sources */, ); @@ -396,6 +403,7 @@ files = ( 32C43E1A22FD583700BE87F5 /* WebImage.swift in Sources */, 32C43E3322FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, + 326E480B23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, 32C43E1922FD583700BE87F5 /* ImageManager.swift in Sources */, 32C43E1B22FD583700BE87F5 /* AnimatedImage.swift in Sources */, ); @@ -407,6 +415,7 @@ files = ( 32C43E1D22FD583800BE87F5 /* WebImage.swift in Sources */, 32C43E3422FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, + 326E480C23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, 32C43E1C22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E1E22FD583800BE87F5 /* AnimatedImage.swift in Sources */, ); @@ -418,6 +427,7 @@ files = ( 32C43E2022FD583800BE87F5 /* WebImage.swift in Sources */, 32C43E3522FD5DF400BE87F5 /* SDWebImageSwiftUI.swift in Sources */, + 326E480D23431C0F00C633E9 /* ImageViewWrapper.swift in Sources */, 32C43E1F22FD583800BE87F5 /* ImageManager.swift in Sources */, 32C43E2122FD583800BE87F5 /* AnimatedImage.swift in Sources */, ); diff --git a/SDWebImageSwiftUI/Classes/AnimatedImage.swift b/SDWebImageSwiftUI/Classes/AnimatedImage.swift index 446c192..5ba8ae1 100644 --- a/SDWebImageSwiftUI/Classes/AnimatedImage.swift +++ b/SDWebImageSwiftUI/Classes/AnimatedImage.swift @@ -20,6 +20,10 @@ final class AnimatedImageModel : ObservableObject { // Layout Binding Object final class AnimatedImageLayout : ObservableObject { @Published var contentMode: ContentMode = .fill + @Published var aspectRatio: CGFloat? + @Published var renderingMode: Image.TemplateRenderingMode? + @Published var interpolation: Image.Interpolation? + @Published var antialiased: Bool = false } // View @@ -31,53 +35,115 @@ public struct AnimatedImage : ViewRepresentable { var webContext: [SDWebImageContextOption : Any]? = nil #if os(macOS) - public typealias NSViewType = SDAnimatedImageView + public typealias NSViewType = AnimatedImageViewWrapper #else - public typealias UIViewType = SDAnimatedImageView + public typealias UIViewType = AnimatedImageViewWrapper #endif #if os(macOS) - public func makeNSView(context: NSViewRepresentableContext) -> SDAnimatedImageView { + public func makeNSView(context: NSViewRepresentableContext) -> AnimatedImageViewWrapper { makeView(context: context) } - public func updateNSView(_ nsView: SDAnimatedImageView, context: NSViewRepresentableContext) { + public func updateNSView(_ nsView: AnimatedImageViewWrapper, context: NSViewRepresentableContext) { updateView(nsView, context: context) } #else - public func makeUIView(context: UIViewRepresentableContext) -> SDAnimatedImageView { + public func makeUIView(context: UIViewRepresentableContext) -> AnimatedImageViewWrapper { makeView(context: context) } - public func updateUIView(_ uiView: SDAnimatedImageView, context: UIViewRepresentableContext) { + public func updateUIView(_ uiView: AnimatedImageViewWrapper, context: UIViewRepresentableContext) { updateView(uiView, context: context) } #endif - func makeView(context: ViewRepresentableContext) -> SDAnimatedImageView { - SDAnimatedImageView() + func makeView(context: ViewRepresentableContext) -> AnimatedImageViewWrapper { + AnimatedImageViewWrapper() } - func updateView(_ view: SDAnimatedImageView, context: ViewRepresentableContext) { - view.image = imageModel.image + func updateView(_ view: AnimatedImageViewWrapper, context: ViewRepresentableContext) { + view.wrapped.image = imageModel.image if let url = imageModel.url { - view.sd_setImage(with: url, placeholderImage: view.image, options: webOptions, context: webContext) + view.wrapped.sd_setImage(with: url, placeholderImage: nil, options: webOptions, context: webContext) } + layoutView(view, context: context) + } + + func layoutView(_ view: AnimatedImageViewWrapper, context: ViewRepresentableContext) { + // AspectRatio + if let _ = imageLayout.aspectRatio { + // TODO: Needs layer transform and geometry calculation + } + + // ContentMode switch imageLayout.contentMode { case .fit: #if os(macOS) - view.imageScaling = .scaleProportionallyUpOrDown + view.wrapped.imageScaling = .scaleProportionallyUpOrDown #else - view.contentMode = .scaleAspectFit + view.wrapped.contentMode = .scaleAspectFit #endif case .fill: #if os(macOS) - view.imageScaling = .scaleAxesIndependently + view.wrapped.imageScaling = .scaleAxesIndependently #else - view.contentMode = .scaleToFill + view.wrapped.contentMode = .scaleToFill #endif } + + // RenderingMode + if let renderingMode = imageLayout.renderingMode { + switch renderingMode { + case .template: + #if os(macOS) + view.wrapped.image?.isTemplate = true + #else + view.wrapped.image = view.wrapped.image?.withRenderingMode(.alwaysTemplate) + #endif + case .original: + #if os(macOS) + view.wrapped.image?.isTemplate = false + #else + view.wrapped.image = view.wrapped.image?.withRenderingMode(.alwaysOriginal) + #endif + @unknown default: + // Future cases, not implements + break + } + } + + // Interpolation + if let interpolation = imageLayout.interpolation { + switch interpolation { + case .high: + view.interpolationQuality = .high + case .medium: + view.interpolationQuality = .medium + case .low: + view.interpolationQuality = .low + case .none: + view.interpolationQuality = .none + @unknown default: + // Future cases, not implements + break + } + } else { + view.interpolationQuality = .default + } + + // Antialiased + view.shouldAntialias = imageLayout.antialiased + + // Display + #if os(macOS) + view.needsLayout = true + view.needsDisplay = true + #else + view.setNeedsLayout() + view.setNeedsDisplay() + #endif } public func image(_ image: SDAnimatedImage?) -> Self { @@ -90,15 +156,49 @@ public struct AnimatedImage : ViewRepresentable { return self } - public func scaledToFit() -> Self { - imageLayout.contentMode = .fit + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> AnimatedImage + { + return self + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> AnimatedImage { + imageLayout.renderingMode = renderingMode + return self + } + + public func interpolation(_ interpolation: Image.Interpolation) -> AnimatedImage { + imageLayout.interpolation = interpolation + return self + } + + public func antialiased(_ isAntialiased: Bool) -> AnimatedImage { + imageLayout.antialiased = isAntialiased return self } - public func scaledToFill() -> Self { - imageLayout.contentMode = .fill + public func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> AnimatedImage { + imageLayout.aspectRatio = aspectRatio + imageLayout.contentMode = contentMode return self } + + public func aspectRatio(_ aspectRatio: CGSize, contentMode: ContentMode) -> AnimatedImage { + var ratio: CGFloat? + if aspectRatio.width > 0 && aspectRatio.height > 0 { + ratio = aspectRatio.width / aspectRatio.height + } + return self.aspectRatio(ratio, contentMode: contentMode) + } + + public func scaledToFit() -> AnimatedImage { + self.aspectRatio(nil, contentMode: .fit) + } + + public func scaledToFill() -> AnimatedImage { + self.aspectRatio(nil, contentMode: .fill) + } } extension AnimatedImage { @@ -123,4 +223,17 @@ extension AnimatedImage { } } +#if DEBUG +struct AnimatedImage_Previews : PreviewProvider { + static var previews: some View { + Group { + AnimatedImage(url: URL(string: "http://assets.sbnation.com/assets/2512203/dogflops.gif")) + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif + #endif diff --git a/SDWebImageSwiftUI/Classes/ImageManager.swift b/SDWebImageSwiftUI/Classes/ImageManager.swift index b4b99e0..d9ac3b1 100644 --- a/SDWebImageSwiftUI/Classes/ImageManager.swift +++ b/SDWebImageSwiftUI/Classes/ImageManager.swift @@ -14,10 +14,10 @@ class ImageManager : ObservableObject { var objectWillChange = PassthroughSubject() - private var manager = SDWebImageManager.shared - private weak var currentOperation: SDWebImageOperation? = nil + var manager = SDWebImageManager.shared + weak var currentOperation: SDWebImageOperation? = nil - var image: Image? { + var image: PlatformImage? { willSet { objectWillChange.send(self) } @@ -36,11 +36,7 @@ class ImageManager : ObservableObject { func load() { currentOperation = manager.loadImage(with: url, options: options, context: context, progress: nil) { (image, data, error, cacheType, _, _) in if let image = image { - #if os(macOS) - self.image = Image(nsImage: image) - #else - self.image = Image(uiImage: image) - #endif + self.image = image } } } diff --git a/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift new file mode 100644 index 0000000..f6ff8ee --- /dev/null +++ b/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift @@ -0,0 +1,61 @@ +/* +* This file is part of the SDWebImage package. +* (c) DreamPiggy +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +import Foundation +import SDWebImage + +// View Wrapper +public class AnimatedImageViewWrapper : PlatformView { + var wrapped = SDAnimatedImageView() + var interpolationQuality = CGInterpolationQuality.default + var shouldAntialias = false + + override public func draw(_ rect: CGRect) { + #if os(macOS) + guard let ctx = NSGraphicsContext.current?.cgContext else { + return + } + #else + guard let ctx = UIGraphicsGetCurrentContext() else { + return + } + #endif + ctx.interpolationQuality = interpolationQuality + ctx.setShouldAntialias(shouldAntialias) + } + + public override init(frame frameRect: CGRect) { + super.init(frame: frameRect) + addSubview(wrapped) + wrapped.bindFrameToSuperviewBounds() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + addSubview(wrapped) + wrapped.bindFrameToSuperviewBounds() + } +} + +extension PlatformView { + /// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview. + /// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this. + func bindFrameToSuperviewBounds() { + guard let superview = self.superview else { + print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.") + return + } + + self.translatesAutoresizingMaskIntoConstraints = false + self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true + self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true + self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true + self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true + + } +} diff --git a/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift b/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift index 89661c7..a7a68d5 100644 --- a/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift +++ b/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift @@ -9,6 +9,28 @@ import Foundation import SwiftUI +#if os(macOS) +typealias PlatformImage = NSImage +#else +typealias PlatformImage = UIImage +#endif + +#if os(macOS) +public typealias PlatformView = NSView +#else +public typealias PlatformView = UIView +#endif + +extension Image { + init(platformImage: PlatformImage) { + #if os(macOS) + self.init(nsImage: platformImage) + #else + self.init(uiImage: platformImage) + #endif + } +} + #if !os(watchOS) #if os(macOS) diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 7f99490..793c60b 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -15,6 +15,8 @@ public struct WebImage : View { public var options: SDWebImageOptions public var context: [SDWebImageContextOption : Any]? + var configurations: [(Image) -> Image] = [] + @ObservedObject var imageManager: ImageManager public init(url: URL?, placeholder: Image? = nil, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) { @@ -26,26 +28,70 @@ public struct WebImage : View { } public var body: some View { - if let image = imageManager.image { - return image - .resizable() - .onAppear {} - .onDisappear {} - } else if let image = placeholder { - return image - .resizable() - .onAppear { self.imageManager.load() } - .onDisappear { self.imageManager.cancel() } + let image: Image + if let platformImage = imageManager.image { + image = Image(platformImage: platformImage) + } else if let placeholder = placeholder { + image = placeholder } else { #if os(macOS) let emptyImage = Image(nsImage: NSImage()) #else let emptyImage = Image(uiImage: UIImage()) #endif - return emptyImage - .resizable() - .onAppear { self.imageManager.load() } - .onDisappear { self.imageManager.cancel() } + image = emptyImage + } + return configurations.reduce(image) { (previous, configuration) in + configuration(previous) + } + .onAppear { + if self.imageManager.image == nil { + self.imageManager.load() + } + } + .onDisappear { + self.imageManager.cancel() } } } + +extension WebImage { + func configure(_ block: @escaping (Image) -> Image) -> WebImage { + var result = self + result.configurations.append(block) + return result + } + + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> WebImage + { + configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> WebImage { + configure { $0.renderingMode(renderingMode) } + } + + public func interpolation(_ interpolation: Image.Interpolation) -> WebImage { + configure { $0.interpolation(interpolation) } + } + + public func antialiased(_ isAntialiased: Bool) -> WebImage { + configure { $0.antialiased(isAntialiased) } + } +} + + +#if DEBUG +struct WebImage_Previews : PreviewProvider { + static var previews: some View { + Group { + WebImage(url: URL(string: "https://raw.githubusercontent.com/SDWebImage/SDWebImage/master/SDWebImage_logo.png")) + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif