├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cartfile ├── Example ├── SDWebImageSwiftUI.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── SDWebImageSwiftUIDemo-macOS.xcscheme │ │ ├── SDWebImageSwiftUIDemo-tvOS.xcscheme │ │ ├── SDWebImageSwiftUIDemo-visionOS.xcscheme │ │ ├── SDWebImageSwiftUIDemo-watchOS WatchKit App.xcscheme │ │ ├── SDWebImageSwiftUIDemo.xcscheme │ │ ├── SDWebImageSwiftUITests macOS.xcscheme │ │ ├── SDWebImageSwiftUITests tvOS.xcscheme │ │ └── SDWebImageSwiftUITests.xcscheme ├── SDWebImageSwiftUIDemo-macOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── wifi.exclamationmark.imageset │ │ │ ├── Contents.json │ │ │ └── wifi.exclamationmark.svg │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── SDWebImageSwiftUIDemo_macOS.entitlements ├── SDWebImageSwiftUIDemo-tvOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ └── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SDWebImageSwiftUIDemo-visionOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.solidimagestack │ │ │ ├── Back.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SDWebImageSwiftUIDemo-watchOS WatchKit App │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── Interface.storyboard │ └── Info.plist ├── SDWebImageSwiftUIDemo-watchOS WatchKit Extension │ ├── Assets.xcassets │ │ ├── Complication.complicationset │ │ │ ├── Circular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Extra Large.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Bezel.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Circular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Corner.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Large Rectangular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Modular.imageset │ │ │ │ └── Contents.json │ │ │ └── Utilitarian.imageset │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ExtensionDelegate.swift │ ├── HostingController.swift │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SDWebImageSwiftUIDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── DetailView.swift │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── SceneDelegate.swift └── Screenshot │ ├── Demo-iOS.jpg │ ├── Demo-macOS.jpg │ ├── Demo-tvOS.jpg │ └── Demo-watchOS.jpg ├── LICENSE ├── Package.resolved ├── Package.swift ├── Podfile ├── README.md ├── SDWebImageSwiftUI.podspec ├── SDWebImageSwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── SDWebImageSwiftUI macOS.xcscheme │ ├── SDWebImageSwiftUI tvOS.xcscheme │ ├── SDWebImageSwiftUI visionOS.xcscheme │ ├── SDWebImageSwiftUI watchOS.xcscheme │ └── SDWebImageSwiftUI.xcscheme ├── SDWebImageSwiftUI.xcworkspace └── contents.xcworkspacedata ├── SDWebImageSwiftUI ├── Classes │ ├── .gitkeep │ ├── AnimatedImage.swift │ ├── Image.swift │ ├── ImageManager.swift │ ├── ImagePlayer.swift │ ├── ImageViewWrapper.swift │ ├── Indicator │ │ └── Indicator.swift │ ├── SDWebImageSwiftUI.swift │ ├── Transition │ │ └── Transition.swift │ └── WebImage.swift ├── Module │ ├── Info.plist │ └── SDWebImageSwiftUI.h └── Resources │ └── PrivacyInfo.xcprivacy ├── Tests ├── AnimatedImageTests.swift ├── ImageManagerTests.swift ├── Images.bundle │ ├── MonochromeTestImage.jpg │ ├── TestEXIF.png │ ├── TestImage.gif │ ├── TestImage.heic │ ├── TestImage.heif │ ├── TestImage.jpg │ ├── TestImage.png │ ├── TestImageAnimated.apng │ ├── TestImageAnimated.heic │ ├── TestImageLarge.jpg │ └── TestLoopCount.gif ├── Info.plist ├── TestUtils.swift └── WebImageTests.swift ├── carthage.sh └── codecov.yml /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.3] - 2024-11-06 10 | - Fixed old version compiler does not support automatic self capture in Xcode 14.2 and Swift 5.7.2 #340 11 | - Fix the data race because progress block is called in non-main queue #341 12 | 13 | ## [3.1.2] - 2024-08-29 14 | - Allows easy to use WebImage with isAnimating default to false and change to true later #333 15 | - Note: This changes WebImage's internal loaded image from `UIImage/NSImage` to `SDAnimatedImage`, which is compatible for `UIImageView/NSImageView` 16 | 17 | ## [3.1.1] - 2024-07-01 18 | - Fix the transition visual jump between placeholderImage and final image for AnimatedImage #326 19 | 20 | ## [3.1.0] - 2024-06-27 21 | - Re-implements the aspectRatio support on AnimatedImage, fix issue like cornerRadius #324 22 | - Add Image scale support in WebImage init #323 23 | - Update platform names in `available` attributes #321 24 | - - This is source compatible but binary incompatible version 25 | 26 | ## [3.0.4] - 2024-04-30 27 | - Trying to move the initial state setup before onAppear to fix the watchOS switching url or any other state issue #316 28 | - This solve a issue in history when sometimes SwiftUI does not trigger the `onAppear` and cause state error, like #312 #314 29 | 30 | ## [3.0.3] - 2024-04-29 31 | - Added totally empty privacy manifest #315 32 | - People who facing the issue because of Privacy Manifest declaration during ITC validation can try this version 33 | 34 | ## [3.0.2] - 2024-03-27 35 | - Fix the assert crash then when using Data/Name in AnimatedImage #309 36 | 37 | ## [3.0.1] - 2024-03-18 38 | - Fix the issue for WebImage/AnimatedImage when url is nil will not cause the reloading #304 39 | 40 | ## [3.0.0] - 2024-03-09 41 | - 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) 42 | - Fix AnimatedImage aspectRatio issue when ratio is nil #301 43 | - Upgrade to support visionOS on CocoaPods #298 44 | 45 | ## [3.0.0-beta.3] - 2023-12-04 46 | 47 | ### Changed 48 | - Update the AnimatedImage API to expose the SDAnimatedImageView #285 49 | - Fix the AnimatedImgae rendering mode about compatible with SDWebImage 5.18+ 50 | 51 | ## [3.0.0-beta.2] - 2023-10-21 52 | 53 | ### Changed 54 | - Update the WebImage API to match SwiftUI.AsyncImage #275 @Kyle-Ye 55 | - Allows to use UIImage/NSImage as defaults when init the AnimatedImage with JPEG data #277 56 | 57 | ### Removed 58 | - `WebImage.placeholder(@ViewBuilder content: () -> T) -> WebImage` 59 | - `WebImage.placeholder(_ image: Image) -> WebImage` 60 | - `AnimatedImage.placeholder(@ViewBuilder content: () -> T) -> AnimatedImage` 61 | - `AnimatedImage.placeholder(_ image: PlatformImage) -> AnimatedImage` 62 | 63 | ## [3.0.0-beta] - 2023-09-02 64 | 65 | ### Added 66 | - (Part 1) Support compile for visionOS (no package manager support) #267 67 | 68 | ### Changed 69 | 70 | - Drop iOS 13/macOS 10.15/tvOS 13/watchOS 6 support #250 71 | - ProgressIndicator and ActivityIndicator is removed. Use `ProgressView` instead 72 | - Availability is changed to iOS 14/macOS 11/tvOS 11/watchOS 7 73 | - Embed `SwiftUIBackports` dependency is removed. 74 | 75 | ## [2.2.3] - 2023-04-32 76 | - Fix the issue that Static Library + Library Evolution cause the build issue on Swift 5.8 #263 77 | 78 | ## [2.2.2] - 2022-12-27 79 | 80 | ### Fixed 81 | - Fix the bug that isAnimating control does not works on WebImage #251 82 | - Note you should upgrade the SDWebImage 5.14.3+, or this may cause extra Xcode 14's runtime warning (function is unaffected) 83 | 84 | ## [2.2.1] - 2022-09-23 85 | 86 | ### Fixed 87 | 88 | - Fix the nil url always returns Error will cause infinity onAppear call and image manager to load, which waste CPU #235 89 | - Fix the case which sometimes the player does not stop when WebImage it out of screen #236 90 | - Al v2.2.0 users are recommended to update 91 | 92 | ## [2.2.0] - 2022-09-22 93 | 94 | ### Fixed 95 | 96 | - Fix iOS 13 compatibility #232 97 | - Fix WebImage/Animated using @State to publish changes 98 | - Al v2.1.0 users are recommended to update 99 | 100 | ### Changed 101 | - ImageManager API changes. The init method has no args, use `load(url:options:context:)` instead 102 | 103 | ## [2.1.0] - 2022-09-15 104 | 105 | ### Fixed 106 | 107 | - Refactor WebImage/AnimatedImage using SwiftUIBackports and StateObject #227 108 | - Fix iOS 16 undefined behavior warnings because of Publishing changes from within view updates. 109 | - Fix iOS 14+ WebImage behavior using `@StateObject` (and backport on iOS 13) 110 | 111 | ### Changed 112 | 113 | - The `IndicatorReportable` is misused and removed. Use `IndicatorStatus` instead. 114 | - Deprecate iOS 13 support, this may be the last version to support iOS 13. 115 | 116 | ## [2.0.2] - 2021-03-10 117 | 118 | ### Fixed 119 | - 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 120 | 121 | ### Changed 122 | - 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 123 | 124 | ## [2.0.1] - 2021-02-25 125 | ### Fixed 126 | - Fix the rare cases that WebImage will lost animation when visibility changes. #171 127 | 128 | ## [2.0.0] - 2021-02-23 129 | ### Added 130 | - Update with the playbackMode support for `WebImage` and `AnimatedImage` #168 131 | - Update watchOS demo to watchOS 7, remove the custom indicator sample and use `ProgressView` instead #166 132 | - Update the Example to make WebImage animatable by default #160 133 | 134 | ### Fixed 135 | - Fix the issue sometime the `WebImage` appear/disappear logic wrong. Using UIKit/AppKit to detect the visibility #164 136 | - Fix the leak of WebImage with animation and NavigationLink. #163 137 | - Try to fix the recursive updateView when using AnimatedImage inside `ScrollView/LazyVStack`. Which cause App freeze #162 138 | - Remove the fix for EXIF image in WebImage, which is fixed by Apple in iOS 14 #159 139 | 140 | ### Changed 141 | - Bump the limit to Xcode 12, because we need new iOS 14+ APIs check #167 142 | - Update the WebImage to defaults animatable #165 143 | 144 | ### Removed 145 | - Remove the wrong design onSuccess API. Using the full params one instead #169 146 | 147 | ## [1.5.0] - 2020-06-01 148 | ### Added 149 | - Add the convenient API support to use SwiftUI transition with ease-in-out duration #116 150 | - Update the Travis-CI to use Catalina and enable macOS test case #98 151 | 152 | ## [1.4.0] - 2020-05-07 153 | ### Added 154 | - Add the same overload method for onSuccess API, which introduce the image data arg. Keep the source code compatibility #109 155 | - Add the support for image data observable on ImageManager #107 156 | 157 | ## [1.3.4] - 2020-04-30 158 | ### Fixed 159 | - Revert the changes to prefetch the image url from memory cache #106 160 | 161 | ## [1.3.3] - 2020-04-15 162 | ### Fixed 163 | - Try to solve the SwiftUI bug of rendering EXIF UIImage in WebImage, as well as vector images #102 164 | - 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. 165 | 166 | ## [1.3.2] - 2020-04-14 167 | ### Added 168 | - Automatically import SDWebImage when user write import SDWebImageSwiftUI #100 169 | 170 | ## [1.3.1] - 2020-04-10 171 | ### Fixed 172 | - Fix Carthage support. Do not embed SDWebImage.framework in SDWebImageSwiftUI.framework #97. Thanks @jonkan 173 | 174 | ## [1.3.0] - 2020-04-05 175 | ### Added 176 | - Supports the `placeholder` View Builder API for `AnimatedImage` #94 177 | 178 | ### Changed 179 | - Upgrade the dependency of SDWebImage 5.7.0 #93 180 | 181 | ## [1.2.1] - 2020-04-01 182 | ### Fixed 183 | - Fix the issue when using `WebImage` with some transition like scaleEffect, each time the new state update will cause unused image fetching #92 184 | 185 | ## [1.2.0] - 2020-03-29 186 | ### Added 187 | - Supports the `delayPlaceholder` for WebImage #91 188 | - `AnimatedImage` little patch - UIKit/AppKit animated image now applied for `resizingMode` #89 189 | 190 | ### Fixed 191 | - Fix the issue when dealloc `AnimatedImage`'s native View, the window does not exist and cause Crash #90 192 | 193 | ## [1.1.0] - 2020-03-24 194 | ### Added 195 | - `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. 196 | 197 | ## [1.0.0] - 2020-03-03 198 | ### Added 199 | - `WebImage` now supports animation, use `isAnimating` binding value on init methods. 200 | - `WebImage` now supports the detailed animation control options, like `customLoopCount`, `pausable`, `purgeable`, `playbackRate`. 201 | - `AnimatedImage` now supports the indicator with `ViewModifier` as `WebImage`. 202 | - `IndicatorViewModifier` now public. 203 | - `IndicatorReportable` now public. 204 | 205 | ### Changed 206 | - Indicator's `progress` type now changed from `CGFloat` to `Double`. 207 | - `WebImage.aniamted(_:)` now becomes the `WebImage.init(url:options:context:isAnimating:)` Binding arg, you can use the Binding to control animations as well. 208 | - `AnimatedImage.playBackRate` now becomes `AnimatedImage.playbackRate` 209 | - `AnimatedImage.customLoopCount` now is `UInt` instead of `Int`. 210 | - `AnimatedImage.resizable` modifier now matches the SwiftUI behavior, you must call it or the size will be fixed to image pixel size. 211 | 212 | ### Removed 213 | - Removed all the description about 0.x version behavior in README.md. 214 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "SDWebImage/SDWebImage" ~> 5.10 -------------------------------------------------------------------------------- /Example/SDWebImageSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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-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-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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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-macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Example/SDWebImageSwiftUIDemo-macOS/Assets.xcassets/wifi.exclamationmark.imageset/wifi.exclamationmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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-macOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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/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-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/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 - 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 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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.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/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/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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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-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-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-tvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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-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-tvOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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-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/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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-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/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/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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/SDWebImageSwiftUIDemo-visionOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/SDWebImageSwiftUIDemo-visionOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 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-watchOS WatchKit App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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/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 | } -------------------------------------------------------------------------------- /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 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 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/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 | } -------------------------------------------------------------------------------- /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/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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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/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-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 | -------------------------------------------------------------------------------- /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/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/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/SDWebImageSwiftUIDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Example/SDWebImageSwiftUIDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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/Screenshot/Demo-iOS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Example/Screenshot/Demo-iOS.jpg -------------------------------------------------------------------------------- /Example/Screenshot/Demo-macOS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Example/Screenshot/Demo-macOS.jpg -------------------------------------------------------------------------------- /Example/Screenshot/Demo-tvOS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Example/Screenshot/Demo-tvOS.jpg -------------------------------------------------------------------------------- /Example/Screenshot/Demo-watchOS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Example/Screenshot/Demo-watchOS.jpg -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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": "73b9397cfbd902f606572964055464903b1d84c6", 10 | "version": "5.19.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /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.10.0") 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 | -------------------------------------------------------------------------------- /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.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.3' 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.10' 41 | s.swift_version = '5.3' 42 | end 43 | -------------------------------------------------------------------------------- /SDWebImageSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /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.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.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SDWebImageSwiftUI/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/SDWebImageSwiftUI/Classes/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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.3 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SDWebImageSwiftUI/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTrackingDomains 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Tests/Images.bundle/MonochromeTestImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/MonochromeTestImage.jpg -------------------------------------------------------------------------------- /Tests/Images.bundle/TestEXIF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestEXIF.png -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImage.gif -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImage.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImage.heic -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImage.heif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImage.heif -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImage.jpg -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImage.png -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImageAnimated.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImageAnimated.apng -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImageAnimated.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImageAnimated.heic -------------------------------------------------------------------------------- /Tests/Images.bundle/TestImageLarge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestImageLarge.jpg -------------------------------------------------------------------------------- /Tests/Images.bundle/TestLoopCount.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SDWebImage/SDWebImageSwiftUI/ea241055c576d977c607d8cac5fac840c90f80f4/Tests/Images.bundle/TestLoopCount.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 "$@" -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------