Skip to content

Fix iOS 13 compatibility && WebImage/AnimatedImage using @State to publish changes #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions Example/SDWebImageSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
322E0E2228D332130003A55F /* Images.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 322E0DF228D331A20003A55F /* Images.bundle */; };
322E0E2328D332130003A55F /* Images.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 322E0DF228D331A20003A55F /* Images.bundle */; };
326B0D712345C01900D28269 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326B0D702345C01900D28269 /* DetailView.swift */; };
327B90F228DC4EBB003E8BD9 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 327B90F128DC4EBB003E8BD9 /* ViewInspector */; };
327B90F428DC4EC0003E8BD9 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 327B90F328DC4EC0003E8BD9 /* ViewInspector */; };
32DCFE9528D333E8001A17BF /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 32DCFE9428D333E8001A17BF /* ViewInspector */; };
32E5290C2348A0C700EA46FF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E5290B2348A0C700EA46FF /* AppDelegate.swift */; };
32E529102348A0C900EA46FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 32E5290F2348A0C900EA46FF /* Assets.xcassets */; };
Expand Down Expand Up @@ -220,6 +222,7 @@
buildActionMask = 2147483647;
files = (
833A61715BAAB31702D867CC /* Pods_SDWebImageSwiftUITests_macOS.framework in Frameworks */,
327B90F228DC4EBB003E8BD9 /* ViewInspector in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -228,6 +231,7 @@
buildActionMask = 2147483647;
files = (
2E3D81A12C757E01A3C420F2 /* Pods_SDWebImageSwiftUITests_tvOS.framework in Frameworks */,
327B90F428DC4EC0003E8BD9 /* ViewInspector in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -517,9 +521,13 @@
buildRules = (
);
dependencies = (
327B90EE28DC4EAA003E8BD9 /* PBXTargetDependency */,
322E0E0728D331F00003A55F /* PBXTargetDependency */,
);
name = "SDWebImageSwiftUITests macOS";
packageProductDependencies = (
327B90F128DC4EBB003E8BD9 /* ViewInspector */,
);
productName = "SDWebImageSwiftUITests macOS";
productReference = 322E0E0228D331F00003A55F /* SDWebImageSwiftUITests macOS.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
Expand All @@ -537,9 +545,13 @@
buildRules = (
);
dependencies = (
327B90F028DC4EAE003E8BD9 /* PBXTargetDependency */,
322E0E1428D332050003A55F /* PBXTargetDependency */,
);
name = "SDWebImageSwiftUITests tvOS";
packageProductDependencies = (
327B90F328DC4EC0003E8BD9 /* ViewInspector */,
);
productName = "SDWebImageSwiftUITests tvOS";
productReference = 322E0E0F28D332050003A55F /* SDWebImageSwiftUITests tvOS.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
Expand Down Expand Up @@ -698,7 +710,7 @@
);
mainGroup = 607FACC71AFB9204008FA782;
packageReferences = (
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */,
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */,
);
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -1225,6 +1237,14 @@
target = 32E5291F2348A0D300EA46FF /* SDWebImageSwiftUIDemo-tvOS */;
targetProxy = 322E0E1328D332050003A55F /* PBXContainerItemProxy */;
};
327B90EE28DC4EAA003E8BD9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 327B90ED28DC4EAA003E8BD9 /* ViewInspector */;
};
327B90F028DC4EAE003E8BD9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 327B90EF28DC4EAE003E8BD9 /* ViewInspector */;
};
32DCFE9728D333F1001A17BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 32DCFE9628D333F1001A17BF /* ViewInspector */;
Expand Down Expand Up @@ -2044,7 +2064,7 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */ = {
32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nalexn/ViewInspector.git";
requirement = {
Expand All @@ -2055,14 +2075,34 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
327B90ED28DC4EAA003E8BD9 /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
327B90EF28DC4EAE003E8BD9 /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
327B90F128DC4EBB003E8BD9 /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
327B90F328DC4EC0003E8BD9 /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
32DCFE9428D333E8001A17BF /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
32DCFE9628D333F1001A17BF /* ViewInspector */ = {
isa = XCSwiftPackageProductDependency;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector.git" */;
package = 32DCFE8D28D333B0001A17BF /* XCRemoteSwiftPackageReference "ViewInspector" */;
productName = ViewInspector;
};
/* End XCSwiftPackageProductDependency section */
Expand Down
143 changes: 100 additions & 43 deletions Example/SDWebImageSwiftUIDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ extension Indicator where T == ProgressView<EmptyView, EmptyView> {
}
#endif

// Test Switching url using @State
struct ContentView2: View {
@State var imageURLs = [
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg",
"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg",
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"
]
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
@State var imageIndex : Int = 0
var body: some View {
Group {
Text("\(animated ? "AnimatedImage" : "WebImage") - \((imageURLs[imageIndex] as NSString).lastPathComponent)")
Spacer()
#if os(watchOS)
WebImage(url:URL(string: imageURLs[imageIndex]))
.resizable()
.aspectRatio(contentMode: .fit)
#else
if self.animated {
AnimatedImage(url:URL(string: imageURLs[imageIndex]))
.resizable()
.aspectRatio(contentMode: .fit)
} else {
WebImage(url:URL(string: imageURLs[imageIndex]))
.resizable()
.aspectRatio(contentMode: .fit)
}
#endif
Spacer()
Button("Next") {
if imageIndex + 1 >= imageURLs.count {
imageIndex = 0
} else {
imageIndex += 1
}
}
Button("Reload") {
SDImageCache.shared.clearMemory()
SDImageCache.shared.clearDisk(onCompletion: nil)
}
Toggle("Switch", isOn: $animated)
}
}
}

struct ContentView: View {
@State var imageURLs = [
"http://assets.sbnation.com/assets/2512203/dogflops.gif",
Expand All @@ -58,6 +104,58 @@ struct ContentView: View {
@State var animated: Bool = false // You can change between WebImage/AnimatedImage
@EnvironmentObject var settings: UserSettings

// Used to avoid https://twitter.com/fatbobman/status/1572507700436807683?s=20&t=5rfj6BUza5Jii-ynQatCFA
struct ItemView: View {
@Binding var animated: Bool
@State var url: String
var body: some View {
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
HStack {
if self.animated {
#if os(macOS) || os(iOS) || os(tvOS)
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
.onViewUpdate { view, context in
#if os(macOS)
view.toolTip = url
#endif
}
.indicator(SDWebImageActivityIndicator.medium)
/**
.placeholder(UIImage(systemName: "photo"))
*/
.transition(.fade)
.resizable()
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
#else
WebImage(url: URL(string:url), isAnimating: self.$animated)
.resizable()
.indicator(.activity)
.transition(.fade(duration: 0.5))
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
#endif
} else {
WebImage(url: URL(string:url), isAnimating: .constant(true))
.resizable()
/**
.placeholder {
Image(systemName: "photo")
}
*/
.indicator(.activity)
.transition(.fade(duration: 0.5))
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
}
Text((url as NSString).lastPathComponent)
}
}
.buttonStyle(PlainButtonStyle())
}
}


var body: some View {
#if os(iOS)
return NavigationView {
Expand Down Expand Up @@ -119,49 +217,8 @@ struct ContentView: View {
func contentView() -> some View {
List {
ForEach(imageURLs, id: \.self) { url in
NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
HStack {
if self.animated {
#if os(macOS) || os(iOS) || os(tvOS)
AnimatedImage(url: URL(string:url), isAnimating: .constant(true))
.onViewUpdate { view, context in
#if os(macOS)
view.toolTip = url
#endif
}
.indicator(SDWebImageActivityIndicator.medium)
/**
.placeholder(UIImage(systemName: "photo"))
*/
.transition(.fade)
.resizable()
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
#else
WebImage(url: URL(string:url), isAnimating: self.$animated)
.resizable()
.indicator(.activity)
.transition(.fade(duration: 0.5))
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
#endif
} else {
WebImage(url: URL(string:url), isAnimating: .constant(true))
.resizable()
/**
.placeholder {
Image(systemName: "photo")
}
*/
.indicator(.activity)
.transition(.fade(duration: 0.5))
.scaledToFit()
.frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
}
Text((url as NSString).lastPathComponent)
}
}
.buttonStyle(PlainButtonStyle())
// Must use top level view instead of inlined view structure
ItemView(animated: $animated, url: url)
}
.onDelete { indexSet in
indexSet.forEach { index in
Expand Down
58 changes: 50 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ It looks familiar like `SDWebImageManager`, but it's built for SwiftUI world, wh

```swift
struct MyView : View {
@ObservedObject var imageManager: ImageManager
@ObservedObject var imageManager = ImageManager()
var body: some View {
// Your custom complicated view graph
Group {
Expand All @@ -281,17 +281,11 @@ struct MyView : View {
}
}
// Trigger image loading when appear
.onAppear { self.imageManager.load() }
.onAppear { self.imageManager.load(url: url) }
// Cancel image loading when disappear
.onDisappear { self.imageManager.cancel() }
}
}

struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(imageManager: ImageManager(url: URL(string: "https://via.placeholder.com/200x200.jpg"))
}
}
```

### Customization and configuration setup
Expand Down Expand Up @@ -337,6 +331,54 @@ For more information, it's really recommended to check our demo, to learn detail

### Common Problems

#### Using WebImage/AnimatedImage in List/LazyStack/LazyGrid and ForEach

SwiftUI has a known behavior(bug?) when using stateful view in `List/LazyStack/LazyGrid`.
Only the **Top Level** view can hold its own `@State/@StateObject`, but the sub structure will lose state when scroll out of screen.
However, WebImage/Animated is both stateful. To ensure the state keep in sync even when scroll out of screen. you may use some tricks.

See more: https://twitter.com/fatbobman/status/1572507700436807683?s=21&t=z4FkAWTMvjsgL-wKdJGreQ

In short, it's not recommanded to do so:

```swift
struct ContentView {
@State var imageURLs: [String]
var body: some View {
List {
ForEach(imageURLs, id: \.self) { url in
VStack {
WebImage(url) // The top level is `VStack`
}
}
}
}
}
```

instead, using this approach:

```swift
struct ContentView {
struct BodyView {
@State var url: String
var body: some View {
VStack {
WebImage(url)
}
}
}
@State var imageURLs: [String]
var body: some View {
List {
ForEach(imageURLs, id: \.self) { url in
BodyView(url: url)
}
}
}
}
```

#### Using Image/WebImage/AnimatedImage in Button/NavigationLink

SwiftUI's `Button` apply overlay to its content (except `Text`) by default, this is common mistake to write code like this, which cause strange behavior:
Expand Down
Loading