diff --git a/Example/SDWebImageSwiftUIDemo/ContentView.swift b/Example/SDWebImageSwiftUIDemo/ContentView.swift index 38d1b04c..e8a4a881 100644 --- a/Example/SDWebImageSwiftUIDemo/ContentView.swift +++ b/Example/SDWebImageSwiftUIDemo/ContentView.swift @@ -98,10 +98,10 @@ struct ContentView: View { WebImage(url: URL(string:url)) .resizable() .indicator(.activity) + .animation(.easeInOut(duration: 0.5)) + .transition(.fade) .scaledToFit() .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center) - .animation(.easeInOut(duration: 0.5)) - .transition(.opacity) #else WebImage(url: URL(string:url)) .resizable() diff --git a/README.md b/README.md index 41c11215..4669164f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ let package = Package( - [x] Supports placeholder and detail options control for image loading as SDWebImage - [x] Supports success/failure/progress changes event for custom handling - [x] Supports indicator with activity/progress indicator and customization +- [x] Supports built-in animation and transition, powered by SwiftUI Note: This `WebImage` using `Image` for internal implementation, which is the best compatible for SwiftUI layout and animation system. But it supports static image format only, because unlike `UIImageView` in UIKit, SwiftUI's `Image` does not support animation. @@ -75,6 +76,8 @@ var body: some View { } .resizable() .indicator(.activity) // Activity Indicator + .animation(.easeInOut(duration: 0.5)) + .transition(.fade) // Fade Transition .scaledToFit() .frame(width: 300, height: 300, alignment: .center) } @@ -90,6 +93,7 @@ var body: some View { .onFailure { error in // Error } + .indicator(SDWebImageActivityIndicator.medium) // Activity Indicator .transition(.fade) // Fade Transition .scaledToFit() // Data @@ -104,11 +108,11 @@ var body: some View { - [x] Supports network image as well as local data and bundle image - [x] Supports animation control using the SwiftUI Binding -- [x] Supports indicator and transition powered by SDWebImage and CoreAnimation +- [x] Supports indicator and transition, powered by SDWebImage and Core Animation - [x] Supports advanced control like loop count, incremental load, buffer size -- [x] Supports coordinate with native UIKit/AppKit/WKInterface view +- [x] Supports coordinate with native UIKit/AppKit/WatchKit view -Note: `AnimatedImage` supports both image url or image data for animated image format. Which use the SDWebImage's [Animated ImageView](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for internal implementation. Pay attention that since this base on UIKit/AppKit representable, if you need advanced customized layout and animation, you need CoreAnimation to help. +Note: `AnimatedImage` supports both image url or image data for animated image format. Which use the SDWebImage's [Animated ImageView](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for internal implementation. Pay attention that since this base on UIKit/AppKit representable, some advanced SwiftUI layout and animation system may not work as expected. You may need UIKit/AppKit and Core Animation to modify the native view. Note: From v0.4.0, `AnimatedImage` supports watchOS as well. However, it's not backed by SDWebImage's [Animated ImageView](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) like iOS/tvOS/macOS. It use some tricks and hacks because of the limitation on current Apple's API. It also use Image/IO decoding system, which means it supports GIF and APNG format only, but not external format like WebP. diff --git a/SDWebImageSwiftUI.xcodeproj/project.pbxproj b/SDWebImageSwiftUI.xcodeproj/project.pbxproj index f0839e38..916f00dc 100644 --- a/SDWebImageSwiftUI.xcodeproj/project.pbxproj +++ b/SDWebImageSwiftUI.xcodeproj/project.pbxproj @@ -31,6 +31,10 @@ 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 */; }; + 32B933E523659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; }; + 32B933E623659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; }; + 32B933E723659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.swift */; }; + 32B933E823659A1900BB7CAD /* Transition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B933E423659A1900BB7CAD /* Transition.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, ); }; }; @@ -115,6 +119,7 @@ 326B8486236335110011BDFB /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 326B848B236335400011BDFB /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; 326E480923431C0F00C633E9 /* ImageViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewWrapper.swift; sourceTree = ""; }; + 32B933E423659A1900BB7CAD /* Transition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transition.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 = ""; }; @@ -186,6 +191,14 @@ path = Indicator; sourceTree = ""; }; + 32B933E323659A0700BB7CAD /* Transition */ = { + isa = PBXGroup; + children = ( + 32B933E423659A1900BB7CAD /* Transition.swift */, + ); + path = Transition; + sourceTree = ""; + }; 32C43DC222FD540D00BE87F5 = { isa = PBXGroup; children = ( @@ -219,6 +232,7 @@ 32C43DDB22FD54C600BE87F5 /* Classes */ = { isa = PBXGroup; children = ( + 32B933E323659A0700BB7CAD /* Transition */, 326099472362E09E006EBB22 /* Indicator */, 324F61C4235E07EC003973B8 /* ObjC */, 32C43DDC22FD54C600BE87F5 /* ImageManager.swift */, @@ -443,6 +457,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32B933E523659A1900BB7CAD /* Transition.swift in Sources */, 32C43E1722FD583700BE87F5 /* WebImage.swift in Sources */, 326B848C236335400011BDFB /* ProgressIndicator.swift in Sources */, 326B84822363350C0011BDFB /* Indicator.swift in Sources */, @@ -459,6 +474,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32B933E623659A1900BB7CAD /* Transition.swift in Sources */, 32C43E1A22FD583700BE87F5 /* WebImage.swift in Sources */, 326B848D236335400011BDFB /* ProgressIndicator.swift in Sources */, 326B84832363350C0011BDFB /* Indicator.swift in Sources */, @@ -475,6 +491,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32B933E723659A1900BB7CAD /* Transition.swift in Sources */, 32C43E1D22FD583800BE87F5 /* WebImage.swift in Sources */, 326B848E236335400011BDFB /* ProgressIndicator.swift in Sources */, 326B84842363350C0011BDFB /* Indicator.swift in Sources */, @@ -491,6 +508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 32B933E823659A1900BB7CAD /* Transition.swift in Sources */, 32C43E2022FD583800BE87F5 /* WebImage.swift in Sources */, 326B848F236335400011BDFB /* ProgressIndicator.swift in Sources */, 326B84852363350C0011BDFB /* Indicator.swift in Sources */, diff --git a/SDWebImageSwiftUI/Classes/Transition/Transition.swift b/SDWebImageSwiftUI/Classes/Transition/Transition.swift new file mode 100644 index 00000000..287a5e14 --- /dev/null +++ b/SDWebImageSwiftUI/Classes/Transition/Transition.swift @@ -0,0 +1,47 @@ +/* +* 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 SwiftUI + +extension AnyTransition { + + /// Fade-in transition + public static var fade: AnyTransition { + let insertion = AnyTransition.opacity + let removal = AnyTransition.identity + return AnyTransition.asymmetric(insertion: insertion, removal: removal) + } + + /// Flip from left transition + public static var flipFromLeft: AnyTransition { + let insertion = AnyTransition.move(edge: .leading) + let removal = AnyTransition.identity + return AnyTransition.asymmetric(insertion: insertion, removal: removal) + } + + /// Flip from right transition + public static var flipFromRight: AnyTransition { + let insertion = AnyTransition.move(edge: .trailing) + let removal = AnyTransition.identity + return AnyTransition.asymmetric(insertion: insertion, removal: removal) + } + + /// Flip from top transition + public static var flipFromTop: AnyTransition { + let insertion = AnyTransition.move(edge: .top) + let removal = AnyTransition.identity + return AnyTransition.asymmetric(insertion: insertion, removal: removal) + } + + /// Flip from bottom transition + public static var flipFromBottom: AnyTransition { + let insertion = AnyTransition.move(edge: .bottom) + let removal = AnyTransition.identity + return AnyTransition.asymmetric(insertion: insertion, removal: removal) + } +} diff --git a/SDWebImageSwiftUI/Classes/WebImage.swift b/SDWebImageSwiftUI/Classes/WebImage.swift index 46a59b89..0a33cf4b 100644 --- a/SDWebImageSwiftUI/Classes/WebImage.swift +++ b/SDWebImageSwiftUI/Classes/WebImage.swift @@ -10,6 +10,8 @@ import SwiftUI import SDWebImage public struct WebImage : View { + static var emptyImage = Image(platformImage: PlatformImage()) + var url: URL? var placeholder: Image? var options: SDWebImageOptions @@ -30,32 +32,34 @@ public struct WebImage : View { self.options = options self.context = context self.imageManager = ImageManager(url: url, options: options, context: context) + // load remote image here, SwiftUI sometimes will create a new View struct without calling `onAppear` (like enter EditMode) :) + // this can ensure we load the image, SDWebImage take care of the duplicated query + self.imageManager.load() } public var body: some View { - let image: Image if let platformImage = imageManager.image { - image = Image(platformImage: platformImage) + var image = Image(platformImage: platformImage) + image = configurations.reduce(image) { (previous, configuration) in + configuration(previous) + } + let view = image + return AnyView(view) } else { - if let placeholder = placeholder { - image = placeholder - } else { - image = Image(platformImage: PlatformImage()) + var image = placeholder ?? WebImage.emptyImage + image = configurations.reduce(image) { (previous, configuration) in + configuration(previous) } - // load remote image here, SwiftUI sometimes will create a new View struct without calling `onAppear` (like enter EditMode) :) - // this can ensure we load the image, SDWebImage take care of the duplicated query - self.imageManager.load() - } - return configurations.reduce(image) { (previous, configuration) in - configuration(previous) - } - .onAppear { - if self.imageManager.image == nil { - self.imageManager.load() + let view = image + .onAppear { + if self.imageManager.image == nil { + self.imageManager.load() + } } - } - .onDisappear { - self.imageManager.cancel() + .onDisappear { + self.imageManager.cancel() + } + return AnyView(view) } } }