SDWebImageSwiftUI
如果您只支持 iOS 15+/macOS 12+,且不介意动画图片格式,可以尝试 SwiftUI 的 AsyncImage。
用途
SDWebImageSwiftUI 是一个基于 SDWebImage 的 SwiftUI 图片加载框架。
它提供了 SDWebImage 的所有您喜欢的功能,如异步图片加载、内存/磁盘缓存、动画图片播放和性能。
该框架提供了不同的 View 结构,其 API 符合 SwiftUI 框架指导原则。如果您熟悉 Image
,您会发现使用 WebImage
和 AnimatedImage
很容易。
特性
由于 SDWebImageSwiftUI 是在 SDWebImage 之上构建的,它既提供了一键化的功能,也提供了您在实际应用程序中可能需要的强大先进的功能。当您需要时,请查看我们的 Wiki。
- 动画图像全栈解决方案,CPU 和 RAM 的平衡
- 渐进式图像加载,支持动画
- 可重用下载,永远不要重复请求单个 URL
- URL 请求/响应修饰符,提供自定义 HTTP 头
- 图像转换器,应用圆角或 CIFilter
- 多缓存系统,从不同的来源查询
- 多加载器系统,从不同资源加载数据
您还可以通过SDWebImage获得现有社区的全部优势。您可以通过Coder插件获得对大量图像格式的支持(GIF/APNG/WebP/HEIF/AVIF/SVG/PDF),通过SDWebImagePhotosPlugin获得PhotoKit支持,通过FirebaseUI进行Firebase集成等。
除了所有这些功能,我们还对SwiftUI进行了优化,例如使用绑定、视图修饰符,采用与SwiftUI兼容的设计模式。
版本
此框架处于高度开发状态,建议尽可能使用最新版本(包括SDWebImage依赖项)。
此框架遵循语义版本控制。每次源代码变更都会提升到主版本号。
变更日志
此项目使用keep-a-changelog格式记录更改。请查看CHANGELOG.md以了解版本之间的更改。这些更改也将更新在发布页面。
合作
所有问题报告、功能请求、贡献和GitHub星星都受欢迎。如果您发现此框架有用,希望获得积极的反馈和推广。
需求
- Xcode 12+
- iOS 13+(推荐 14+)
- macOS 10.15+(推荐 11+)
- tvOS 13+(推荐 14+)
- watchOS 6+(推荐 7+)
SwiftUI 2.0 兼容性
iOS 14(macOS 11)推出了 SwiftUI 2.0,保持了最大的 API 兼容性,但改变了许多内部行为,这使得 SDWebImageSwiftUI 的功能发生了破坏。
从 v2.0.0 版本开始,我们采用了 SwiftUI 2.0 和 iOS 14(macOS 11)的行为。你可以在新的 LazyVStack
中使用 WebImage
和 AnimatedImage
。
var body: some View {
ScrollView {
LazyVStack {
ForEach(urls, id: \.self) { url in
AnimatedImage(url: url)
}
}
}
}
注意:然而,iOS 13/14 之间有许多行为差异难以修复。我们可能为了修复它而破坏一些 API(这些 API 并未设计为公开 API)。
由于维护问题,在未来的版本中,我们将放弃对 iOS 13 的支持,并始终匹配 SwiftUI 2.0 的行为。而且 v2.x 可能是支持 iOS 13 的最后一个版本。
安装
Swift Package Manager
SDWebImageSwiftUI 可通过 Swift Package Manager 获取。
- 对于应用程序集成
为了App集成,您应使用Xcode 12或更高版本,将此包添加到您的App目标中。为此,请检查将包依赖项添加到您的应用程序的指南,了解使用Xcode的逐步教程。
- 对于下游框架
对于下游框架的作者,您应在您的git仓库中创建一个Package.swift
文件,然后添加以下行,以标记您的框架依赖于SDWebImageSwiftUI。
let package = Package(
dependencies: [
.package(url: "https://github.com/SDWebImage/SDWebImageSwiftUI.git", from: "2.0.0")
],
)
CocoaPods
SDWebImageSwiftUI可以通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中
pod 'SDWebImageSwiftUI'
Carthage
SDWebImageSwiftUI可以通过Carthage获取。
github "SDWebImage/SDWebImageSwiftUI"
使用
WebImage
加载网络图片
- 支持占位符和详细选项来控制图片加载,与SDWebImage相同
- 支持渐进式图片加载(类似于baselined)
- 支持成功/失败/进度变化事件,用于自定义处理
- 支持带有活动/进度指示器和自定义的indicator
- 支持内置动画和过渡,由SwiftUI提供支持
- 也支持动画图!
var body: some View {
WebImage(url: URL(string: "https://nokiatech.github.io/heif/content/images/ski_jump_1440x960.heic"))
// Supports options and context, like `.delayPlaceholder` to show placeholder only when error
.onSuccess { image, data, cacheType in
// Success
// Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data
}
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
.placeholder(Image(systemName: "photo")) // Placeholder Image
// Supports ViewBuilder as well
.placeholder {
Rectangle().foregroundColor(.gray)
}
.indicator(.activity) // Activity Indicator
.transition(.fade(duration: 0.5)) // Fade Transition with duration
.scaledToFit()
.frame(width: 300, height: 300, alignment: .center)
}
说明:这个 WebImage
在内部实现时使用 Image
,这是与 SwiftUI 布局和动画系统最兼容的。但是,与 SwiftUI 的 Image
不支持动画图像或矢量图像不同,WebImage
支持(自 v2.0.0 版起默认支持)动画图像。
但是,WebImage
的动画提供了简单的常用功能,因此我们仍然推荐使用 AnimatedImage
进行高级操作,如渐进式动画渲染或矢量图像渲染。
@State var isAnimating: Bool = true
var body: some View {
WebImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"), isAnimating: $isAnimating)) // Animation Control, supports dynamic changes
// The initial value of binding should be true
.customLoopCount(1) // Custom loop count
.playbackRate(2.0) // Playback speed rate
.playbackMode(.bounce) // Playback normally to the end, then reversely back to the start
// `WebImage` supports advanced control just like `AnimatedImage`, but without the progressive animation support
}
说明:对于指示器,你也可以自定义。例如,iOS 14 和 watchOS 7 引入了新的 ProgressView
,可以替换我们内置的 ProgressIndicator/ActivityIndicator
(在 watchOS 中不提供)。
WebImage(url: url)
.indicator {
Indicator { _, _ in
ProgressView()
}
}
AnimatedImage
播放动画
使用 - 支持网络图像以及本地数据和包图像
- 支持动态图像格式及矢量图像格式
- 支持类似网络浏览器的动态渐进图像加载
- 支持使用 SwiftUI Binding 控制动画
- 支持由 SDWebImage 和 Core Animation 提供的指示器和过渡效果
- 支持高级控制,如循环次数、播放速率、缓冲区大小、runloop 模式等
- 支持与原生 UIKit/AppKit 视图的坐标对齐
var body: some View {
Group {
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"))
// Supports options and context, like `.progressiveLoad` for progressive animation loading
.onFailure { error in
// Error
}
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
.placeholder(UIImage(systemName: "photo")) // Placeholder Image
// Supports ViewBuilder as well
.placeholder {
Circle().foregroundColor(.gray)
}
.indicator(SDWebImageActivityIndicator.medium) // Activity Indicator
.transition(.fade) // Fade Transition
.scaledToFit() // Attention to call it on AnimatedImage, but not `some View` after View Modifier (Swift Protocol Extension method is static dispatched)
// Data
AnimatedImage(data: try! Data(contentsOf: URL(fileURLWithPath: "/tmp/foo.webp")))
.customLoopCount(1) // Custom loop count
.playbackRate(2.0) // Playback speed rate
// Bundle (not Asset Catalog)
AnimatedImage(name: "animation1.gif", isAnimating: $isAnimating) // Animation control binding
.maxBufferSize(.max)
.onViewUpdate { view, context in // Advanced native view coordinate
// AppKit tooltip for mouse hover
view.toolTip = "Mouseover Tip"
// UIKit advanced content mode
view.contentMode = .topLeft
// Coordinator, used for Cocoa Binding or Delegate method
let coordinator = context.coordinator
}
}
}
说明:AnimatedImage
支持动画图像格式的图像 URL 或图像数据。它使用 SDWebImage 的 Animated ImageView 作为内部实现。请注意,由于这基于 UIKit/AppKit 可表示的,一些高级 SwiftUI 布局和动画系统可能无法按预期工作。你可能需要使用 UIKit/AppKit 和 Core Animation 来修改原生视图。
说明:AnimatedImage
的一些方法,如 .transition
、.indicator
和 .aspectRatio
,与 SwiftUI.View
协议方法的命名相同。但接收不同类型的参数。这是因为在 AnimatedImage
中,可以将其与 UIKit/AppKit 组件和动画一起使用。如果你发现存在歧义,请使用完整的类型声明而不是点表达式语法。
说明:AnimatedImage
上的一些方法将返回 some View
,一种新的修改后的内容。这会导致你失去与类型相关的修改方法。在这种情况下,你可以重新排列方法调用,或在 .onViewUpdate
中使用 Native View 来进行挽救。
// Using UIKit components
var body: some View {
AnimatedImage(name: "animation2.gif")
.indicator(SDWebImageProgressIndicator.default) // UIKit indicator component
.transition(SDWebImageTransition.flipFromLeft) // UIKit animation transition
}
// Using SwiftUI components
var body: some View {
AnimatedImage(name: "animation2.gif")
.indicator(Indicator.progress) // SwiftUI indicator component
.transition(AnyTransition.flipFromLeft) // SwiftUI animation transition
}
选择哪个视图
为什么这里有两种不同的视图类型,是因为当前 SwiftUI 的限制。但我们旨在为所有用例提供最佳解决方案。
如果你不需要动画图像,首先选择使用 WebImage
。它的表现与内置的 SwiftUI 视图几乎无缝。如果 SwiftUI 行得通,它就有效。如果 SwiftUI 不行,就照此进行。
如果您需要简单的动画图片,请使用WebImage
。它提供基本的动画图片支持。但是,它不支持渐进式动画渲染和矢量图,如果您不关心这一点。
如果您需要强大的动画图片,选择AnimatedImage
。请记住,它也支持静态图片,您无需检查格式,直接使用即可。此外,一些强大的功能,如UIKit/AppKit着色颜色、矢量图、符号图配置、tvOS分层图像,仅在AnimatedImage
中可用,不在SwfitUI中。
但是,由于AnimatedImage
使用UIViewRepresentable
并由UIKit驱动,目前 UIKit和SwiftUI的布局和动画系统之间可能存在一些小的兼容性问题,或者与SwiftUI本身相关的错误。我们尽力匹配SwiftUI的行为,并提供与WebImage
相同的API,这样在需要时可以轻松地在这两种类型之间切换。
ImageManager
为您的自定义View类型使用ImageManager
是一个符合Combine's ObservableObject协议的类。它是我们提供的WebImage
的核心数据源。
对于高级用例,例如,在不需要使用WebImage
的复杂View图形中加载图片,您可以直接使用自己的View类型与Manager绑定。
它与SDWebImageManager
类似,但为SwiftUI世界构建,提供加载图片的真相来源。您最好使用SwiftUI的@ObservedObject
绑定每个Manager实例到您的View实例,这样当图片状态改变时,会自动更新您的View的主体。
struct MyView : View {
@ObservedObject var imageManager = ImageManager()
var body: some View {
// Your custom complicated view graph
Group {
if imageManager.image != nil {
Image(uiImage: imageManager.image!)
} else {
Rectangle().fill(Color.gray)
}
}
// Trigger image loading when appear
.onAppear { self.imageManager.load(url: url) }
// Cancel image loading when disappear
.onDisappear { self.imageManager.cancel() }
}
}
自定义和配置设置
这个框架基于SDWebImage,支持高级自定义和配置,以满足不同用户的需要。
您可以注册多个用于外部图像格式的编码器插件。您可以注册多个缓存(不同的路径和配置)、多个加载器(URLSession和Photos URLs)。您可以控制缓存的过期日期、大小、下载优先级等。所有这些都在我们的wiki中。
为SwiftUI应用放置这些设置代码的最佳位置是AppDelegate.swift
。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Add WebP/SVG/PDF support
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
SDImageCodersManager.shared.addCoder(SDImagePDFCoder.shared)
// Add default HTTP header
SDWebImageDownloader.shared.setValue("image/webp,image/apng,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
// Add multiple caches
let cache = SDImageCache(namespace: "tiny")
cache.config.maxMemoryCost = 100 * 1024 * 1024 // 100MB memory
cache.config.maxDiskSize = 50 * 1024 * 1024 // 50MB disk
SDImageCachesManager.shared.addCache(cache)
SDWebImageManager.defaultImageCache = SDImageCachesManager.shared
// Add multiple loaders with Photos Asset support
SDImageLoadersManager.shared.addLoader(SDImagePhotosLoader.shared)
SDWebImageManager.defaultImageLoader = SDImageLoadersManager.shared
return true
}
更多信息,建议查看我们的示例,了解API详细使用。您还可以查看最新的API文档,以了解高级使用。
文档
常见问题
常见问题
在 List/LazyStack/LazyGrid 和 ForEach 中使用 WebImage/AnimatedImage
SwiftUI 在使用 List/LazyStack/LazyGrid
中的有已知行为(问题?)。只有 顶层 视图可以持有其自己的 @State/@StateObject
,但是子结构在滚动出屏幕时会丢失状态。然而,WebImage/Animated 都是状态性的。为了确保即使在滚动出屏幕时也能保持状态的一致性,您可能需要使用一些技巧。
更多信息:见:https://twitter.com/fatbobman/status/1572507700436807683?s=21&t=z4FkAWTMvjsgL-wKdJGreQ
简而言之,不建议这样做
struct ContentView {
@State var imageURLs: [String]
var body: some View {
List {
ForEach(imageURLs, id: \.self) { url in
VStack {
WebImage(url) // The top level is `VStack`
}
}
}
}
}
相反,使用这种方法
struct ContentView {
struct BodyView {
@State var url: String
var body: some View {
VStack {
WebImage(url)
}
}
}
@State var imageURLs: [String]
var body: some View {
List {
ForEach(imageURLs, id: \.self) { url in
BodyView(url: url)
}
}
}
}
在按钮/导航链接中使用 Image/WebImage/AnimatedImage
SwiftUI 的 Button
默认对其内容(除了 Text
)应用叠加层,编写如下代码是一种常见的错误,会导致奇怪的行怍
// Wrong
Button(action: {
// Clicked
}) {
WebImage(url: url)
}
// NavigationLink create Button implicitly
NavigationView {
NavigationLink(destination: Text("Detail view here")) {
WebImage(url: url)
}
}
相反,您必须覆盖 .buttonStyle
以使用纯样式,或使用 .renderingMode
使用原始模式。您还可以使用 .onTapGesture
修饰符进行触摸处理。请参阅 如何在 Button 和 NavigationLink 中的图像禁用叠加颜色
// Correct
Button(action: {
// Clicked
}) {
WebImage(url: url)
}
.buttonStyle(PlainButtonStyle())
// Or
NavigationView {
NavigationLink(destination: Text("Detail view here")) {
WebImage(url: url)
.renderingMode(.original)
}
}
与外部加载器/缓存/编码器一起使用
SDWebImage 本身支持许多自定义加载器(例如 Firebase Storage 和 PhotosKit)、缓存(例如 YYCache 和 PINCache),以及编码器(例如 WebP 和 AVIF,甚至 Lottie)。
这是如何使用 SwiftUI 环境设置这些外部组件的教程。
设置外部 SDK
您可以将设置代码放在您的 SwiftUI App.init()
方法内部。
@main
struct MyApp: App {
init() {
// Custom Firebase Storage Loader
FirebaseApp.configure()
SDImageLoadersManager.shared.loaders = [FirebaseUI.StorageImageLoader.shared]
SDWebImageManager.defaultImageLoader = SDImageLoadersManager.shared
// WebP support
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
或者,如果您的 App 具有复杂的 AppDelegate
类,可以将设置代码放在那里
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
SDImageCachesManager.shared.caches = [YYCache(name: "default")]
SDWebImageManager.defaultImageCache = SDImageCachesManager.shared
return true
}
}
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
使用外部SDK
对于某些自定义加载器,您需要使用一些特殊API创建URL
结构,以便SDWebImage可以从其他SDK中检索上下文,例如
- FirebaseStorage
let storageRef: StorageReference
let storageURL = NSURL.sd_URL(with: storageRef) as URL?
// Or via convenience extension
let storageURL = storageRef.sd_URLRepresentation
- PhotosKit
let asset: PHAsset
let photosURL = NSURL.sd_URL(with: asset) as URL?
// Or via convenience extension
let photosURL = asset.sd_URLRepresentation
对于某些自定义编解码器,您需要使用一些选项请求图像以控制行为,例如矢量图像SVG/PDF。因为SwiftUI.Image或WebImage完全不支持矢量图形。
- SVG/PDF 编解码器
let vectorURL: URL? // URL to SVG or PDF
WebImage(url: vectorURL, context: [.imageThumbnailPixelSize: CGSize(width: 100, height: 100)])
- Lottie 编解码器
let lottieURL: URL? // URL to Lottie.json
WebImage(url: lottieURL, isAnimating: $isAnimating)
对于缓存,您实际上不必担心任何事情。设置完成后,它会自动工作。
用于向后部署和弱引用SwiftUI
SDWebImageSwiftUI支持当您的App Target的部署目标版本小于iOS 13/macOS 10.15/tvOS 13/watchOS 6时使用。这允许弱链接SwiftUI(Combine),以便在运行时进行可用性检查编写代码。
要使用向后部署,您必须完成以下操作
添加弱引用框架
将-weak_framework SwiftUI -weak_framework Combine
添加到您的App Target的Other Linker Flags
构建设置中。您也可以使用Xcode的Optional Framework
复选框完成此操作,这将产生相同的效果。
请注意,所有第三方SwiftUI框架都应该有此构建设置,而不仅仅是SDWebImageSwiftUI。否则,当在iOS 12设备上运行时,它将在启动时触发运行时dyld错误。
在iOS 12.1上的回退部署-
对于低于iOS 12.2的部署目标版本(Swift 5运行时首次内置到iOS系统中的版本),您必须更改SDWebImageSwiftUI的最小部署目标版本。这可能会对编译器的优化产生一些副作用,并触发某些框架的大量警告。
然而,对于iOS 12.2+,您仍然可以将最小部署目标版本保持为iOS 13,iOS 13客户端不会出现额外警告或性能下降。
因为Swift使用最小部署目标版本来检测是否链接App内打包的Swift运行时,还是系统内建的(/usr/lib/swift/libswiftCore.dylib)。
- 对于CocoaPods用户,您可以通过安装后处理器在Podfile中更改最小部署目标版本。
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' # version you need
end
end
end
-
对于Carthage用户,您可以使用
carthage update --no-build
下载依赖项,然后更改Xcode项目的部署目标版本并构建二进制框架。 -
对于SwiftPM用户,您必须使用本地依赖项(带有Git子模块)来更改部署目标版本。
在iOS 12.2+上的回退部署-
-
对于Carthage用户,构建的二进制框架将使用库进化来支持回退部署。
-
对于CocoaPods用户,您可以在Podfile中使用以下命令跳过平台版本验证:
platform :ios, '13.0' # This does not effect your App Target's deployment target version, just a hint for CocoaPods
- 对于SwiftPM用户,SwiftPM不支持弱链接或库进化,因此如果不更改最小部署目标,它无法部署到iOS 12+用户。
添加可用性注释-
将所有带有可用性注释和运行时检查的SwiftUI代码添加进来,如下所示:
// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ...
if #available(iOS 13, *) {
window.rootViewController = UIHostingController(rootView: ContentView())
} else {
window.rootViewController = ViewController()
}
// ...
}
// ViewController.swift
class ViewController: UIViewController {
var label: UILabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(label)
label.text = "Hello World iOS 12!"
label.sizeToFit()
label.center = view.center
}
}
// ContentView.swift
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
struct ContentView : View {
var body: some View {
Group {
Text("Hello World iOS 13!")
WebImage(url: URL(string: "https://i.loli.net/2019/09/24/rX2RkVWeGKIuJvc.jpg"))
}
}
}
示例
要使用 SwiftUI 运行示例,请按照以下步骤操作
- 在根目录下运行
pod install
以安装依赖项。 - 打开
SDWebImageSwiftUI.xcworkspace
,等待 SwiftPM 下载测试依赖项完成。 - 选择
SDWebImageSwiftUIDemo
方案并运行示例应用程序。
由于 SwiftUI 致力于支持所有 Apple 平台,我们的示例也做到了这一点,一个代码库涵盖
- iOS(iPhone/iPad/Mac Catalyst)
- macOS
- tvOS
- watchOS
示例提示
- 使用
Switch
(在 macOS 上右键单击/在 watchOS 上强制按压)在WebImage
和AnimatedImage
之间切换。 - 使用
Reload
(在 macOS 上右键单击/在 watchOS 上强制按压)清除缓存。 - 使用
Swipe Left
(在 tvOS 上的菜单按钮)从列表中删除一个图像 URL。 - 捏合手势(在 watchOS 上的数字 crown/在 tvOS 上的播放按钮)可放大详细页面上的图像。
- 清除缓存并转到详细页面以查看渐进加载。
测试
SDWebImageSwiftUI 有单元测试来提高代码质量。对于 SwiftUI,苹果没有提供官方的单元测试解决方案。
然而,由于 SwiftUI 是基于状态的和基于属性的布局系统,有一些开源项目提供了解决方案
- ViewInspector:检查视图的运行时属性值(如
.frame
修饰符、.image
值)。我们使用它来测试AnimatedImage
和WebImage
。它还允许检查原生 UIView/NSView,我们将其用于测试ActivityIndicator
和ProgressIndicator
。
要运行测试
- 在根目录下运行
pod install
以安装依赖项。 - 打开
SDWebImageSwiftUI.xcworkspace
,等待 SwiftPM 下载测试依赖项完成。 - 选择
SDWebImageSwiftUITests
方案并开始测试。
我们已经设置了 CI 管道,每个 PR 都将运行测试用例并将测试报告上传到 codecov。
屏幕截图
- iOS 示例
- macOS 示例
- tvOS 示例
- watchOS 示例
额外说明
除了上述所有内容之外,该项目还可以在Swift平台上为SDWebImage本身确保以下功能可用。
- SwiftUI兼容性
- Swift包管理器集成
- Swift源代码兼容性和Swifty
这意味着,该项目是一个核心用例和下游依赖,它推动SDWebImage本身的未来发展。
作者
感谢
许可
SDWebImageSwiftUI遵循MIT许可。更多信息请参阅LICENSE文件。