├── SDWebImageSwiftUI
├── Classes
│ ├── .gitkeep
│ ├── SDWebImageSwiftUI.swift
│ ├── Image.swift
│ ├── Transition
│ │ └── Transition.swift
│ ├── Indicator
│ │ └── Indicator.swift
│ ├── ImagePlayer.swift
│ ├── ImageViewWrapper.swift
│ └── ImageManager.swift
├── Resources
│ └── PrivacyInfo.xcprivacy
└── Module
│ ├── SDWebImageSwiftUI.h
│ └── Info.plist
├── Cartfile
├── Example
├── Screenshot
│ ├── Demo-iOS.jpg
│ ├── Demo-tvOS.jpg
│ ├── Demo-macOS.jpg
│ └── Demo-watchOS.jpg
├── SDWebImageSwiftUIDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── AppDelegate.swift
│ ├── SceneDelegate.swift
│ ├── DetailView.swift
│ └── ContentView.swift
├── SDWebImageSwiftUIDemo-macOS
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── wifi.exclamationmark.imageset
│ │ │ ├── Contents.json
│ │ │ └── wifi.exclamationmark.svg
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── SDWebImageSwiftUIDemo_macOS.entitlements
│ ├── Info.plist
│ └── AppDelegate.swift
├── SDWebImageSwiftUIDemo-tvOS
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── App Icon & Top Shelf Image.brandassets
│ │ │ ├── App Icon.imagestack
│ │ │ ├── Back.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ ├── Front.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ ├── Middle.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── App Icon - App Store.imagestack
│ │ │ ├── Back.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ ├── Front.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ ├── Middle.imagestacklayer
│ │ │ │ ├── Contents.json
│ │ │ │ └── Content.imageset
│ │ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ │ ├── Top Shelf Image.imageset
│ │ │ └── Contents.json
│ │ │ ├── Top Shelf Image Wide.imageset
│ │ │ └── Contents.json
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ └── AppDelegate.swift
├── SDWebImageSwiftUIDemo-visionOS
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.solidimagestack
│ │ │ ├── Back.solidimagestacklayer
│ │ │ ├── Contents.json
│ │ │ └── Content.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── Front.solidimagestacklayer
│ │ │ ├── Contents.json
│ │ │ └── Content.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── Middle.solidimagestacklayer
│ │ │ ├── Contents.json
│ │ │ └── Content.imageset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Info.plist
│ └── AppDelegate.swift
├── SDWebImageSwiftUIDemo-watchOS WatchKit App
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ └── Interface.storyboard
│ └── Info.plist
├── SDWebImageSwiftUIDemo-watchOS WatchKit Extension
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── Complication.complicationset
│ │ │ ├── Circular.imageset
│ │ │ └── Contents.json
│ │ │ ├── Modular.imageset
│ │ │ └── Contents.json
│ │ │ ├── Extra Large.imageset
│ │ │ └── Contents.json
│ │ │ ├── Graphic Bezel.imageset
│ │ │ └── Contents.json
│ │ │ ├── Graphic Corner.imageset
│ │ │ └── Contents.json
│ │ │ ├── Utilitarian.imageset
│ │ │ └── Contents.json
│ │ │ ├── Graphic Circular.imageset
│ │ │ └── Contents.json
│ │ │ ├── Graphic Large Rectangular.imageset
│ │ │ └── Contents.json
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── HostingController.swift
│ ├── Info.plist
│ └── ExtensionDelegate.swift
└── SDWebImageSwiftUI.xcodeproj
│ ├── project.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ ├── SDWebImageSwiftUITests.xcscheme
│ ├── SDWebImageSwiftUITests macOS.xcscheme
│ ├── SDWebImageSwiftUITests tvOS.xcscheme
│ ├── SDWebImageSwiftUIDemo-visionOS.xcscheme
│ ├── SDWebImageSwiftUIDemo.xcscheme
│ ├── SDWebImageSwiftUIDemo-tvOS.xcscheme
│ ├── SDWebImageSwiftUIDemo-macOS.xcscheme
│ └── SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme
├── Tests
├── Images.bundle
│ ├── TestEXIF.png
│ ├── TestImage.gif
│ ├── TestImage.heic
│ ├── TestImage.heif
│ ├── TestImage.jpg
│ ├── TestImage.png
│ ├── TestLoopCount.gif
│ ├── TestImageLarge.jpg
│ ├── TestImageAnimated.apng
│ ├── TestImageAnimated.heic
│ └── MonochromeTestImage.jpg
├── Info.plist
├── TestUtils.swift
├── ImageManagerTests.swift
├── WebImageTests.swift
└── AnimatedImageTests.swift
├── SDWebImageSwiftUI.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── SDWebImageSwiftUI.xcscheme
│ ├── SDWebImageSwiftUI macOS.xcscheme
│ ├── SDWebImageSwiftUI tvOS.xcscheme
│ ├── SDWebImageSwiftUI watchOS.xcscheme
│ └── SDWebImageSwiftUI visionOS.xcscheme
├── SDWebImageSwiftUI.xcworkspace
└── contents.xcworkspacedata
├── codecov.yml
├── Package.resolved
├── carthage.sh
├── .gitignore
├── LICENSE
├── Package.swift
├── SDWebImageSwiftUI.podspec
├── .travis.yml
├── Podfile
├── .github
└── workflows
│ └── CI.yml
└── CHANGELOG.md
/SDWebImageSwiftUI/Classes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | github "SDWebImage/SDWebImage" ~> 5.21.1
2 |
--------------------------------------------------------------------------------
/Example/Screenshot/Demo-iOS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Example/Screenshot/Demo-iOS.jpg
--------------------------------------------------------------------------------
/Example/Screenshot/Demo-tvOS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Example/Screenshot/Demo-tvOS.jpg
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestEXIF.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestEXIF.png
--------------------------------------------------------------------------------
/Example/Screenshot/Demo-macOS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Example/Screenshot/Demo-macOS.jpg
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImage.gif
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImage.heic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImage.heic
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImage.heif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImage.heif
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImage.jpg
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImage.png
--------------------------------------------------------------------------------
/Example/Screenshot/Demo-watchOS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Example/Screenshot/Demo-watchOS.jpg
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestLoopCount.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestLoopCount.gif
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImageLarge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImageLarge.jpg
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImageAnimated.apng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImageAnimated.apng
--------------------------------------------------------------------------------
/Tests/Images.bundle/TestImageAnimated.heic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/TestImageAnimated.heic
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Tests/Images.bundle/MonochromeTestImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/HEAD/Tests/Images.bundle/MonochromeTestImage.jpg
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | ignore:
3 | - "Example"
4 | - "Tests"
5 | status:
6 | project:
7 | default: off
8 | ios:
9 | flags: ios
10 | macos:
11 | flags: macos
12 | tvos:
13 | flags: tvos
14 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv"
5 | }
6 | ],
7 | "info" : {
8 | "version" : 1,
9 | "author" : "xcode"
10 | }
11 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "reality",
5 | "scale" : "2x"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "reality",
5 | "scale" : "2x"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "reality",
5 | "scale" : "2x"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SDWebImage",
6 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "b62cb63bf4ed1f04c961a56c9c6c9d5ab8524ec6",
10 | "version": "5.21.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | }
11 | ],
12 | "info" : {
13 | "version" : 1,
14 | "author" : "xcode"
15 | }
16 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/AppIcon.solidimagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "layers" : [
7 | {
8 | "filename" : "Front.solidimagestacklayer"
9 | },
10 | {
11 | "filename" : "Middle.solidimagestacklayer"
12 | },
13 | {
14 | "filename" : "Back.solidimagestacklayer"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "layers" : [
3 | {
4 | "filename" : "Front.imagestacklayer"
5 | },
6 | {
7 | "filename" : "Middle.imagestacklayer"
8 | },
9 | {
10 | "filename" : "Back.imagestacklayer"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "layers" : [
3 | {
4 | "filename" : "Front.imagestacklayer"
5 | },
6 | {
7 | "filename" : "Middle.imagestacklayer"
8 | },
9 | {
10 | "filename" : "Back.imagestacklayer"
11 | }
12 | ],
13 | "info" : {
14 | "version" : 1,
15 | "author" : "xcode"
16 | }
17 | }
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Resources/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyTracking
6 |
7 | NSPrivacyAccessedAPITypes
8 |
9 | NSPrivacyCollectedDataTypes
10 |
11 | NSPrivacyTrackingDomains
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/SDWebImageSwiftUIDemo_macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "tv-marketing",
13 | "scale" : "1x"
14 | },
15 | {
16 | "idiom" : "tv-marketing",
17 | "scale" : "2x"
18 | }
19 | ],
20 | "info" : {
21 | "version" : 1,
22 | "author" : "xcode"
23 | }
24 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/HostingController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import WatchKit
10 | import Foundation
11 | import SwiftUI
12 |
13 | class HostingController: WKHostingController {
14 | override var body: ContentView {
15 | return ContentView()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "tv",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "tv",
9 | "scale" : "2x"
10 | },
11 | {
12 | "idiom" : "tv-marketing",
13 | "scale" : "1x"
14 | },
15 | {
16 | "idiom" : "tv-marketing",
17 | "scale" : "2x"
18 | }
19 | ],
20 | "info" : {
21 | "version" : 1,
22 | "author" : "xcode"
23 | }
24 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "wifi.exclamationmark.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | },
21 | "properties" : {
22 | "preserves-vector-representation" : true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Module/SDWebImageSwiftUI.h:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | #import
10 |
11 | //! Project version number for SDWebImageSwiftUI.
12 | FOUNDATION_EXPORT double SDWebImageSwiftUIVersionNumber;
13 |
14 | //! Project version string for SDWebImageSwiftUI.
15 | FOUNDATION_EXPORT const unsigned char SDWebImageSwiftUIVersionString[];
16 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/carthage.sh:
--------------------------------------------------------------------------------
1 | # carthage.sh
2 | # Usage example: ./carthage.sh build --platform iOS
3 |
4 | set -euo pipefail
5 |
6 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
7 | trap 'rm -f "$xcconfig"' INT TERM HUP EXIT
8 |
9 | # For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise
10 | # the build will fail on lipo due to duplicate architectures.
11 |
12 | echo 'EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig
13 | echo 'EXCLUDED_ARCHS[sdk=appletvsimulator*] = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig
14 | echo 'EXCLUDED_ARCHS[sdk=watchsimulator*] = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig
15 |
16 | export XCODE_XCCONFIG_FILE="$xcconfig"
17 | carthage "$@"
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Module/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 3.1.4
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "size" : "1280x768",
5 | "idiom" : "tv",
6 | "filename" : "App Icon - App Store.imagestack",
7 | "role" : "primary-app-icon"
8 | },
9 | {
10 | "size" : "400x240",
11 | "idiom" : "tv",
12 | "filename" : "App Icon.imagestack",
13 | "role" : "primary-app-icon"
14 | },
15 | {
16 | "size" : "2320x720",
17 | "idiom" : "tv",
18 | "filename" : "Top Shelf Image Wide.imageset",
19 | "role" : "top-shelf-image-wide"
20 | },
21 | {
22 | "size" : "1920x720",
23 | "idiom" : "tv",
24 | "filename" : "Top Shelf Image.imageset",
25 | "role" : "top-shelf-image"
26 | }
27 | ],
28 | "info" : {
29 | "version" : 1,
30 | "author" : "xcode"
31 | }
32 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit App/Base.lproj/Interface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Tests/TestUtils.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftUI
3 | import ViewInspector
4 | @testable import SDWebImageSwiftUI
5 |
6 | extension PlatformViewRepresentable where Self: Inspectable {
7 |
8 | func platformView() throws -> PlatformViewType {
9 | #if os(macOS)
10 | return try nsView()
11 | #else
12 | return try uiView()
13 | #endif
14 | }
15 | }
16 |
17 | class TestUtils {
18 | static var testBundle = Bundle(for: TestUtils.self)
19 |
20 | class func testImageBundle() -> Bundle {
21 | let imagePath = (testBundle.resourcePath! as NSString).appendingPathComponent("Images.bundle")
22 | return Bundle(path: imagePath)!
23 | }
24 |
25 | class func testImageData(name: String) -> Data? {
26 | guard let url = testImageBundle().url(forResource: name, withExtension: nil) else {
27 | return nil
28 | }
29 | return try? Data(contentsOf: url)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 | IDEWorkspaceChecks.plist
22 |
23 | # Bundler
24 | .bundle
25 |
26 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
27 | Carthage/Checkouts
28 | Carthage/Build
29 | Cartfile.resolved
30 |
31 | # We recommend against adding the Pods directory to your .gitignore. However
32 | # you should judge for yourself, the pros and cons are mentioned at:
33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
34 | #
35 | # Note: if you ignore the Pods directory, make sure to uncomment
36 | # `pod install` in .travis.yml
37 | #
38 | Pods/
39 | Podfile.lock
40 |
41 | # SwiftPM
42 | .build
43 | .swiftpm
44 | Package.resolved
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 lizhuoli1126@126.com
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit App/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | SDWebImageSwiftUIDemo-watchOS WatchKit App
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | UISupportedInterfaceOrientations
24 |
25 | UIInterfaceOrientationPortrait
26 | UIInterfaceOrientationPortraitUpsideDown
27 |
28 | WKWatchKitApp
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIRequiredDeviceCapabilities
26 |
27 | arm64
28 |
29 | UIUserInterfaceStyle
30 | Automatic
31 | NSAppTransportSecurity
32 |
33 | NSAllowsArbitraryLoads
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "idiom" : "watch",
5 | "filename" : "Circular.imageset",
6 | "role" : "circular"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "filename" : "Extra Large.imageset",
11 | "role" : "extra-large"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "filename" : "Graphic Bezel.imageset",
16 | "role" : "graphic-bezel"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "filename" : "Graphic Circular.imageset",
21 | "role" : "graphic-circular"
22 | },
23 | {
24 | "idiom" : "watch",
25 | "filename" : "Graphic Corner.imageset",
26 | "role" : "graphic-corner"
27 | },
28 | {
29 | "idiom" : "watch",
30 | "filename" : "Graphic Large Rectangular.imageset",
31 | "role" : "graphic-large-rectangular"
32 | },
33 | {
34 | "idiom" : "watch",
35 | "filename" : "Modular.imageset",
36 | "role" : "modular"
37 | },
38 | {
39 | "idiom" : "watch",
40 | "filename" : "Utilitarian.imageset",
41 | "role" : "utilitarian"
42 | }
43 | ],
44 | "info" : {
45 | "version" : 1,
46 | "author" : "xcode"
47 | }
48 | }
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SDWebImageSwiftUI",
8 | platforms: [
9 | .macOS(.v11), .iOS(.v14), .tvOS(.v14), .watchOS(.v7)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "SDWebImageSwiftUI",
15 | targets: ["SDWebImageSwiftUI"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.21.1")
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
25 | .target(
26 | name: "SDWebImageSwiftUI",
27 | dependencies: ["SDWebImage"],
28 | path: "SDWebImageSwiftUI",
29 | sources: ["Classes"],
30 | resources: [.copy("Resources/PrivacyInfo.xcprivacy")]
31 | ),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2019 CocoaPods. All rights reserved.
27 | NSMainStoryboardFile
28 | Main
29 | NSPrincipalClass
30 | NSApplication
31 | NSSupportsAutomaticTermination
32 |
33 | NSSupportsSuddenTermination
34 |
35 | NSAppTransportSecurity
36 |
37 | NSAllowsArbitraryLoads
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | SDWebImageSwiftUIDemo-watchOS WatchKit Extension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | NSExtension
24 |
25 | NSExtensionAttributes
26 |
27 | WKAppBundleIdentifier
28 | com.dreampiggy.SDWebImageSwiftUIDemo-watchOS.watchkitapp
29 |
30 | NSExtensionPointIdentifier
31 | com.apple.watchkit
32 |
33 | WKExtensionDelegateClassName
34 | $(PRODUCT_MODULE_NAME).ExtensionDelegate
35 | WKWatchOnly
36 |
37 | NSAppTransportSecurity
38 |
39 | NSAllowsArbitraryLoads
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Tests/ImageManagerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftUI
3 | import ViewInspector
4 | @testable import SDWebImageSwiftUI
5 |
6 | class ImageManagerTests: XCTestCase {
7 |
8 | override func setUp() {
9 | super.setUp()
10 | // Put setup code here. This method is called before the invocation of each test method in the class.
11 | }
12 |
13 | override func tearDown() {
14 | // Put teardown code here. This method is called after the invocation of each test method in the class.
15 | super.tearDown()
16 | }
17 |
18 | func testImageManager() throws {
19 | let expectation = self.expectation(description: "ImageManager usage with Combine")
20 | let imageUrl = URL(string: "https://placehold.co/500x500.jpg")
21 | let imageManager = ImageManager()
22 | imageManager.setOnSuccess { image, cacheType, data in
23 | XCTAssertNotNil(image)
24 | expectation.fulfill()
25 | }
26 | imageManager.setOnFailure { error in
27 | XCTFail()
28 | }
29 | imageManager.setOnProgress { receivedSize, expectedSize in
30 |
31 | }
32 | imageManager.load(url: imageUrl)
33 | XCTAssertNotNil(imageManager.currentOperation)
34 | let sub = imageManager.objectWillChange
35 | .subscribe(on: RunLoop.main)
36 | .receive(on: RunLoop.main)
37 | .sink { value in
38 | print(value)
39 | }
40 | sub.cancel()
41 | self.waitForExpectations(timeout: 10, handler: nil)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/wifi.exclamationmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint SDWebImageSwiftUI.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'SDWebImageSwiftUI'
11 | s.version = '3.1.4'
12 | s.summary = 'SwiftUI Image loading and Animation framework powered by SDWebImage'
13 |
14 | s.description = <<-DESC
15 | SDWebImageSwiftUI is a SwiftUI image loading framework, which based on SDWebImage.
16 | It brings all your favorite features from SDWebImage, like async image loading, memory/disk caching, animated image playback and performances.
17 | DESC
18 |
19 | s.homepage = 'https://github.com/SDWebImage/SDWebImageSwiftUI'
20 | s.license = { :type => 'MIT', :file => 'LICENSE' }
21 | s.author = { 'DreamPiggy' => 'lizhuoli1126@126.com' }
22 | s.source = { :git => 'https://github.com/SDWebImage/SDWebImageSwiftUI.git', :tag => s.version.to_s }
23 |
24 | s.ios.deployment_target = '14.0'
25 | s.osx.deployment_target = '11.0'
26 | s.tvos.deployment_target = '14.0'
27 | s.watchos.deployment_target = '7.0'
28 | s.visionos.deployment_target = '1.0'
29 |
30 | s.source_files = 'SDWebImageSwiftUI/Classes/**/*', 'SDWebImageSwiftUI/Module/*.h'
31 | s.pod_target_xcconfig = {
32 | 'SUPPORTS_MACCATALYST' => 'YES',
33 | 'DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER' => 'NO',
34 | }
35 | s.resource_bundles = {
36 | 'SDWebImageSwiftUI' => ['SDWebImageSwiftUI/Resources/PrivacyInfo.xcprivacy'],
37 | }
38 |
39 | s.weak_frameworks = 'SwiftUI', 'Combine'
40 | s.dependency 'SDWebImage', '~> 5.21.1'
41 | s.swift_version = '5.3'
42 | end
43 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-visionOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import UIKit
11 | import SDWebImage
12 | import SDWebImageWebPCoder
13 | import SDWebImageSVGCoder
14 | import SDWebImagePDFCoder
15 |
16 | // no changes in your AppDelegate class
17 | class AppDelegate: NSObject, UIApplicationDelegate {
18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
19 | // Add WebP/SVG/PDF support
20 | SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
21 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
22 | SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
23 | // Dynamic check to support vector format for both WebImage/AnimatedImage
24 | SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
25 | var options = options
26 | if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
27 | // AnimatedImage supports vector rendering, should not force decode
28 | options.insert(.avoidDecodeImage)
29 | }
30 | return SDWebImageOptionsResult(options: options, context: context)
31 | }
32 | return true
33 | }
34 | }
35 |
36 | @main
37 | struct SDWebImageSwiftUIDemo: App {
38 | // inject into SwiftUI life-cycle via adaptor
39 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
40 |
41 | var body: some Scene {
42 | WindowGroup {
43 | ContentView()
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "24x24",
5 | "idiom" : "watch",
6 | "scale" : "2x",
7 | "role" : "notificationCenter",
8 | "subtype" : "38mm"
9 | },
10 | {
11 | "size" : "27.5x27.5",
12 | "idiom" : "watch",
13 | "scale" : "2x",
14 | "role" : "notificationCenter",
15 | "subtype" : "42mm"
16 | },
17 | {
18 | "size" : "29x29",
19 | "idiom" : "watch",
20 | "role" : "companionSettings",
21 | "scale" : "2x"
22 | },
23 | {
24 | "size" : "29x29",
25 | "idiom" : "watch",
26 | "role" : "companionSettings",
27 | "scale" : "3x"
28 | },
29 | {
30 | "size" : "40x40",
31 | "idiom" : "watch",
32 | "scale" : "2x",
33 | "role" : "appLauncher",
34 | "subtype" : "38mm"
35 | },
36 | {
37 | "size" : "44x44",
38 | "idiom" : "watch",
39 | "scale" : "2x",
40 | "role" : "appLauncher",
41 | "subtype" : "40mm"
42 | },
43 | {
44 | "size" : "50x50",
45 | "idiom" : "watch",
46 | "scale" : "2x",
47 | "role" : "appLauncher",
48 | "subtype" : "44mm"
49 | },
50 | {
51 | "size" : "86x86",
52 | "idiom" : "watch",
53 | "scale" : "2x",
54 | "role" : "quickLook",
55 | "subtype" : "38mm"
56 | },
57 | {
58 | "size" : "98x98",
59 | "idiom" : "watch",
60 | "scale" : "2x",
61 | "role" : "quickLook",
62 | "subtype" : "42mm"
63 | },
64 | {
65 | "size" : "108x108",
66 | "idiom" : "watch",
67 | "scale" : "2x",
68 | "role" : "quickLook",
69 | "subtype" : "44mm"
70 | },
71 | {
72 | "idiom" : "watch-marketing",
73 | "size" : "1024x1024",
74 | "scale" : "1x"
75 | }
76 | ],
77 | "info" : {
78 | "version" : 1,
79 | "author" : "xcode"
80 | }
81 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUITests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUITests macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUITests tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/SDWebImageSwiftUI.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import Foundation
10 | import SwiftUI
11 | @_exported import SDWebImage // Automatically import SDWebImage
12 |
13 | #if os(macOS)
14 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
15 | public typealias PlatformImage = NSImage
16 | #else
17 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
18 | public typealias PlatformImage = UIImage
19 | #endif
20 |
21 | #if os(macOS)
22 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
23 | public typealias PlatformView = NSView
24 | #endif
25 | #if os(iOS) || os(tvOS) || os(visionOS)
26 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
27 | public typealias PlatformView = UIView
28 | #endif
29 | #if os(watchOS)
30 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
31 | public typealias PlatformView = WKInterfaceObject
32 | #endif
33 |
34 | #if os(macOS)
35 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
36 | public typealias PlatformViewRepresentable = NSViewRepresentable
37 | #endif
38 | #if os(iOS) || os(tvOS) || os(visionOS)
39 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
40 | public typealias PlatformViewRepresentable = UIViewRepresentable
41 | #endif
42 | #if os(watchOS)
43 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
44 | public typealias PlatformViewRepresentable = WKInterfaceObjectRepresentable
45 | #endif
46 |
47 | #if os(macOS)
48 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
49 | extension NSViewRepresentable {
50 | typealias PlatformViewType = NSViewType
51 | }
52 | #endif
53 | #if os(iOS) || os(tvOS) || os(visionOS)
54 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
55 | extension UIViewRepresentable {
56 | typealias PlatformViewType = UIViewType
57 | }
58 | #endif
59 | #if os(watchOS)
60 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
61 | extension WKInterfaceObjectRepresentable {
62 | typealias PlatformViewType = WKInterfaceObjectType
63 | }
64 | #endif
65 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-macOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import Cocoa
10 | import SwiftUI
11 | import SDWebImage
12 | import SDWebImageWebPCoder
13 | import SDWebImageSVGCoder
14 | import SDWebImagePDFCoder
15 |
16 | @NSApplicationMain
17 | class AppDelegate: NSObject, NSApplicationDelegate {
18 |
19 | var window: NSWindow!
20 |
21 |
22 | func applicationDidFinishLaunching(_ aNotification: Notification) {
23 | // Create the SwiftUI view that provides the window contents.
24 | let contentView = ContentView()
25 |
26 | // Create the window and set the content view.
27 | window = NSWindow(
28 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
29 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
30 | backing: .buffered, defer: false)
31 | window.center()
32 | window.setFrameAutosaveName("Main Window")
33 | window.contentView = NSHostingView(rootView: contentView)
34 | window.makeKeyAndOrderFront(nil)
35 | // Add WebP/SVG/PDF support
36 | SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
37 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
38 | SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
39 | // Dynamic check to support vector format for both WebImage/AnimatedImage
40 | SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
41 | var options = options
42 | if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
43 | // AnimatedImage supports vector rendering, should not force decode
44 | options.insert(.avoidDecodeImage)
45 | }
46 | return SDWebImageOptionsResult(options: options, context: context)
47 | }
48 | }
49 |
50 | func applicationWillTerminate(_ aNotification: Notification) {
51 | // Insert code here to tear down your application
52 | }
53 |
54 |
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/Image.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
13 | extension Image {
14 | @inlinable init(platformImage: PlatformImage) {
15 | #if os(macOS)
16 | self.init(nsImage: platformImage)
17 | #else
18 | self.init(uiImage: platformImage)
19 | #endif
20 | }
21 | }
22 |
23 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
24 | extension PlatformImage {
25 | static var empty = PlatformImage()
26 | }
27 |
28 | #if !os(macOS)
29 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
30 | extension PlatformImage.Orientation {
31 | @inlinable var toSwiftUI: Image.Orientation {
32 | switch self {
33 | case .up:
34 | return .up
35 | case .upMirrored:
36 | return .upMirrored
37 | case .down:
38 | return .down
39 | case .downMirrored:
40 | return .downMirrored
41 | case .left:
42 | return .left
43 | case .leftMirrored:
44 | return .leftMirrored
45 | case .right:
46 | return .right
47 | case .rightMirrored:
48 | return .rightMirrored
49 | @unknown default:
50 | return .up
51 | }
52 | }
53 | }
54 |
55 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
56 | extension Image.Orientation {
57 | @inlinable var toPlatform: PlatformImage.Orientation {
58 | switch self {
59 | case .up:
60 | return .up
61 | case .upMirrored:
62 | return .upMirrored
63 | case .down:
64 | return .down
65 | case .downMirrored:
66 | return .downMirrored
67 | case .left:
68 | return .left
69 | case .leftMirrored:
70 | return .leftMirrored
71 | case .right:
72 | return .right
73 | case .rightMirrored:
74 | return .rightMirrored
75 | }
76 | }
77 | }
78 | #endif
79 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | UIApplicationSceneManifest
29 |
30 | UIApplicationSupportsMultipleScenes
31 |
32 | UISceneConfigurations
33 |
34 | UIWindowSceneSessionRoleApplication
35 |
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UISceneConfigurationName
40 | Default Configuration
41 | UISceneDelegateClassName
42 | $(PRODUCT_MODULE_NAME).SceneDelegate
43 |
44 |
45 |
46 |
47 | UILaunchStoryboardName
48 | LaunchScreen
49 | UIRequiredDeviceCapabilities
50 |
51 | armv7
52 |
53 | UISupportedInterfaceOrientations
54 |
55 | UIInterfaceOrientationPortrait
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 | UISupportedInterfaceOrientations~ipad
60 |
61 | UIInterfaceOrientationPortrait
62 | UIInterfaceOrientationPortraitUpsideDown
63 | UIInterfaceOrientationLandscapeLeft
64 | UIInterfaceOrientationLandscapeRight
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode12
3 |
4 | env:
5 | global:
6 | - LC_CTYPE=en_US.UTF-8
7 | - LANG=en_US.UTF-8
8 |
9 | addons:
10 | ssh_known_hosts: github.com
11 |
12 | notifications:
13 | email: false
14 |
15 | before_install:
16 | - env
17 | - locale
18 | - gem install cocoapods --no-document --quiet
19 | - gem install xcpretty --no-document --quiet
20 | - pod --version
21 | - pod repo update --silent
22 | - xcpretty --version
23 | - xcodebuild -version
24 | - xcodebuild -showsdks
25 |
26 | script:
27 | - set -o pipefail
28 |
29 | - echo Check if the library described by the podspec can be built
30 | - pod lib lint --allow-warnings
31 |
32 | - echo Build example
33 | - pod install --project-directory=Example
34 | - xcodebuild build -workspace Example/SDWebImageSwiftUI.xcworkspace -scheme SDWebImageSwiftUIDemo -destination 'name=iPhone 11 Pro Max' -configuration Debug | xcpretty -c
35 |
36 | - ./carthage.sh update --configuration Debug
37 | - xcodebuild build -project SDWebImageSwiftUI.xcodeproj -scheme 'SDWebImageSwiftUI' -destination 'name=iPhone 11 Pro Max' -configuration Debug | xcpretty -c
38 |
39 | - echo Clean DerivedData
40 | - rm -rf ~/Library/Developer/Xcode/DerivedData/
41 | - mkdir DerivedData
42 |
43 | - echo Run the tests
44 | - xcodebuild clean test -project SDWebImageSwiftUI.xcodeproj -scheme 'SDWebImageSwiftUITests' -destination 'name=iPhone 11 Pro Max' -configuration Debug | xcpretty -c
45 | - mv ~/Library/Developer/Xcode/DerivedData/ ./DerivedData/iOS
46 | - xcodebuild clean test -project SDWebImageSwiftUI.xcodeproj -scheme 'SDWebImageSwiftUITests macOS' -destination 'platform=macOS,arch=x86_64' -configuration Debug | xcpretty -c
47 | - mv ~/Library/Developer/Xcode/DerivedData/ ./DerivedData/macOS
48 | - xcodebuild clean test -project SDWebImageSwiftUI.xcodeproj -scheme 'SDWebImageSwiftUITests tvOS' -destination 'platform=tvOS Simulator,name=Apple TV' -configuration Debug | xcpretty -c
49 | - mv ~/Library/Developer/Xcode/DerivedData/ ./DerivedData/tvOS
50 |
51 | after_success:
52 | - bash <(curl -s https://codecov.io/bash) -D './DerivedData/iOS' -J '^SDWebImageSwiftUI$' -F ios
53 | - bash <(curl -s https://codecov.io/bash) -D './DerivedData/macOS' -J '^SDWebImageSwiftUI$' -F macos
54 | - bash <(curl -s https://codecov.io/bash) -D './DerivedData/tvOS' -J '^SDWebImageSwiftUI$'-F tvos
55 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | install! 'cocoapods', :warn_for_unused_master_specs_repo => false
2 | use_frameworks!
3 |
4 | def all_pods
5 | pod 'SDWebImageSwiftUI', :path => './'
6 | pod 'SDWebImageWebPCoder'
7 | pod 'SDWebImageSVGCoder'
8 | pod 'SDWebImagePDFCoder'
9 | pod 'SDWebImageAVIFCoder'
10 | pod 'libavif', :subspecs => ['libdav1d']
11 | end
12 |
13 | def all_test_pods
14 | pod 'SDWebImageSwiftUI', :path => './'
15 | end
16 |
17 | example_project_path = 'Example/SDWebImageSwiftUI'
18 | test_project_path = 'Example/SDWebImageSwiftUI'
19 | workspace 'SDWebImageSwiftUI.xcworkspace'
20 |
21 | target 'SDWebImageSwiftUIDemo' do
22 | project example_project_path
23 | platform :ios, '14.0'
24 | all_pods
25 | end
26 |
27 | target 'SDWebImageSwiftUIDemo-macOS' do
28 | project example_project_path
29 | platform :osx, '11.0'
30 | all_pods
31 | end
32 |
33 | target 'SDWebImageSwiftUIDemo-tvOS' do
34 | project example_project_path
35 | platform :tvos, '14.0'
36 | all_pods
37 | end
38 |
39 | target 'SDWebImageSwiftUIDemo-watchOS WatchKit Extension' do
40 | project example_project_path
41 | platform :watchos, '7.0'
42 | all_pods
43 | end
44 |
45 | # Test Project
46 | target 'SDWebImageSwiftUITests' do
47 | project test_project_path
48 | platform :ios, '14.0'
49 | all_test_pods
50 | end
51 |
52 | target 'SDWebImageSwiftUITests macOS' do
53 | project test_project_path
54 | platform :osx, '11.0'
55 | all_test_pods
56 | end
57 |
58 | target 'SDWebImageSwiftUITests tvOS' do
59 | project test_project_path
60 | platform :tvos, '14.0'
61 | all_test_pods
62 | end
63 |
64 |
65 | # Inject macro during SDWebImage Demo and Tests
66 | post_install do |installer_representation|
67 | installer_representation.pods_project.targets.each do |target|
68 | if target.product_name == 'SDWebImage'
69 | target.build_configurations.each do |config|
70 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) SD_CHECK_CGIMAGE_RETAIN_SOURCE=1'
71 | end
72 | elsif target.product_name == 'SDWebImageSwiftUI'
73 | # Do nothing
74 | else
75 | target.build_configurations.each do |config|
76 | # Override the min deployment target for some test specs to workaround `libarclite.a` missing issue
77 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
78 | config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.11'
79 | config.build_settings['TVOS_DEPLOYMENT_TARGET'] = '9.0'
80 | config.build_settings['WATCHOS_DEPLOYMENT_TARGET'] = '2.0'
81 | config.build_settings['XROS_DEPLOYMENT_TARGET'] = '1.0'
82 | end
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUI macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUI tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUI watchOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUI visionOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
43 |
49 |
50 |
56 |
57 |
58 |
59 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import UIKit
10 | import SDWebImage
11 | import SDWebImageWebPCoder
12 | #if canImport(SDWebImageAVIFCoder)
13 | import SDWebImageAVIFCoder
14 | #endif
15 | import SDWebImageSVGCoder
16 | import SDWebImagePDFCoder
17 |
18 | @UIApplicationMain
19 | class AppDelegate: UIResponder, UIApplicationDelegate {
20 |
21 |
22 |
23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
24 | // Override point for customization after application launch.
25 | // Add WebP/SVG/PDF support
26 | SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
27 | #if canImport(SDWebImageAVIFCoder)
28 | SDImageCodersManager.shared.addCoder(SDImageAVIFCoder.shared)
29 | #endif
30 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
31 | SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
32 | // Dynamic check to support vector format for both WebImage/AnimatedImage
33 | SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
34 | var options = options
35 | if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
36 | // AnimatedImage supports vector rendering, should not force decode
37 | options.insert(.avoidDecodeImage)
38 | }
39 | return SDWebImageOptionsResult(options: options, context: context)
40 | }
41 | return true
42 | }
43 |
44 | // MARK: UISceneSession Lifecycle
45 |
46 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
47 | // Called when a new scene session is being created.
48 | // Use this method to select a configuration to create the new scene with.
49 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
50 | }
51 |
52 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
53 | // Called when the user discards a scene session.
54 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
55 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
56 | }
57 |
58 |
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
21 |
22 | // Use a UIHostingController as window root view controller
23 | if let windowScene = scene as? UIWindowScene {
24 | let window = UIWindow(windowScene: windowScene)
25 | window.rootViewController = UIHostingController(rootView: ContentView())
26 | self.window = window
27 | window.makeKeyAndVisible()
28 | }
29 | }
30 |
31 | func sceneDidDisconnect(_ scene: UIScene) {
32 | // Called as the scene is being released by the system.
33 | // This occurs shortly after the scene enters the background, or when its session is discarded.
34 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
35 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
36 | }
37 |
38 | func sceneDidBecomeActive(_ scene: UIScene) {
39 | // Called when the scene has moved from an inactive state to an active state.
40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
41 | }
42 |
43 | func sceneWillResignActive(_ scene: UIScene) {
44 | // Called when the scene will move from an active state to an inactive state.
45 | // This may occur due to temporary interruptions (ex. an incoming phone call).
46 | }
47 |
48 | func sceneWillEnterForeground(_ scene: UIScene) {
49 | // Called as the scene transitions from the background to the foreground.
50 | // Use this method to undo the changes made on entering the background.
51 | }
52 |
53 | func sceneDidEnterBackground(_ scene: UIScene) {
54 | // Called as the scene transitions from the foreground to the background.
55 | // Use this method to save data, release shared resources, and store enough scene-specific state information
56 | // to restore the scene back to its current state.
57 | }
58 |
59 |
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-visionOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-watchOS WatchKit Extension/ExtensionDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import WatchKit
10 | import SDWebImage
11 | import SDWebImageWebPCoder
12 | import SDWebImageSVGCoder
13 | import SDWebImagePDFCoder
14 |
15 | class ExtensionDelegate: NSObject, WKExtensionDelegate {
16 |
17 | func applicationDidFinishLaunching() {
18 | // Perform any final initialization of your application.
19 | // Add WebP/SVG/PDF support
20 | SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
21 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
22 | SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
23 | }
24 |
25 | func applicationDidBecomeActive() {
26 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
27 | }
28 |
29 | func applicationWillResignActive() {
30 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
31 | // Use this method to pause ongoing tasks, disable timers, etc.
32 | }
33 |
34 | func handle(_ backgroundTasks: Set) {
35 | // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
36 | for task in backgroundTasks {
37 | // Use a switch statement to check the task type
38 | switch task {
39 | case let backgroundTask as WKApplicationRefreshBackgroundTask:
40 | // Be sure to complete the background task once you’re done.
41 | backgroundTask.setTaskCompletedWithSnapshot(false)
42 | case let snapshotTask as WKSnapshotRefreshBackgroundTask:
43 | // Snapshot tasks have a unique completion call, make sure to set your expiration date
44 | snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
45 | case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
46 | // Be sure to complete the connectivity task once you’re done.
47 | connectivityTask.setTaskCompletedWithSnapshot(false)
48 | case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
49 | // Be sure to complete the URL session task once you’re done.
50 | urlSessionTask.setTaskCompletedWithSnapshot(false)
51 | case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
52 | // Be sure to complete the relevant-shortcut task once you're done.
53 | relevantShortcutTask.setTaskCompletedWithSnapshot(false)
54 | case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
55 | // Be sure to complete the intent-did-run task once you're done.
56 | intentDidRunTask.setTaskCompletedWithSnapshot(false)
57 | default:
58 | // make sure to complete unhandled task types
59 | task.setTaskCompletedWithSnapshot(false)
60 | }
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUI.xcodeproj/xcshareddata/xcschemes/SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
57 |
59 |
65 |
66 |
67 |
68 |
74 |
76 |
82 |
83 |
84 |
85 |
87 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/Transition/Transition.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
12 | extension AnyTransition {
13 |
14 | /// Fade-in transition
15 | public static var fade: AnyTransition {
16 | let insertion = AnyTransition.opacity
17 | let removal = AnyTransition.identity
18 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
19 | }
20 |
21 | /// Fade-in transition with duration
22 | /// - Parameter duration: transition duration, use ease-in-out
23 | /// - Returns: A transition with duration
24 | public static func fade(duration: Double) -> AnyTransition {
25 | let insertion = AnyTransition.opacity.animation(.easeInOut(duration: duration))
26 | let removal = AnyTransition.identity
27 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
28 | }
29 |
30 | /// Flip from left transition
31 | public static var flipFromLeft: AnyTransition {
32 | let insertion = AnyTransition.move(edge: .leading)
33 | let removal = AnyTransition.identity
34 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
35 | }
36 |
37 | /// Flip from left transition with duration
38 | /// - Parameter duration: transition duration, use ease-in-out
39 | /// - Returns: A transition with duration
40 | public static func flipFromLeft(duration: Double) -> AnyTransition {
41 | let insertion = AnyTransition.move(edge: .leading).animation(.easeInOut(duration: duration))
42 | let removal = AnyTransition.identity
43 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
44 | }
45 |
46 | /// Flip from right transition
47 | public static var flipFromRight: AnyTransition {
48 | let insertion = AnyTransition.move(edge: .trailing)
49 | let removal = AnyTransition.identity
50 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
51 | }
52 |
53 | /// Flip from right transition with duration
54 | /// - Parameter duration: transition duration, use ease-in-out
55 | /// - Returns: A transition with duration
56 | public static func flipFromRight(duration: Double) -> AnyTransition {
57 | let insertion = AnyTransition.move(edge: .trailing).animation(.easeInOut(duration: duration))
58 | let removal = AnyTransition.identity
59 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
60 | }
61 |
62 | /// Flip from top transition
63 | public static var flipFromTop: AnyTransition {
64 | let insertion = AnyTransition.move(edge: .top)
65 | let removal = AnyTransition.identity
66 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
67 | }
68 |
69 | /// Flip from top transition with duration
70 | /// - Parameter duration: transition duration, use ease-in-out
71 | /// - Returns: A transition with duration
72 | public static func flipFromTop(duration: Double) -> AnyTransition {
73 | let insertion = AnyTransition.move(edge: .top).animation(.easeInOut(duration: duration))
74 | let removal = AnyTransition.identity
75 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
76 | }
77 |
78 | /// Flip from bottom transition
79 | public static var flipFromBottom: AnyTransition {
80 | let insertion = AnyTransition.move(edge: .bottom)
81 | let removal = AnyTransition.identity
82 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
83 | }
84 |
85 | /// Flip from bottom transition with duration
86 | /// - Parameter duration: transition duration, use ease-in-out
87 | /// - Returns: A transition with duration
88 | public static func flipFromBottom(duration: Double) -> AnyTransition {
89 | let insertion = AnyTransition.move(edge: .bottom).animation(.easeInOut(duration: duration))
90 | let removal = AnyTransition.identity
91 | return AnyTransition.asymmetric(insertion: insertion, removal: removal)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/Indicator/Indicator.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import Combine
11 |
12 | /// A type to build the indicator
13 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
14 | public struct Indicator where T : View {
15 | var content: (Binding, Binding) -> T
16 |
17 | /// Create a indicator with builder
18 | /// - Parameter builder: A builder to build indicator
19 | /// - Parameter isAnimating: A Binding to control the animation. If image is during loading, the value is true, else (like start loading) the value is false.
20 | /// - Parameter progress: A Binding to control the progress during loading. Value between [0.0, 1.0]. If no progress can be reported, the value is 0.
21 | /// Associate a indicator when loading image with url
22 | public init(@ViewBuilder content: @escaping (_ isAnimating: Binding, _ progress: Binding) -> T) {
23 | self.content = content
24 | }
25 | }
26 |
27 | /// A observable model to report indicator loading status
28 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
29 | public class IndicatorStatus : ObservableObject {
30 | /// whether indicator is loading or not
31 | var isLoading: Bool = false {
32 | didSet {
33 | DispatchQueue.main.async {
34 | self.objectWillChange.send()
35 | }
36 | }
37 | }
38 | /// indicator progress, should only be used for indicator binding, value between [0.0, 1.0]
39 | var progress: Double = 0 {
40 | didSet {
41 | DispatchQueue.main.async {
42 | self.objectWillChange.send()
43 | }
44 | }
45 | }
46 | }
47 |
48 | /// A implementation detail View Modifier with indicator
49 | /// SwiftUI View Modifier construced by using a internal View type which modify the `body`
50 | /// It use type system to represent the view hierarchy, and Swift `some View` syntax to hide the type detail for users
51 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
52 | public struct IndicatorViewModifier : ViewModifier where T : View {
53 |
54 | /// The loading status
55 | @ObservedObject public var status: IndicatorStatus
56 |
57 | /// The indicator
58 | public var indicator: Indicator
59 |
60 | @ViewBuilder
61 | private var overlay: some View {
62 | if status.isLoading {
63 | indicator.content($status.isLoading, $status.progress)
64 | }
65 | }
66 |
67 | public func body(content: Content) -> some View {
68 | ZStack {
69 | content
70 | overlay
71 | }
72 | }
73 | }
74 |
75 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
76 | extension Indicator where T == AnyView {
77 | /// Activity Indicator
78 | public static var activity: Indicator {
79 | Indicator { isAnimating, _ in
80 | AnyView(ProgressView().opacity(isAnimating.wrappedValue ? 1 : 0))
81 | }
82 | }
83 |
84 | /// Activity Indicator with style
85 | /// - Parameter style: style
86 | public static func activity(style: S) -> Indicator where S: ProgressViewStyle {
87 | Indicator { isAnimating, _ in
88 | AnyView(ProgressView().progressViewStyle(style).opacity(isAnimating.wrappedValue ? 1 : 0))
89 | }
90 | }
91 | }
92 |
93 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
94 | extension Indicator where T == AnyView {
95 | /// Progress Indicator
96 | public static var progress: Indicator {
97 | Indicator { isAnimating, progress in
98 | AnyView(ProgressView(value: progress.wrappedValue).opacity(isAnimating.wrappedValue ? 1 : 0))
99 | }
100 | }
101 |
102 | /// Progress Indicator with style
103 | /// - Parameter style: style
104 | public static func progress(style: S) -> Indicator where S: ProgressViewStyle {
105 | Indicator { isAnimating, progress in
106 | AnyView(ProgressView(value: progress.wrappedValue).progressViewStyle(style).opacity(isAnimating.wrappedValue ? 1 : 0))
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/ImagePlayer.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import Combine
11 | import SDWebImage
12 |
13 | /// A Image observable object for handle aniamted image playback. This is used to avoid `@State` update may capture the View struct type and cause memory leak.
14 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
15 | public final class ImagePlayer : ObservableObject {
16 | var player: SDAnimatedImagePlayer?
17 |
18 | /// Max buffer size
19 | public var maxBufferSize: UInt?
20 |
21 | /// Custom loop count
22 | public var customLoopCount: UInt?
23 |
24 | /// Animation runloop mode
25 | public var runLoopMode: RunLoop.Mode = .common
26 |
27 | /// Animation playback rate
28 | public var playbackRate: Double = 1.0
29 |
30 | /// Animation playback mode
31 | public var playbackMode: SDAnimatedImagePlaybackMode = .normal
32 |
33 | deinit {
34 | player?.stopPlaying()
35 | }
36 |
37 | /// Current playing frame image
38 | @Published public var currentFrame: PlatformImage?
39 |
40 | /// Current playing frame index
41 | @Published public var currentFrameIndex: UInt = 0
42 |
43 | /// Current playing loop count
44 | @Published public var currentLoopCount: UInt = 0
45 |
46 | var currentAnimatedImage: (PlatformImage & SDAnimatedImageProvider)?
47 |
48 | /// Whether current player is valid for playing. This will check the internal player exist or not
49 | public var isValid: Bool {
50 | player != nil
51 | }
52 |
53 | /// Current playing status
54 | public var isPlaying: Bool {
55 | player?.isPlaying ?? false
56 | }
57 |
58 | /// Start the animation
59 | public func startPlaying() {
60 | player?.startPlaying()
61 | }
62 |
63 | /// Pause the animation
64 | public func pausePlaying() {
65 | player?.pausePlaying()
66 | }
67 |
68 | /// Stop the animation
69 | public func stopPlaying() {
70 | player?.stopPlaying()
71 | }
72 |
73 | /// Seek to frame and loop count
74 | public func seekToFrame(at: UInt, loopCount: UInt) {
75 | player?.seekToFrame(at: at, loopCount: loopCount)
76 | }
77 |
78 | /// Clear the frame buffer
79 | public func clearFrameBuffer() {
80 | player?.clearFrameBuffer()
81 | }
82 |
83 | /// Setup the player using Animated Image.
84 | /// After setup, you can always check `isValid` status, or call `startPlaying` to play the animation.
85 | /// - Parameter image: animated image
86 | public func setupPlayer(animatedImage: PlatformImage & SDAnimatedImageProvider) {
87 | if isValid {
88 | return
89 | }
90 | currentAnimatedImage = animatedImage
91 | if let imagePlayer = SDAnimatedImagePlayer(provider: animatedImage) {
92 | imagePlayer.animationFrameHandler = { [weak self] (index, frame) in
93 | guard let self = self else {
94 | return
95 | }
96 | if (self.isPlaying) {
97 | self.currentFrameIndex = index
98 | self.currentFrame = frame
99 | }
100 | }
101 | imagePlayer.animationLoopHandler = { [weak self] (loopCount) in
102 | guard let self = self else {
103 | return
104 | }
105 | if (self.isPlaying) {
106 | self.currentLoopCount = loopCount
107 | }
108 | }
109 | // Setup configuration
110 | if let maxBufferSize = maxBufferSize {
111 | imagePlayer.maxBufferSize = maxBufferSize
112 | }
113 | if let customLoopCount = customLoopCount {
114 | imagePlayer.totalLoopCount = customLoopCount
115 | }
116 | imagePlayer.runLoopMode = runLoopMode
117 | imagePlayer.playbackRate = playbackRate
118 | imagePlayer.playbackMode = playbackMode
119 |
120 | self.player = imagePlayer
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/ImageViewWrapper.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import Foundation
10 | import SDWebImage
11 | import SwiftUI
12 |
13 | #if !os(watchOS)
14 |
15 | /// Use wrapper to solve tne `UIImageView`/`NSImageView` frame size become image size issue (SwiftUI's Bug)
16 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
17 | public class AnimatedImageViewWrapper : PlatformView {
18 | /// The wrapped actual image view, using SDWebImage's aniamted image view
19 | @objc dynamic public var wrapped = SDAnimatedImageView()
20 | var observation: NSKeyValueObservation?
21 | var interpolationQuality = CGInterpolationQuality.default
22 | var shouldAntialias = false
23 | var resizingMode: Image.ResizingMode?
24 |
25 | deinit {
26 | observation?.invalidate()
27 | }
28 |
29 | public override func draw(_ rect: CGRect) {
30 | #if os(macOS)
31 | guard let ctx = NSGraphicsContext.current?.cgContext else {
32 | return
33 | }
34 | #else
35 | guard let ctx = UIGraphicsGetCurrentContext() else {
36 | return
37 | }
38 | #endif
39 | ctx.interpolationQuality = interpolationQuality
40 | ctx.setShouldAntialias(shouldAntialias)
41 | }
42 |
43 | #if os(macOS)
44 | public override func layout() {
45 | super.layout()
46 | wrapped.frame = self.bounds
47 | }
48 | #else
49 | public override func layoutSubviews() {
50 | super.layoutSubviews()
51 | wrapped.frame = self.bounds
52 | }
53 | #endif
54 |
55 | public override var intrinsicContentSize: CGSize {
56 | /// Match the behavior of SwiftUI.Image, only when image is resizable, use the super implementation to calculate size
57 | let contentSize = wrapped.intrinsicContentSize
58 | if let _ = resizingMode {
59 | /// Keep aspect ratio
60 | if contentSize.width > 0 && contentSize.height > 0 {
61 | let ratio = contentSize.width / contentSize.height
62 | let size = CGSize(width: ratio, height: 1)
63 | return size
64 | } else {
65 | return contentSize
66 | }
67 | } else {
68 | /// Not resizable, always use image size, like SwiftUI.Image
69 | return contentSize
70 | }
71 | }
72 |
73 | public override init(frame frameRect: CGRect) {
74 | super.init(frame: frameRect)
75 | addSubview(wrapped)
76 | observation = observe(\.wrapped.image, options: [.new]) { [weak self] _, _ in
77 | guard let self = self else {
78 | return
79 | }
80 | self.invalidateIntrinsicContentSize()
81 | }
82 | }
83 |
84 | public required init?(coder: NSCoder) {
85 | super.init(coder: coder)
86 | addSubview(wrapped)
87 | observation = observe(\.wrapped.image, options: [.new]) { [weak self] _, _ in
88 | guard let self = self else {
89 | return
90 | }
91 | self.invalidateIntrinsicContentSize()
92 | }
93 | }
94 | }
95 |
96 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
97 | extension PlatformView {
98 | /// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview.
99 | /// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this.
100 | func bindFrameToSuperviewBounds() {
101 | guard let superview = self.superview else {
102 | print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.")
103 | return
104 | }
105 |
106 | self.translatesAutoresizingMaskIntoConstraints = false
107 | self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true
108 | self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true
109 | self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true
110 | self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true
111 | }
112 | }
113 |
114 | #endif
115 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo-tvOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import UIKit
10 | import SwiftUI
11 | import SDWebImage
12 | import SDWebImageWebPCoder
13 | import SDWebImageSVGCoder
14 | import SDWebImagePDFCoder
15 |
16 | @UIApplicationMain
17 | class AppDelegate: UIResponder, UIApplicationDelegate {
18 |
19 | var window: UIWindow?
20 | var settings = UserSettings()
21 |
22 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
23 |
24 | // Create the SwiftUI view that provides the window contents.
25 | let contentView = ContentView().environmentObject(settings)
26 |
27 | // Use a UIHostingController as window root view controller.
28 | let window = UIWindow(frame: UIScreen.main.bounds)
29 | let hostingController = UIHostingController(rootView: contentView)
30 | window.rootViewController = hostingController
31 | self.window = window
32 | window.makeKeyAndVisible()
33 |
34 | // Hack here because of SwiftUI's bug, when using `NavigationLink`, the focusable no longer works, so the `onExitCommand` does not get called
35 | let menuGesture = UITapGestureRecognizer(target: self, action: #selector(handleMenuGesture(_:)))
36 | menuGesture.allowedPressTypes = [NSNumber(value: UIPress.PressType.menu.rawValue)]
37 | hostingController.view.addGestureRecognizer(menuGesture)
38 |
39 | let playPauseGesture = UITapGestureRecognizer(target: self, action: #selector(handlePlayPauseGesture(_:)))
40 | playPauseGesture.allowedPressTypes = [NSNumber(value: UIPress.PressType.playPause.rawValue)]
41 | hostingController.view.addGestureRecognizer(playPauseGesture)
42 |
43 | // Add WebP/SVG/PDF support
44 | SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
45 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
46 | SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
47 | // Dynamic check to support vector format for both WebImage/AnimatedImage
48 | SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor { url, options, context in
49 | var options = options
50 | if let _ = context?[.animatedImageClass] as? SDAnimatedImage.Type {
51 | // AnimatedImage supports vector rendering, should not force decode
52 | options.insert(.avoidDecodeImage)
53 | }
54 | return SDWebImageOptionsResult(options: options, context: context)
55 | }
56 |
57 | return true
58 | }
59 |
60 | @objc func handleMenuGesture(_ gesture: UITapGestureRecognizer) {
61 | switch settings.editMode {
62 | case .inactive:
63 | settings.editMode = .active
64 | case .active:
65 | settings.editMode = .inactive
66 | case .transient:
67 | break
68 | @unknown default:
69 | break
70 | }
71 | }
72 |
73 | @objc func handlePlayPauseGesture(_ gesture: UITapGestureRecognizer) {
74 | settings.zoomed.toggle()
75 | }
76 |
77 | func applicationWillResignActive(_ application: UIApplication) {
78 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
79 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
80 | }
81 |
82 | func applicationDidEnterBackground(_ application: UIApplication) {
83 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
84 | }
85 |
86 | func applicationWillEnterForeground(_ application: UIApplication) {
87 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
88 | }
89 |
90 | func applicationDidBecomeActive(_ application: UIApplication) {
91 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
92 | }
93 |
94 |
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/DetailView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import SDWebImageSwiftUI
11 |
12 | // Placeholder when image load failed (with `.delayPlaceholder`)
13 | #if !os(watchOS)
14 | extension PlatformImage {
15 | static var wifiExclamationmark: PlatformImage {
16 | #if os(macOS)
17 | return PlatformImage(named: "wifi.exclamationmark")!
18 | #else
19 | return PlatformImage(systemName: "wifi.exclamationmark")!.withTintColor(.label, renderingMode: .alwaysOriginal)
20 | #endif
21 | }
22 | }
23 | #endif
24 |
25 | extension Image {
26 | static var wifiExclamationmark: Image {
27 | #if os(macOS)
28 | return Image("wifi.exclamationmark")
29 | .resizable()
30 | #else
31 | return Image(systemName: "wifi.exclamationmark")
32 | .resizable()
33 | #endif
34 | }
35 | }
36 |
37 | struct DetailView: View {
38 | let url: String
39 | @State var animated: Bool = true // You can change between WebImage/AnimatedImage
40 | @State var isAnimating: Bool = true
41 | @State var lastScale: CGFloat = 1.0
42 | @State var scale: CGFloat = 1.0
43 | @EnvironmentObject var settings: UserSettings
44 |
45 | var body: some View {
46 | VStack {
47 | #if os(iOS) || os(tvOS) || os(visionOS)
48 | zoomView()
49 | .navigationBarItems(trailing: Button(isAnimating ? "Stop" : "Start") {
50 | self.isAnimating.toggle()
51 | })
52 | #endif
53 | #if os(macOS) || os(watchOS)
54 | zoomView()
55 | .onTapGesture {
56 | self.isAnimating.toggle()
57 | }
58 | #endif
59 | }
60 | }
61 |
62 | func zoomView() -> some View {
63 | #if os(macOS) || os(iOS) || os(visionOS)
64 | return contentView()
65 | .scaleEffect(self.scale)
66 | .gesture(MagnificationGesture(minimumScaleDelta: 0.1).onChanged { value in
67 | let delta = value / self.lastScale
68 | self.lastScale = value
69 | let newScale = self.scale * delta
70 | self.scale = min(max(newScale, 0.5), 2)
71 | }.onEnded { value in
72 | self.lastScale = 1.0
73 | })
74 | #endif
75 | #if os(tvOS)
76 | return contentView()
77 | .scaleEffect(self.scale)
78 | .onReceive(self.settings.$zoomed) { zoomed in
79 | withAnimation {
80 | self.scale = zoomed ? 2 : 1
81 | }
82 | }
83 | #endif
84 | #if os(watchOS)
85 | return contentView()
86 | .scaleEffect(self.scale)
87 | .focusable(true)
88 | .digitalCrownRotation($scale, from: 0.5, through: 2, by: 0.1, sensitivity: .low, isHapticFeedbackEnabled: false)
89 | #endif
90 | }
91 |
92 | func contentView() -> some View {
93 | HStack {
94 | if animated {
95 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
96 | AnimatedImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating, placeholderImage: .wifiExclamationmark)
97 | .indicator(.progress)
98 | .resizable()
99 | .scaledToFit()
100 | #else
101 | WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
102 | image.resizable()
103 | .scaledToFit()
104 | } placeholder: {
105 | Image.wifiExclamationmark
106 | .resizable()
107 | .scaledToFit()
108 | }
109 | .indicator(.progress)
110 | #endif
111 | } else {
112 | WebImage(url: URL(string:url), options: [.progressiveLoad, .delayPlaceholder], isAnimating: $isAnimating) { image in
113 | image.resizable()
114 | .scaledToFit()
115 | } placeholder: {
116 | Image.wifiExclamationmark
117 | .resizable()
118 | .scaledToFit()
119 | }
120 | .indicator(.progress(style: .circular))
121 | }
122 | }
123 | }
124 | }
125 |
126 | #if DEBUG
127 | struct DetailView_Previews: PreviewProvider {
128 | static var previews: some View {
129 | DetailView(url: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic", animated: false)
130 | }
131 | }
132 | #endif
133 |
--------------------------------------------------------------------------------
/SDWebImageSwiftUI/Classes/ImageManager.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import Combine
11 | import SDWebImage
12 |
13 | /// A Image observable object for handle image load process. This drive the Source of Truth for image loading status.
14 | /// You can use `@ObservedObject` to associate each instance of manager to your View type, which update your view's body from SwiftUI framework when image was loaded.
15 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
16 | public final class ImageManager : ObservableObject {
17 | /// loaded image, note when progressive loading, this will published multiple times with different partial image
18 | public var image: PlatformImage? {
19 | didSet {
20 | DispatchQueue.main.async {
21 | self.objectWillChange.send()
22 | }
23 | }
24 | }
25 | /// loaded image data, may be nil if hit from memory cache. This will only published once even on incremental image loading
26 | public var imageData: Data? {
27 | didSet {
28 | DispatchQueue.main.async {
29 | self.objectWillChange.send()
30 | }
31 | }
32 | }
33 | /// loaded image cache type, .none means from network
34 | public var cacheType: SDImageCacheType = .none {
35 | didSet {
36 | DispatchQueue.main.async {
37 | self.objectWillChange.send()
38 | }
39 | }
40 | }
41 | /// loading error, you can grab the error code and reason listed in `SDWebImageErrorDomain`, to provide a user interface about the error reason
42 | public var error: Error? {
43 | didSet {
44 | DispatchQueue.main.async {
45 | self.objectWillChange.send()
46 | }
47 | }
48 | }
49 | /// true means during incremental loading
50 | public var isIncremental: Bool = false {
51 | didSet {
52 | DispatchQueue.main.async {
53 | self.objectWillChange.send()
54 | }
55 | }
56 | }
57 | /// A observed object to pass through the image manager loading status to indicator
58 | public var indicatorStatus = IndicatorStatus()
59 |
60 | weak var currentOperation: SDWebImageOperation? = nil
61 |
62 | var currentURL: URL?
63 | var transaction = Transaction()
64 | var successBlock: ((PlatformImage, Data?, SDImageCacheType) -> Void)?
65 | var failureBlock: ((Error) -> Void)?
66 | var progressBlock: ((Int, Int) -> Void)?
67 |
68 | public init() {}
69 |
70 | /// Start to load the url operation
71 | /// - Parameter url: The image url
72 | /// - Parameter options: The options to use when downloading the image. See `SDWebImageOptions` for the possible values.
73 | /// - Parameter context: A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
74 | public func load(url: URL?, options: SDWebImageOptions = [], context: [SDWebImageContextOption : Any]? = nil) {
75 | let manager: SDWebImageManager
76 | if let customManager = context?[.customManager] as? SDWebImageManager {
77 | manager = customManager
78 | } else {
79 | manager = .shared
80 | }
81 | if (currentOperation != nil && currentURL == url) {
82 | return
83 | }
84 | currentURL = url
85 | self.indicatorStatus.isLoading = true
86 | self.indicatorStatus.progress = 0
87 | currentOperation = manager.loadImage(with: url, options: options, context: context, progress: { [weak self] (receivedSize, expectedSize, _) in
88 | // This block may be called in non-main thread
89 | guard let self = self else {
90 | return
91 | }
92 | let progress: Double
93 | if (expectedSize > 0) {
94 | progress = Double(receivedSize) / Double(expectedSize)
95 | } else {
96 | progress = 0
97 | }
98 | self.indicatorStatus.progress = progress
99 | if let progressBlock = self.progressBlock {
100 | DispatchQueue.main.async {
101 | progressBlock(receivedSize, expectedSize)
102 | }
103 | }
104 | }) { [weak self] (image, data, error, cacheType, finished, _) in
105 | guard let self = self else {
106 | return
107 | }
108 | if let error = error as? SDWebImageError, error.code == .cancelled {
109 | // Ignore user cancelled
110 | // There are race condition when quick scroll
111 | // Indicator modifier disapper and trigger `WebImage.body`
112 | // So previous View struct call `onDisappear` and cancel the currentOperation
113 | return
114 | }
115 | withTransaction(self.transaction) {
116 | self.image = image
117 | self.error = error
118 | self.isIncremental = !finished
119 | if finished {
120 | self.imageData = data
121 | self.cacheType = cacheType
122 | self.indicatorStatus.isLoading = false
123 | self.indicatorStatus.progress = 1
124 | if let image = image {
125 | self.successBlock?(image, data, cacheType)
126 | } else {
127 | self.failureBlock?(error ?? NSError())
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | /// Cancel the current url loading
135 | public func cancel() {
136 | if let operation = currentOperation {
137 | operation.cancel()
138 | currentOperation = nil
139 | }
140 | indicatorStatus.isLoading = false
141 | currentURL = nil
142 | }
143 |
144 | }
145 |
146 | // Completion Handler
147 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
148 | extension ImageManager {
149 | /// Provide the action when image load fails.
150 | /// - Parameters:
151 | /// - action: The action to perform. The first arg is the error during loading. If `action` is `nil`, the call has no effect.
152 | public func setOnFailure(perform action: ((Error) -> Void)? = nil) {
153 | self.failureBlock = action
154 | }
155 |
156 | /// Provide the action when image load successes.
157 | /// - Parameters:
158 | /// - action: The action to perform. The first arg is the loaded image, the second arg is the loaded image data, the third arg is the cache type loaded from. If `action` is `nil`, the call has no effect.
159 | public func setOnSuccess(perform action: ((PlatformImage, Data?, SDImageCacheType) -> Void)? = nil) {
160 | self.successBlock = action
161 | }
162 |
163 | /// Provide the action when image load progress changes.
164 | /// - Parameters:
165 | /// - action: The action to perform. The first arg is the received size, the second arg is the total size, all in bytes. If `action` is `nil`, the call has no effect.
166 | public func setOnProgress(perform action: ((Int, Int) -> Void)? = nil) {
167 | self.progressBlock = action
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: "SDWebImageSwiftUI CI"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - '*'
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | Pods:
16 | name: Cocoapods Lint
17 | runs-on: macos-14
18 | env:
19 | DEVELOPER_DIR: /Applications/Xcode_15.2.app
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v3
23 |
24 | - name: Install Cocoapods
25 | run: gem install cocoapods --no-document --quiet
26 |
27 | - name: Install Xcpretty
28 | run: gem install xcpretty --no-document --quiet
29 |
30 | - name: Pod Update
31 | run: pod repo update --silent
32 |
33 | - name: Pod Install
34 | run: pod install
35 |
36 | - name: Run SDWebImageSwiftUI podspec lint
37 | run: |
38 | set -o pipefail
39 | pod lib lint SDWebImageSwiftUI.podspec --allow-warnings --skip-tests
40 |
41 | Demo:
42 | name: Run Demo
43 | runs-on: macos-14
44 | env:
45 | DEVELOPER_DIR: /Applications/Xcode_15.2.app
46 | WORKSPACE_NAME: SDWebImageSwiftUI.xcworkspace
47 | OSXSCHEME: SDWebImageSwiftUIDemo-macOS
48 | iOSSCHEME: SDWebImageSwiftUIDemo
49 | TVSCHEME: SDWebImageSwiftUIDemo-tvOS
50 | WATCHSCHEME: SDWebImageSwiftUIDemo-watchOS WatchKit App
51 | iosDestination: platform=iOS Simulator,name=iPhone 15 Pro
52 | macOSDestination: platform=macOS,arch=x86_64
53 | macCatalystDestination: platform=macOS,arch=x86_64,variant=Mac Catalyst
54 | tvOSDestination: platform=tvOS Simulator,name=Apple TV 4K (3rd generation)
55 | watchOSDestination: platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)
56 | visionOSDestination: platform=visionOS Simulator,name=Apple Vision Pro
57 | steps:
58 | - name: Checkout
59 | uses: actions/checkout@v3
60 |
61 | - name: Clean DerivedData
62 | run: |
63 | rm -rf ~/Library/Developer/Xcode/DerivedData/
64 | mkdir DerivedData
65 |
66 | - name: Install Cocoapods
67 | run: gem install cocoapods --no-document --quiet
68 |
69 | - name: Install Xcpretty
70 | run: gem install xcpretty --no-document --quiet
71 |
72 | - name: Pod Update
73 | run: pod repo update --silent
74 |
75 | - name: Pod Install
76 | run: pod install
77 |
78 | - name: Run demo for OSX
79 | run: |
80 | set -o pipefail
81 | xcodebuild build -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ env.OSXSCHEME }}" -destination "${{ env.macOSDestination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO | xcpretty -c
82 |
83 | - name: Run demo for iOS
84 | run: |
85 | set -o pipefail
86 | xcodebuild build -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ env.iOSSCHEME }}" -destination "${{ env.iosDestination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO | xcpretty -c
87 |
88 | - name: Run demo for TV
89 | run: |
90 | set -o pipefail
91 | xcodebuild build -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ env.TVSCHEME }}" -destination "${{ env.tvOSDestination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO | xcpretty -c
92 |
93 | - name: Run demo for Watch
94 | run: |
95 | set -o pipefail
96 | xcodebuild build -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ env.WATCHSCHEME }}" -destination "${{ env.watchOSDestination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO | xcpretty -c
97 |
98 | Test:
99 | name: Unit Test
100 | runs-on: macos-14
101 | env:
102 | DEVELOPER_DIR: /Applications/Xcode_15.2.app
103 | WORKSPACE_NAME: SDWebImageSwiftUI.xcworkspace
104 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
105 | # use matrix to generate jobs for each platform
106 | strategy:
107 | fail-fast: false
108 | matrix:
109 | platform: [iOS, macOS, tvOS]
110 | include:
111 | - platform: iOS
112 | destination: platform=iOS Simulator,name=iPhone 15 Pro
113 | scheme: SDWebImageSwiftUITests
114 | flag: ios
115 | - platform: macOS
116 | destination: platform=macOS,arch=x86_64
117 | scheme: SDWebImageSwiftUITests macOS
118 | flag: macos
119 | - platform: tvOS
120 | destination: platform=tvOS Simulator,name=Apple TV 4K (3rd generation)
121 | scheme: SDWebImageSwiftUITests tvOS
122 | flag: tvos
123 | # - platform: visionOS
124 | # destination: platform=visionOS Simulator,name=Apple Vision Pro
125 | # scheme: Vision
126 | # flag: visionos
127 | steps:
128 | - name: Checkout
129 | uses: actions/checkout@v3
130 | with:
131 | fetch-depth: 0
132 |
133 | - name: Install Cocoapods
134 | run: gem install cocoapods --no-document --quiet
135 |
136 | - name: Install Xcpretty
137 | run: gem install xcpretty --no-document --quiet
138 |
139 | - name: Pod Update
140 | run: pod repo update --silent
141 |
142 | - name: Pod Install
143 | run: pod install
144 |
145 | - name: Clean DerivedData
146 | run: |
147 | rm -rf ~/Library/Developer/Xcode/DerivedData/
148 | mkdir DerivedData
149 |
150 | - name: Run test
151 | run: |
152 | set -o pipefail
153 | xcodebuild build-for-testing -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO | xcpretty -c
154 | xcodebuild test-without-building -workspace "${{ env.WORKSPACE_NAME }}" -scheme "${{ matrix.scheme }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_ALLOWED=NO
155 | mv ~/Library/Developer/Xcode/DerivedData/ "./DerivedData/${{ matrix.platform }}"
156 |
157 | - name: Code Coverage
158 | run: |
159 | set -o pipefail
160 | export PATH="/usr/local/opt/curl/bin:$PATH"
161 | curl --version
162 | bash <(curl -s https://codecov.io/bash) -v -D "./DerivedData/${{ matrix.platform }}" -J '^SDWebImageSwiftUI$' -c -X gcov -F "${{ matrix.flag }}"
163 |
164 | Build:
165 | name: Build Library
166 | runs-on: macos-14
167 | env:
168 | DEVELOPER_DIR: /Applications/Xcode_15.2.app
169 | PROJECT_NAME: SDWebImageSwiftUI.xcodeproj
170 | OSXSCHEME: SDWebImageSwiftUI macOS
171 | iOSSCHEME: SDWebImageSwiftUI
172 | TVSCHEME: SDWebImageSwiftUI tvOS
173 | WATCHSCHEME: SDWebImageSwiftUI watchOS
174 | steps:
175 | - name: Checkout
176 | uses: actions/checkout@v3
177 |
178 | - name: Build the SwiftPM
179 | run: |
180 | set -o pipefail
181 | swift build
182 | rm -rf ~/.build
183 |
184 | - name: Install Carthage
185 | run: brew install carthage
186 |
187 | - name: Carthage Update
188 | run: ./carthage.sh update --platform "iOS, tvOS, macOS, watchOS"
189 |
190 | - name: Build as dynamic frameworks
191 | run: |
192 | set -o pipefail
193 | xcodebuild build -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.OSXSCHEME }}" -sdk macosx -configuration Release | xcpretty -c
194 | xcodebuild build -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.iOSSCHEME }}" -sdk iphoneos -configuration Release | xcpretty -c
195 | xcodebuild build -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.TVSCHEME }}" -sdk appletvos -configuration Release | xcpretty -c
196 | xcodebuild build -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.WATCHSCHEME }}" -sdk watchos -configuration Release | xcpretty -c
197 |
--------------------------------------------------------------------------------
/Tests/WebImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftUI
3 | import ViewInspector
4 | @testable import SDWebImageSwiftUI
5 |
6 | extension WebImage : Inspectable {}
7 |
8 | class WebImageTests: XCTestCase {
9 |
10 | override func setUp() {
11 | super.setUp()
12 | // Put setup code here. This method is called before the invocation of each test method in the class.
13 | }
14 |
15 | override func tearDown() {
16 | // Put teardown code here. This method is called after the invocation of each test method in the class.
17 | super.tearDown()
18 | }
19 |
20 | func testWebImageWithStaticURL() throws {
21 | let expectation = self.expectation(description: "WebImage static url initializer")
22 | let imageUrl = URL(string: "https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png")
23 | let imageView = WebImage(url: imageUrl)
24 | let introspectView = imageView.onSuccess { image, data, cacheType in
25 | #if os(macOS)
26 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().nsImage()
27 | #else
28 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().cgImage()
29 | #endif
30 | XCTAssertNotNil(displayImage)
31 | expectation.fulfill()
32 | }.onFailure { error in
33 | XCTFail(error.localizedDescription)
34 | }
35 | _ = try introspectView.inspect()
36 | ViewHosting.host(view: introspectView)
37 | self.waitForExpectations(timeout: 5, handler: nil)
38 | ViewHosting.expel()
39 | }
40 |
41 | func testWebImageWithAnimatedURL() throws {
42 | let expectation = self.expectation(description: "WebImage animated url initializer")
43 | let imageUrl = URL(string: "https://apng.onevcat.com/assets/elephant.png")
44 | let binding = Binding(wrappedValue: true)
45 | let imageView = WebImage(url: imageUrl, isAnimating: binding)
46 | let introspectView = imageView.onSuccess { image, data, cacheType in
47 | if let animatedImage = image as? SDAnimatedImage {
48 | XCTAssertTrue(imageView.isAnimating)
49 | #if os(macOS)
50 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().nsImage()
51 | let size = displayImage?.size
52 | #else
53 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().cgImage()
54 | let size = CGSize(width: displayImage?.width ?? 0, height: displayImage?.height ?? 0)
55 | #endif
56 | XCTAssertNotNil(displayImage)
57 | // Check display image should match the animated poster frame
58 | let posterImage = animatedImage.animatedImageFrame(at: 0)
59 | XCTAssertEqual(size, posterImage?.size)
60 | expectation.fulfill()
61 | } else {
62 | XCTFail("WebImage animated image invalid")
63 | }
64 | }.onFailure { error in
65 | XCTFail(error.localizedDescription)
66 | }
67 | _ = try introspectView.inspect()
68 | ViewHosting.host(view: introspectView)
69 | self.waitForExpectations(timeout: 5, handler: nil)
70 | ViewHosting.expel()
71 | }
72 |
73 | func testWebImageModifier() throws {
74 | let expectation = self.expectation(description: "WebImage modifier")
75 | let imageUrl = URL(string: "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg")
76 | let imageView = WebImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) { image in
77 | image.resizable()
78 | } placeholder: {
79 | Circle()
80 | }
81 | let introspectView = imageView
82 | .onSuccess { _, _, _ in
83 | expectation.fulfill()
84 | }
85 | .onFailure { _ in
86 | XCTFail()
87 | }
88 | .onProgress { _, _ in
89 |
90 | }
91 | // Image
92 | .resizable()
93 | .renderingMode(.original)
94 | .interpolation(.high)
95 | .antialiased(true)
96 | // Animation
97 | .runLoopMode(.common)
98 | .customLoopCount(1)
99 | .maxBufferSize(0)
100 | .pausable(true)
101 | .purgeable(true)
102 | .playbackRate(1)
103 | // WebImage
104 | .retryOnAppear(true)
105 | .cancelOnDisappear(true)
106 | .indicator(.activity)
107 | .transition(.fade)
108 | .animation(.easeInOut)
109 | _ = try introspectView.inspect()
110 | ViewHosting.host(view: introspectView)
111 | self.waitForExpectations(timeout: 5, handler: nil)
112 | ViewHosting.expel()
113 | }
114 |
115 | func testWebImageOnSuccessWhenMemoryCacheHit() throws {
116 | let expectation = self.expectation(description: "WebImage onSuccess when memory cache hit")
117 | let imageUrl = URL(string: "https://foo.bar/buzz.png")
118 | let cacheKey = SDWebImageManager.shared.cacheKey(for: imageUrl)
119 | #if os(macOS)
120 | let testImage = TestUtils.testImageBundle().image(forResource: "TestImage")
121 | #else
122 | let testImage = UIImage(named: "TestImage", in: TestUtils.testImageBundle(), compatibleWith: nil)
123 | #endif
124 | SDImageCache.shared.storeImage(toMemory: testImage, forKey: cacheKey)
125 | let imageView = WebImage(url: imageUrl)
126 | let introspectView = imageView.onSuccess { image, data, cacheType in
127 | XCTAssert(cacheType == .memory)
128 | XCTAssertNotNil(image)
129 | XCTAssertEqual(image, testImage)
130 | expectation.fulfill()
131 | }
132 | _ = try introspectView.inspect()
133 | ViewHosting.host(view: introspectView)
134 | self.waitForExpectations(timeout: 5, handler: nil)
135 | ViewHosting.expel()
136 | }
137 |
138 | func testWebImageOnSuccessWhenCacheMiss() throws {
139 | let expectation = self.expectation(description: "WebImage onSuccess when cache miss")
140 | let imageUrl = URL(string: "https://placehold.co/100x100.png")
141 | let cacheKey = SDWebImageManager.shared.cacheKey(for: imageUrl)
142 | SDImageCache.shared.removeImageFromMemory(forKey: cacheKey)
143 | SDImageCache.shared.removeImageFromDisk(forKey: cacheKey)
144 | let imageView = WebImage(url: imageUrl)
145 | let introspectView = imageView.onSuccess { image, data, cacheType in
146 | XCTAssert(cacheType == .none)
147 | XCTAssertNotNil(image)
148 | XCTAssertNotNil(data)
149 | expectation.fulfill()
150 | }
151 | _ = try introspectView.inspect()
152 | ViewHosting.host(view: introspectView)
153 | self.waitForExpectations(timeout: 5, handler: nil)
154 | ViewHosting.expel()
155 | }
156 |
157 | func testWebImageEXIFImage() throws {
158 | let expectation = self.expectation(description: "WebImage EXIF image url")
159 | // EXIF 5, Left Mirrored
160 | let imageUrl = URL(string: "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg")
161 | let imageView = WebImage(url: imageUrl)
162 | let introspectView = imageView.onSuccess { image, data, cacheType in
163 | #if os(macOS)
164 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().nsImage()
165 | XCTAssertNotNil(displayImage)
166 | #else
167 | let displayImage = try? imageView.inspect().zStack().image(0).actualImage().cgImage()
168 | let orientation = try? imageView.inspect().zStack().image(0).actualImage().orientation()
169 | XCTAssertNotNil(displayImage)
170 | XCTAssertEqual(orientation, .leftMirrored)
171 | #endif
172 | expectation.fulfill()
173 | }.onFailure { error in
174 | XCTFail(error.localizedDescription)
175 | }
176 | _ = try introspectView.inspect()
177 | ViewHosting.host(view: introspectView)
178 | self.waitForExpectations(timeout: 5, handler: nil)
179 | ViewHosting.expel()
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/Tests/AnimatedImageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftUI
3 | import ViewInspector
4 | @testable import SDWebImageSwiftUI
5 |
6 | extension AnimatedImage : Inspectable {}
7 |
8 | extension AnimatedImage {
9 | struct WrapperView: View & Inspectable {
10 | var name: String
11 | var bundle: Bundle?
12 | @State var isAnimating: Bool
13 |
14 | var onViewUpdate: (Self, PlatformView, AnimatedImage.Context) -> Void
15 |
16 | var body: some View {
17 | AnimatedImage(name: name, bundle: bundle, isAnimating: $isAnimating)
18 | .onViewUpdate { view, context in
19 | self.onViewUpdate(self, view, context)
20 | }
21 | }
22 | }
23 | }
24 |
25 | class AnimatedImageTests: XCTestCase {
26 |
27 | override func setUp() {
28 | super.setUp()
29 | // Put setup code here. This method is called before the invocation of each test method in the class.
30 | }
31 |
32 | override func tearDown() {
33 | // Put teardown code here. This method is called after the invocation of each test method in the class.
34 | super.tearDown()
35 | }
36 |
37 | func testAnimatedImageWithName() throws {
38 | let expectation = self.expectation(description: "AnimatedImage name initializer")
39 | let imageName = "TestImage.gif"
40 | let imageView = AnimatedImage(name: imageName, bundle: TestUtils.testImageBundle())
41 | ViewHosting.host(view: imageView)
42 | let animatedImageView = try imageView.inspect().actualView().platformView().wrapped
43 | if let animatedImage = animatedImageView.image as? SDAnimatedImage {
44 | XCTAssertEqual(animatedImage.animatedImageLoopCount, 0)
45 | XCTAssertEqual(animatedImage.animatedImageFrameCount, 5)
46 | } else {
47 | XCTFail("SDAnimatedImageView.image invalid")
48 | }
49 | expectation.fulfill()
50 | self.waitForExpectations(timeout: 5, handler: nil)
51 | ViewHosting.expel()
52 | }
53 |
54 | func testAnimatedImageWithData() throws {
55 | let expectation = self.expectation(description: "AnimatedImage data initializer")
56 | let imageData = try XCTUnwrap(TestUtils.testImageData(name: "TestLoopCount.gif"))
57 | let imageView = AnimatedImage(data: imageData)
58 | ViewHosting.host(view: imageView)
59 | let animatedImageView = try imageView.inspect().actualView().platformView().wrapped
60 | if let animatedImage = animatedImageView.image as? SDAnimatedImage {
61 | XCTAssertEqual(animatedImage.animatedImageLoopCount, 1)
62 | XCTAssertEqual(animatedImage.animatedImageFrameCount, 2)
63 | } else {
64 | XCTFail("SDAnimatedImageView.image invalid")
65 | }
66 | expectation.fulfill()
67 | self.waitForExpectations(timeout: 5, handler: nil)
68 | ViewHosting.expel()
69 | }
70 |
71 | func testAnimatedImageWithURL() throws {
72 | let expectation = self.expectation(description: "AnimatedImage url initializer")
73 | let imageUrl = URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif")
74 | let imageView = AnimatedImage(url: imageUrl)
75 | .onSuccess { image, data, cacheType in
76 | XCTAssertNotNil(image)
77 | if let animatedImage = image as? SDAnimatedImage {
78 | XCTAssertEqual(animatedImage.animatedImageLoopCount, 0)
79 | XCTAssertEqual(animatedImage.animatedImageFrameCount, 389)
80 | } else {
81 | XCTFail("SDAnimatedImageView.image invalid")
82 | }
83 | expectation.fulfill()
84 | }.onFailure { error in
85 | XCTFail(error.localizedDescription)
86 | }
87 | ViewHosting.host(view: imageView)
88 | let animatedImageView = try imageView.inspect().actualView().platformView().wrapped
89 | XCTAssertEqual(animatedImageView.sd_imageURL, imageUrl)
90 | self.waitForExpectations(timeout: 10, handler: nil)
91 | ViewHosting.expel()
92 | }
93 |
94 | func testAnimatedImageBinding() throws {
95 | let expectation = self.expectation(description: "AnimatedImage binding control")
96 | var isStopped = false
97 | // Use wrapper to make the @Binding works
98 | let wrapperView = AnimatedImage.WrapperView(name: "TestImageAnimated.apng", bundle: TestUtils.testImageBundle(), isAnimating: true) { wrapperView, view, context in
99 | guard let animatedImageView = view as? SDAnimatedImageView else {
100 | XCTFail("AnimatedImage's view should be SDAnimatedImageView")
101 | return
102 | }
103 | if let animatedImage = animatedImageView.image as? SDAnimatedImage {
104 | XCTAssertEqual(animatedImage.animatedImageLoopCount, 0)
105 | XCTAssertEqual(animatedImage.animatedImageFrameCount, 101)
106 | } else {
107 | XCTFail("AnimatedImage's image should be SDAnimatedImage")
108 | }
109 | // Wait 1 second for SwiftUI's own `updateUIView` callback finished.
110 | // It's suck that the actual callback behavior is different on different iOS version or Simulator version, so I can assume which is the last callback using the callback count.
111 | if !isStopped {
112 | // # SwiftUI's own updateUIView call
113 | // Ignore in Travis-CI because of macOS 10.14's bug behavior on iPhone Simulator
114 | // #if os(iOS) || os(tvOS)
115 | // XCTAssertTrue(animatedImageView.isAnimating)
116 | // #else
117 | // XCTAssertTrue(animatedImageView.animates)
118 | // #endif
119 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
120 | if !isStopped {
121 | isStopped = true
122 | wrapperView.isAnimating = false
123 | } else {
124 | // Extra `updateUIView` from SwiftUI, ignore
125 | }
126 | }
127 | } else {
128 | // # AnimatedImage's isAnimating @Binding trigger update (from above)
129 | #if os(iOS) || os(tvOS)
130 | XCTAssertFalse(animatedImageView.isAnimating)
131 | #else
132 | XCTAssertFalse(animatedImageView.animates)
133 | #endif
134 | expectation.fulfill()
135 | }
136 | }
137 | ViewHosting.host(view: wrapperView)
138 | self.waitForExpectations(timeout: 5, handler: nil)
139 | ViewHosting.expel()
140 | }
141 |
142 | func testAnimatedImageModifier() throws {
143 | let expectation = self.expectation(description: "WebImage modifier")
144 | let imageUrl = URL(string: "https://assets.sbnation.com/assets/2512203/dogflops.gif")
145 | let imageView = AnimatedImage(url: imageUrl, options: [.progressiveLoad], context: [.imageScaleFactor: 1]) {
146 | Circle()
147 | }
148 | let introspectView = imageView
149 | .onSuccess { _, _, _ in
150 | expectation.fulfill()
151 | }
152 | .onFailure { _ in
153 | XCTFail()
154 | }
155 | .onProgress { _, _ in
156 |
157 | }
158 | .onViewCreate { view, context in
159 | XCTAssert(view.isKind(of: SDAnimatedImageView.self))
160 | context.coordinator.userInfo = ["foo" : "bar"]
161 | }
162 | .onViewUpdate { view, context in
163 | XCTAssert(view.isKind(of: SDAnimatedImageView.self))
164 | XCTAssertEqual(context.coordinator.userInfo?["foo"] as? String, "bar")
165 | }
166 | .indicator(.activity)
167 | // Image
168 | .resizable()
169 | .renderingMode(.original)
170 | .interpolation(.high)
171 | .antialiased(true)
172 | // Animation
173 | .runLoopMode(.common)
174 | .customLoopCount(1)
175 | .maxBufferSize(0)
176 | .pausable(true)
177 | .purgeable(true)
178 | .playbackRate(1)
179 | .transition(.fade)
180 | .animation(.easeInOut)
181 | _ = try introspectView.inspect()
182 | ViewHosting.host(view: introspectView)
183 | self.waitForExpectations(timeout: 10, handler: nil)
184 | ViewHosting.expel()
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 |
9 | ## [3.1.4] - 2025-11-03
10 | - Xcode 26 Compliant (Update SDWebImage to 5.21.1) #354
11 |
12 | ## [3.1.3] - 2024-11-06
13 | - Fixed old version compiler does not support automatic self capture in Xcode 14.2 and Swift 5.7.2 #340
14 | - Fix the data race because progress block is called in non-main queue #341
15 |
16 | ## [3.1.2] - 2024-08-29
17 | - Allows easy to use WebImage with isAnimating default to false and change to true later #333
18 | - Note: This changes WebImage's internal loaded image from `UIImage/NSImage` to `SDAnimatedImage`, which is compatible for `UIImageView/NSImageView`
19 |
20 | ## [3.1.1] - 2024-07-01
21 | - Fix the transition visual jump between placeholderImage and final image for AnimatedImage #326
22 |
23 | ## [3.1.0] - 2024-06-27
24 | - Re-implements the aspectRatio support on AnimatedImage, fix issue like cornerRadius #324
25 | - Add Image scale support in WebImage init #323
26 | - Update platform names in `available` attributes #321
27 | - - This is source compatible but binary incompatible version
28 |
29 | ## [3.0.4] - 2024-04-30
30 | - Trying to move the initial state setup before onAppear to fix the watchOS switching url or any other state issue #316
31 | - This solve a issue in history when sometimes SwiftUI does not trigger the `onAppear` and cause state error, like #312 #314
32 |
33 | ## [3.0.3] - 2024-04-29
34 | - Added totally empty privacy manifest #315
35 | - People who facing the issue because of Privacy Manifest declaration during ITC validation can try this version
36 |
37 | ## [3.0.2] - 2024-03-27
38 | - Fix the assert crash then when using Data/Name in AnimatedImage #309
39 |
40 | ## [3.0.1] - 2024-03-18
41 | - Fix the issue for WebImage/AnimatedImage when url is nil will not cause the reloading #304
42 |
43 | ## [3.0.0] - 2024-03-09
44 | - This is the first release for 3.x version. Bump the min deplouyment from SwiftUI 1.0 to 2.0 (means iOS 14/macOS 11/tvOS 14/watchOS 7/visionOS 1)
45 | - Fix AnimatedImage aspectRatio issue when ratio is nil #301
46 | - Upgrade to support visionOS on CocoaPods #298
47 |
48 | ## [3.0.0-beta.3] - 2023-12-04
49 |
50 | ### Changed
51 | - Update the AnimatedImage API to expose the SDAnimatedImageView #285
52 | - Fix the AnimatedImgae rendering mode about compatible with SDWebImage 5.18+
53 |
54 | ## [3.0.0-beta.2] - 2023-10-21
55 |
56 | ### Changed
57 | - Update the WebImage API to match SwiftUI.AsyncImage #275 @Kyle-Ye
58 | - Allows to use UIImage/NSImage as defaults when init the AnimatedImage with JPEG data #277
59 |
60 | ### Removed
61 | - `WebImage.placeholder(@ViewBuilder content: () -> T) -> WebImage`
62 | - `WebImage.placeholder(_ image: Image) -> WebImage`
63 | - `AnimatedImage.placeholder(@ViewBuilder content: () -> T) -> AnimatedImage`
64 | - `AnimatedImage.placeholder(_ image: PlatformImage) -> AnimatedImage`
65 |
66 | ## [3.0.0-beta] - 2023-09-02
67 |
68 | ### Added
69 | - (Part 1) Support compile for visionOS (no package manager support) #267
70 |
71 | ### Changed
72 |
73 | - Drop iOS 13/macOS 10.15/tvOS 13/watchOS 6 support #250
74 | - ProgressIndicator and ActivityIndicator is removed. Use `ProgressView` instead
75 | - Availability is changed to iOS 14/macOS 11/tvOS 11/watchOS 7
76 | - Embed `SwiftUIBackports` dependency is removed.
77 |
78 | ## [2.2.3] - 2023-04-32
79 | - Fix the issue that Static Library + Library Evolution cause the build issue on Swift 5.8 #263
80 |
81 | ## [2.2.2] - 2022-12-27
82 |
83 | ### Fixed
84 | - Fix the bug that isAnimating control does not works on WebImage #251
85 | - Note you should upgrade the SDWebImage 5.14.3+, or this may cause extra Xcode 14's runtime warning (function is unaffected)
86 |
87 | ## [2.2.1] - 2022-09-23
88 |
89 | ### Fixed
90 |
91 | - Fix the nil url always returns Error will cause infinity onAppear call and image manager to load, which waste CPU #235
92 | - Fix the case which sometimes the player does not stop when WebImage it out of screen #236
93 | - Al v2.2.0 users are recommended to update
94 |
95 | ## [2.2.0] - 2022-09-22
96 |
97 | ### Fixed
98 |
99 | - Fix iOS 13 compatibility #232
100 | - Fix WebImage/Animated using @State to publish changes
101 | - Al v2.1.0 users are recommended to update
102 |
103 | ### Changed
104 | - ImageManager API changes. The init method has no args, use `load(url:options:context:)` instead
105 |
106 | ## [2.1.0] - 2022-09-15
107 |
108 | ### Fixed
109 |
110 | - Refactor WebImage/AnimatedImage using SwiftUIBackports and StateObject #227
111 | - Fix iOS 16 undefined behavior warnings because of Publishing changes from within view updates.
112 | - Fix iOS 14+ WebImage behavior using `@StateObject` (and backport on iOS 13)
113 |
114 | ### Changed
115 |
116 | - The `IndicatorReportable` is misused and removed. Use `IndicatorStatus` instead.
117 | - Deprecate iOS 13 support, this may be the last version to support iOS 13.
118 |
119 | ## [2.0.2] - 2021-03-10
120 |
121 | ### Fixed
122 | - Fix the issue that using `Image(uiImage:)` will result wrong rendering mode in some component like `TabBarItem`, while using `Image(decorative:scale:orientation:)` works well #177
123 |
124 | ### Changed
125 | - Remove the WebImage placeholder maxWidth/maxHeight modifier, this may break some use case like `TabView`. If user want to use placeholder, limit themselves #178 #175
126 |
127 | ## [2.0.1] - 2021-02-25
128 | ### Fixed
129 | - Fix the rare cases that WebImage will lost animation when visibility changes. #171
130 |
131 | ## [2.0.0] - 2021-02-23
132 | ### Added
133 | - Update with the playbackMode support for `WebImage` and `AnimatedImage` #168
134 | - Update watchOS demo to watchOS 7, remove the custom indicator sample and use `ProgressView` instead #166
135 | - Update the Example to make WebImage animatable by default #160
136 |
137 | ### Fixed
138 | - Fix the issue sometime the `WebImage` appear/disappear logic wrong. Using UIKit/AppKit to detect the visibility #164
139 | - Fix the leak of WebImage with animation and NavigationLink. #163
140 | - Try to fix the recursive updateView when using AnimatedImage inside `ScrollView/LazyVStack`. Which cause App freeze #162
141 | - Remove the fix for EXIF image in WebImage, which is fixed by Apple in iOS 14 #159
142 |
143 | ### Changed
144 | - Bump the limit to Xcode 12, because we need new iOS 14+ APIs check #167
145 | - Update the WebImage to defaults animatable #165
146 |
147 | ### Removed
148 | - Remove the wrong design onSuccess API. Using the full params one instead #169
149 |
150 | ## [1.5.0] - 2020-06-01
151 | ### Added
152 | - Add the convenient API support to use SwiftUI transition with ease-in-out duration #116
153 | - Update the Travis-CI to use Catalina and enable macOS test case #98
154 |
155 | ## [1.4.0] - 2020-05-07
156 | ### Added
157 | - Add the same overload method for onSuccess API, which introduce the image data arg. Keep the source code compatibility #109
158 | - Add the support for image data observable on ImageManager #107
159 |
160 | ## [1.3.4] - 2020-04-30
161 | ### Fixed
162 | - Revert the changes to prefetch the image url from memory cache #106
163 |
164 | ## [1.3.3] - 2020-04-15
165 | ### Fixed
166 | - Try to solve the SwiftUI bug of rendering EXIF UIImage in WebImage, as well as vector images #102
167 | - Now `WebImage` will render the vector images as bitmap version even if you don't provide `.thumbnailPixelSize`. To render real vector images, use `AnimatedImage` instead.
168 |
169 | ## [1.3.2] - 2020-04-14
170 | ### Added
171 | - Automatically import SDWebImage when user write import SDWebImageSwiftUI #100
172 |
173 | ## [1.3.1] - 2020-04-10
174 | ### Fixed
175 | - Fix Carthage support. Do not embed SDWebImage.framework in SDWebImageSwiftUI.framework #97. Thanks @jonkan
176 |
177 | ## [1.3.0] - 2020-04-05
178 | ### Added
179 | - Supports the `placeholder` View Builder API for `AnimatedImage` #94
180 |
181 | ### Changed
182 | - Upgrade the dependency of SDWebImage 5.7.0 #93
183 |
184 | ## [1.2.1] - 2020-04-01
185 | ### Fixed
186 | - Fix the issue when using `WebImage` with some transition like scaleEffect, each time the new state update will cause unused image fetching #92
187 |
188 | ## [1.2.0] - 2020-03-29
189 | ### Added
190 | - Supports the `delayPlaceholder` for WebImage #91
191 | - `AnimatedImage` little patch - UIKit/AppKit animated image now applied for `resizingMode` #89
192 |
193 | ### Fixed
194 | - Fix the issue when dealloc `AnimatedImage`'s native View, the window does not exist and cause Crash #90
195 |
196 | ## [1.1.0] - 2020-03-24
197 | ### Added
198 | - `ImageManager` now public. Which allows advanced usage for custom View type. Use `@ObservedObject` to bind the manager with your own View and update the image.
199 |
200 | ## [1.0.0] - 2020-03-03
201 | ### Added
202 | - `WebImage` now supports animation, use `isAnimating` binding value on init methods.
203 | - `WebImage` now supports the detailed animation control options, like `customLoopCount`, `pausable`, `purgeable`, `playbackRate`.
204 | - `AnimatedImage` now supports the indicator with `ViewModifier` as `WebImage`.
205 | - `IndicatorViewModifier` now public.
206 | - `IndicatorReportable` now public.
207 |
208 | ### Changed
209 | - Indicator's `progress` type now changed from `CGFloat` to `Double`.
210 | - `WebImage.aniamted(_:)` now becomes the `WebImage.init(url:options:context:isAnimating:)` Binding arg, you can use the Binding to control animations as well.
211 | - `AnimatedImage.playBackRate` now becomes `AnimatedImage.playbackRate`
212 | - `AnimatedImage.customLoopCount` now is `UInt` instead of `Int`.
213 | - `AnimatedImage.resizable` modifier now matches the SwiftUI behavior, you must call it or the size will be fixed to image pixel size.
214 |
215 | ### Removed
216 | - Removed all the description about 0.x version behavior in README.md.
217 |
--------------------------------------------------------------------------------
/Example/SDWebImageSwiftUIDemo/ContentView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of the SDWebImage package.
3 | * (c) DreamPiggy
4 | *
5 | * For the full copyright and license information, please view the LICENSE
6 | * file that was distributed with this source code.
7 | */
8 |
9 | import SwiftUI
10 | import SDWebImageSwiftUI
11 |
12 | class UserSettings: ObservableObject {
13 | // Some environment configuration
14 | #if os(tvOS)
15 | @Published var editMode: EditMode = .inactive
16 | @Published var zoomed: Bool = false
17 | #endif
18 | }
19 |
20 | struct ContentView5: View {
21 | let url: URL = URL(string: "http://assets.sbnation.com/assets/2512203/dogflops.gif")!
22 |
23 | @State private var isAnimating = false
24 |
25 | var body: some View {
26 | ZStack {
27 | WebImage(url: url, isAnimating: $isAnimating)
28 | .pausable(false)
29 | Button {
30 | isAnimating.toggle()
31 | } label: {
32 | Text(isAnimating ? "Stop" : "Start")
33 | }
34 | }
35 | }
36 | }
37 |
38 | #if !os(watchOS)
39 | struct ContentView4: View {
40 | var url = URL(string: "https://github.com/SDWebImage/SDWebImageSwiftUI/assets/97430818/72d27f90-e9d8-48d7-b144-82ada828a027")!
41 | var body: some View {
42 | AnimatedImage(url: url)
43 | .resizable()
44 | .scaledToFit()
45 | // .aspectRatio(nil, contentMode: .fit)
46 | .clipShape(RoundedRectangle(cornerRadius: 50, style: .continuous))
47 | }
48 | }
49 | #endif
50 |
51 | // Test Switching nil url
52 | struct ContentView3: View {
53 | @State var isOn = false
54 | @State var animated: Bool = false // You can change between WebImage/AnimatedImage
55 |
56 | var url: URL? {
57 | if isOn {
58 | .init(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1024px-Google_%22G%22_logo.svg.png")
59 | } else {
60 | nil
61 | }
62 | }
63 |
64 | var body: some View {
65 | VStack {
66 | Text("\(animated ? "AnimatedImage" : "WebImage")")
67 | Spacer()
68 | #if os(watchOS)
69 | WebImage(url: url)
70 | .resizable()
71 | .scaledToFit()
72 | .frame(width: 100, height: 100)
73 | #else
74 | if animated {
75 | AnimatedImage(url: url)
76 | .resizable()
77 | .scaledToFit()
78 | .frame(width: 100, height: 100)
79 | } else {
80 | WebImage(url: url)
81 | .resizable()
82 | .scaledToFit()
83 | .frame(width: 100, height: 100)
84 | }
85 | #endif
86 | Button("Toggle \(isOn ? "nil" : "valid") URL") {
87 | isOn.toggle()
88 | }
89 | Spacer()
90 | Toggle("Switch", isOn: $animated)
91 | }
92 | }
93 | }
94 |
95 | // Test Switching url using @State
96 | struct ContentView2: View {
97 | @State var imageURLs = [
98 | "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg",
99 | "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg",
100 | "http://assets.sbnation.com/assets/2512203/dogflops.gif",
101 | "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"
102 | ]
103 | @State var animated: Bool = false // You can change between WebImage/AnimatedImage
104 | @State var imageIndex : Int = 0
105 | var body: some View {
106 | Group {
107 | Text("\(animated ? "AnimatedImage" : "WebImage") - \((imageURLs[imageIndex] as NSString).lastPathComponent)")
108 | Spacer()
109 | #if os(watchOS)
110 | WebImage(url:URL(string: imageURLs[imageIndex]))
111 | .resizable()
112 | .aspectRatio(contentMode: .fit)
113 | #else
114 | if self.animated {
115 | AnimatedImage(url:URL(string: imageURLs[imageIndex]))
116 | .resizable()
117 | .aspectRatio(contentMode: .fit)
118 | } else {
119 | WebImage(url:URL(string: imageURLs[imageIndex]))
120 | .resizable()
121 | .aspectRatio(contentMode: .fit)
122 | }
123 | #endif
124 | Spacer()
125 | Button("Next") {
126 | if imageIndex + 1 >= imageURLs.count {
127 | imageIndex = 0
128 | } else {
129 | imageIndex += 1
130 | }
131 | }
132 | Button("Reload") {
133 | SDImageCache.shared.clearMemory()
134 | SDImageCache.shared.clearDisk(onCompletion: nil)
135 | }
136 | Toggle("Switch", isOn: $animated)
137 | }
138 | }
139 | }
140 |
141 | struct ContentView: View {
142 | @State var imageURLs = [
143 | "http://assets.sbnation.com/assets/2512203/dogflops.gif",
144 | "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif",
145 | "http://apng.onevcat.com/assets/elephant.png",
146 | "http://www.ioncannon.net/wp-content/uploads/2011/06/test2.webp",
147 | "http://www.ioncannon.net/wp-content/uploads/2011/06/test9.webp",
148 | "http://littlesvr.ca/apng/images/SteamEngine.webp",
149 | "http://littlesvr.ca/apng/images/world-cup-2014-42.webp",
150 | "https://isparta.github.io/compare-webp/image/gif_webp/webp/2.webp",
151 | "https://raw.githubusercontent.com/link-u/avif-sample-images/master/fox.profile0.8bpc.yuv420.avif",
152 | "https://raw.githubusercontent.com/link-u/avif-sample-images/master/star-12bpc-with-alpha.avifs",
153 | "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic",
154 | "https://nokiatech.github.io/heif/content/image_sequences/starfield_animation.heic",
155 | "https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png",
156 | "https://raw.githubusercontent.com/ibireme/YYImage/master/Demo/YYImageDemo/mew_baseline.jpg",
157 | "https://via.placeholder.com/200x200.jpg",
158 | "https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_5.jpg",
159 | "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/w3c.svg",
160 | "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/wikimedia.svg",
161 | "https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/stack_of_photos.pdf",
162 | "https://raw.githubusercontent.com/icons8/flat-color-icons/master/pdf/smartphone_tablet.pdf"
163 | ]
164 | @State var animated: Bool = false // You can change between WebImage/AnimatedImage
165 | @EnvironmentObject var settings: UserSettings
166 |
167 | // Used to avoid https://twitter.com/fatbobman/status/1572507700436807683?s=20&t=5rfj6BUza5Jii-ynQatCFA
168 | struct ItemView: View {
169 | @Binding var animated: Bool
170 | @State var url: String
171 | var body: some View {
172 | NavigationLink(destination: DetailView(url: url, animated: self.animated)) {
173 | HStack {
174 | if self.animated {
175 | #if os(macOS) || os(iOS) || os(tvOS) || os(visionOS)
176 | AnimatedImage(url: URL(string:url))
177 | .onViewUpdate { view, context in
178 | #if os(macOS)
179 | view.toolTip = url
180 | #endif
181 | }
182 | .indicator(.activity)
183 | .transition(.fade)
184 | .resizable()
185 | .scaledToFit()
186 | .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
187 | #else
188 | WebImage(url: URL(string:url))
189 | .resizable()
190 | .indicator(.activity)
191 | .transition(.fade(duration: 0.5))
192 | .scaledToFit()
193 | .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
194 | #endif
195 | } else {
196 | WebImage(url: URL(string:url))
197 | .resizable()
198 | .indicator(.activity)
199 | .transition(.fade(duration: 0.5))
200 | .scaledToFit()
201 | .frame(width: CGFloat(100), height: CGFloat(100), alignment: .center)
202 | }
203 | Text((url as NSString).lastPathComponent)
204 | }
205 | }
206 | .buttonStyle(PlainButtonStyle())
207 | }
208 | }
209 |
210 |
211 | var body: some View {
212 | #if os(visionOS)
213 | return NavigationView {
214 | contentView()
215 | .navigationBarTitle(animated ? "AnimatedImage" : "WebImage")
216 | .navigationBarItems(leading:
217 | Button(action: { self.reloadCache() }) {
218 | Text("Reload")
219 | }, trailing:
220 | Button(action: { self.switchView() }) {
221 | Text("Switch")
222 | }
223 | )
224 | }
225 | #endif
226 | #if os(iOS)
227 | return NavigationView {
228 | contentView()
229 | .navigationBarTitle(animated ? "AnimatedImage" : "WebImage")
230 | .navigationBarItems(leading:
231 | Button(action: { self.reloadCache() }) {
232 | Text("Reload")
233 | }, trailing:
234 | Button(action: { self.switchView() }) {
235 | Text("Switch")
236 | }
237 | )
238 | }
239 | #endif
240 | #if os(tvOS)
241 | return NavigationView {
242 | contentView()
243 | .environment(\EnvironmentValues.editMode, self.$settings.editMode)
244 | .navigationBarTitle(animated ? "AnimatedImage" : "WebImage")
245 | .navigationBarItems(leading:
246 | Button(action: { self.reloadCache() }) {
247 | Text("Reload")
248 | }, trailing:
249 | Button(action: { self.switchView() }) {
250 | Text("Switch")
251 | }
252 | )
253 | }
254 | #endif
255 | #if os(macOS)
256 | return NavigationView {
257 | contentView()
258 | .frame(minWidth: 200)
259 | .listStyle(SidebarListStyle())
260 | .contextMenu {
261 | Button(action: { self.reloadCache() }) {
262 | Text("Reload")
263 | }
264 | Button(action: { self.switchView() }) {
265 | Text("Switch")
266 | }
267 | }
268 | }
269 | #endif
270 | #if os(watchOS)
271 | return NavigationView {
272 | contentView()
273 | .navigationTitle("WebImage")
274 | .toolbar {
275 | Button(action: { self.reloadCache() }) {
276 | Text("Reload")
277 | }
278 | }
279 |
280 | }
281 | #endif
282 | }
283 |
284 | func contentView() -> some View {
285 | List {
286 | ForEach(imageURLs, id: \.self) { url in
287 | // Must use top level view instead of inlined view structure
288 | ItemView(animated: $animated, url: url)
289 | }
290 | .onDelete { indexSet in
291 | indexSet.forEach { index in
292 | self.imageURLs.remove(at: index)
293 | }
294 | }
295 | }
296 | }
297 |
298 | func reloadCache() {
299 | SDImageCache.shared.clearMemory()
300 | SDImageCache.shared.clearDisk(onCompletion: nil)
301 | }
302 |
303 | func switchView() {
304 | SDImageCache.shared.clearMemory()
305 | animated.toggle()
306 | }
307 | }
308 |
309 | #if DEBUG
310 | struct ContentView_Previews: PreviewProvider {
311 | static var previews: some View {
312 | ContentView()
313 | }
314 | }
315 | #endif
316 |
--------------------------------------------------------------------------------