Image 图像

SwiftUI 有一个专用的 Image 类型来处理应用程序中的图片,可以通过以下主要方式创建它们:

1. 使用自定义图片

使用图片前首先要将图片导入到素材目录,也就是 Assets.xcassets

// 只需要指定圖片名稱,你就會在預覽畫布中見到圖片
Image("paris")

// 当使用这样的固定图像名称时,Xcode 会自动生成一个常量名称,因此可以使用这些名称来代替字符串(Xcode 15 及更高版本)
// 也就是写成 Image(.example) ,这比使用字符串安全,会有代码提示
Image(.paris)

2. 从 UIImage 创建

// 由于使用 named 初始值设定项加载 UIImage 会返回一个可选图像,因此您应该添加默认值,
// 或者如果您确定它存在于资源目录中,则应使用强制展开:
Image(uiImage: UIImage(named: "cat")!)

3. 装饰性图片 decorative

// Image(decorative: "pencil") 同样可以加载图像,但不会为启用屏幕阅读器(screen reader)的用户读出该图像
// 这对于不传达其他重要信息的图像很有用
Image(decorative: "pencil")

SF Symbol

Apple 将 SF Symbols 设计为包含 6000+ 图标的库,您可以作为开发人员免费使用。它们与 Apple 的系统字体 San Francisco 无缝集成,并提供 9 种粗细和 3 种刻度。SF 符号会自动与文本对齐,您可以将其导出为矢量,以创建具有相似设计特征和辅助功能的自定义符号。

我们可以通过 官方 SF Symbol 应用程序 浏览 6000+ 可用符号,找到想要的符号后可以右键单击该图标并选择“复制名称”。

// 基本语法
Image(systemName: "square.and.pencil")

// 或者将图像与标签组合在一起
Label("Heart", systemImage: "heart")

// 甚至可以将多行字符串与符号组合在一起
Text("""
    This SwiftLee example is combined with a symbol \\(Image(systemName: "chevron"))
""")

基础样式

// 设置尺寸等级:每个图标都有不同的比例,可以设置它们的尺寸等级
Image(systemName: "square.and.pencil")
    .imageScale(.large)

// 设置字型:SF 与 San Francisco 无缝整合,可以用 font 修饰符进行设置
Image(systemName: "cloud.heavyrain")
		.font(.system(size: 100, weight: .bold))

// 设置颜色:可以应用 foregroundColor 来变更符号颜色
Image(systemName: "cloud.heavyrain")
		.foregroundColor(.blue)

设置变体

// 一些 SF 符号带有多种变体,可以使用 symbolVariant 修饰符调整

// 例如,渲染一个带有斜线的钟形图标
Image(systemName: "bell").symbolVariant(.slash)

// 例如,渲染一个正方形围绕钟形
Image(systemName: "bell").symbolVariant(.square)

// 或者斜杠渲染“响铃”符号并填充响铃
Image(systemName: "bell").symbolVariant(.fill.slash)

// 这和以下写法是等效的
Image(systemName: "bell.fill.slash")

渲染模式 symbolRenderingMode

使用 symbolRenderingMode 修饰符可以设置 SF 的渲染模式,它使用不透明度来创建屏幕上阴影的变化。主要有四种模式: monochromehierarchicalmulticolorpalette

// monochrome 单色模式:该模式会把符号当作单独一层对待,也就是整个符号只有一种颜色
Image(systemName: "battery.100.bolt")
	.symbolRenderingMode(.monochrome)
	.foregroundStyle(.orange)

// hierarchical 模式:会把符号当作多个图层对待,每个图层虽然颜色一样,但有着不同的透明度
Image(systemName: "theatermasks")
	.symbolRenderingMode(.hierarchical)
	.foregroundStyle(.blue)

// multicolor 模式:会把符号当作多个图层对待,每个图层会使用预制的颜色(不能改变)。不是每个符号都支持 multicolor,使用时需确认
Image(systemName: "battery.100.bolt")
	.symbolRenderingMode(.multicolor)

	
// palette 模式:会把符号当作多个图层对待,可以完全自定义图像中的各个颜色。
Image(systemName: "shareplay")
	.symbolRenderingMode(.palette)
	// 例如可以同时渲染 SharePlay 图标为蓝色和黑色,如下所示:
	.foregroundStyle(.blue, .black)
	// 这些颜色的应用方式取决于每个单独的符号是两层还是三层定义,如果是三种变体的符号,只需添加额外的颜色:
	.foregroundStyle(.blue, .green, .red)

// 甚至适用于复杂的前景样式,例如为图标中的每个人提供一个渐变:
Image(systemName: "person.3.sequence.fill")
	.symbolRenderingMode(.palette)
	.foregroundStyle(
			.linearGradient(colors: [.red, .black], startPoint: .top, endPoint: .bottomTrailing),
			.linearGradient(colors: [.green, .black], startPoint: .top, endPoint: .bottomTrailing),
			.linearGradient(colors: [.blue, .black], startPoint: .top, endPoint: .bottomTrailing)
	)

可变着色 variableValue

// 某些 SF 符号支持可变着色,这意味着可以根据 0 到 1 之间的分数填充不同的部分。例如以下显示一个部分填充的 Wi-Fi 图标:
Image(systemName: "wifi", variableValue: 0.5)

多色模式 renderingMode

// 如果您使用的图像具有颜色元素,则可以使用.renderingMode(.original) 激活多色模式,如下所示:
Image(systemName: "cloud.sun.rain.fill")
	.renderingMode(.original)
	.font(.largeTitle)
	.background(.red)
	.clipShape(Circle())

// 还可以将 foregroundStyle() 应用于多色 SF 符号,这将导致符号的一部分重新着色。例如,这将使图标的一部分呈现蓝色,一部分呈现绿色:
Image(systemName: "person.crop.circle.fill.badge.plus")
	.renderingMode(.original)
	.foregroundStyle(.blue)
	.background(.red)
	.clipShape(Circle())

<aside> 💡

关于 SF Symbol 的动画可以查看这里: SF Symbol 动画

</aside>


AsyncImage 网络图像

SwiftUI 的 Image 视图非常适合应用程序包中的图像,但如果想从互联网加载远程图像,则需要使用 AsyncImage 。这些是使用图像 URL 而不是简单的资源名称或 Xcode 生成的常量创建的,但 SwiftUI 会为我们处理剩下的所有事情 – 它下载图像、缓存下载并自动显示它。

1. 声明方法

AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"))

// 还可以提前告诉 SwiftUI 正在尝试加载 3 倍比例的图像,这个能大概设置尺寸,但不精确
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"), scale: 3)

// 请注意 URL 是可选值,如果 URL 字符串无效,AsyncImage 将仅显示默认的灰色占位符。
// 如果由于某种原因无法加载图像(如果用户离线,或者图像不存在),那么系统将继续显示相同的占位符图像。

2. 网络图像设置方式

AsyncImage 最大的问题是,传统的设置图像的修饰符,对它无效。因为 SwiftUI 在实际获取图像数据之前不知道如何应用它们。

AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>"))
    // 这行加了无效
		// 如果这样设置 frame ,实际上它只对包含图像的容器有效(应用程序运行时可以短暂看到占位符,200x200灰色方块,一旦加载完成就会自动消失)
    .frame(width: 200, height: 200)

		//这行加了程序崩溃
		.resizable()

因此 AsyncImage 的正确写法是:

// 该写法意思是:一旦下载好图片,就会向我们传递最终的图像,然后就可以根据需要进行自定义。另外还提供了第二个闭包,可以自定义占位符
AsyncImage(url: URL(string: "<https://hws.dev/img/logo.png>")) { image in
		// 指代下载完成后的图像
    image
	    .resizable()
	    .scaledToFit()
} placeholder: {
		// 指代占位符:占位符视图可以是任何内容。例如替换为 ProgressView() ,那么将得到一个小旋转器活动指示器
    Color.red
}
.frame(width: 200, height: 200)

3. 设置不同阶段的呈现

还可以分阶段(下载时,下载完成,出错)对网络图像进行定义,当您想要在各个阶段显示不同的视图时,特别有用。

// 设置方式1: 
AsyncImage(url: URL(string: "<https://hws.dev/img/bad.png>")) { phase in
		// 下载完成时展示
    if let image = phase.image {
        image
            .resizable()
            .scaledToFit()
    } 
		// 下载出错时展示
		else if phase.error != nil {
        Text("There was an error loading the image.")
    } 
		// 下载过程中展示
		else {
        ProgressView()
    }
}
.frame(width: 200, height: 200)
// 设置方式2: 目前有以下几种内置的阶段:.empty 代表加载尚未完成, .failure 代表图像加载失败, success 代表图像已准备好(如果它有效)
AsyncImage(url: URL(string: "<https://hws.dev/paul3.jpg>")) { phase in
    switch phase {
    case .failure:
            Image(systemName: "photo")
                .font(.largeTitle)
    // let image 是枚举的关联值解包
    case .success(let image):
            image
                .resizable()
    default:
            ProgressView()
    }
}
.frame(width: 256, height: 256)
.clipShape(.rect(cornerRadius: 25))

NSCache  图像缓存

https://www.createwithswift.com/image-caching-in-swiftui/?ref=create-with-swift-newsletter

当你构建一个显示互联网图片的 iOS 应用时,无论是用户头像、产品照片还是社交媒体内容,你可能会注意到一个令人沮丧的现象:每次用户返回屏幕时,图片都会重新加载。这会导致加载时间更长、移动数据使用量增加以及整体体验变慢。像上面的 AsyncImage 不会在屏幕加载之间缓存图像。因此,如果同一张图片滚动出屏幕后又返回,SwiftUI 可能会再次从网络获取它。

解决这个问题的方法是图像缓存 ,缓存是将数据保存在一个临时位置,这样你就不必一次又一次地获取它。以图片为例:

缓存主要有两种类型:

这里介绍使用 NSCache 作为内存缓存的情况,它是 Apple 的线程安全缓存解决方案,可以自动处理内存压力。它是线程安全的,当系统内存不足时它会自动删除项目,并允许您设置它应该存储的项目数或应该使用的内存量的限制。具体定义方法如下:

import SwiftUI
import UIKit

class ImageCache {

    static let shared = NSCache<NSURL, UIImage>()
    
    init() {
        // Configure cache limits
        ImageCache.shared.countLimit = 100 // Maximum 100 images
        ImageCache.shared.totalCostLimit = 50 * 1024 * 1024 // 50MB limit
    }
    
}

// 然后围绕 AsyncImage 构建一个添加缓存功能的包装器:
// 该设计为 content内容和 placeholder占位符使用了单独的泛型,以便支持不同的视图类型(例如, ProgressView 与样式化图像)
// 它还应用了 AnyView 类型擦除,以确保所有 switch case 都返回相同的视图类型,并使用 MainActor.run ,因为 UI 更新必须在主线程上进行
struct CachedAsyncImage<Content: View, Placeholder: View>: View {

    let url: URL
    let scale: CGFloat
    let content: (Image) -> Content
    let placeholder: () -> Placeholder

    @State private var cachedImage: UIImage?

    init(
        url: URL,
        scale: CGFloat = 1.0,
        @ViewBuilder content: @escaping (Image) -> Content,
        @ViewBuilder placeholder: @escaping () -> Placeholder
    ) {
        self.url = url
        self.scale = scale
        self.content = content
        self.placeholder = placeholder
    }

    var body: some View {
        Group {
            if let cachedImage {
                content(Image(uiImage: cachedImage))
            } else {
                AsyncImage(url: url, scale: scale) { phase -> AnyView in
                    switch phase {
                    case .success(let image):
                        // Save to cache when AsyncImage successfully loads
                        saveToCache(from: url)
                        return AnyView(content(image))
                    case .failure(_):
                        return AnyView(placeholder())
                    case .empty:
                        return AnyView(placeholder())
                    @unknown default:
                        return AnyView(placeholder())
                    }
                }
            }
        }
        .onAppear {
            loadFromCache()
        }
    }

		// 当视图出现时, loadFromCache() 首先检查图片是否已存储。如果找到,则立即显示缓存的图片。如果未缓存,代码将回退到使用 AsyncImage
    private func loadFromCache() {
        if let cached = ImageCache.shared.object(forKey: url as NSURL) {
            cachedImage = cached
        }
    }
    
    // 当 AsyncImage 加载成功后, saveToCache(from:) 将存储图片以供将来使用
    private func saveToCache(from url: URL) {
        Task {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                if let uiImage = UIImage(data: data) {
                    await MainActor.run {
                        ImageCache.shared.setObject(uiImage, forKey: url as NSURL)
                        if cachedImage == nil {
                            cachedImage = uiImage
                        }
                    }
                }
            } catch {
                print("Image caching failed: \\(error)")
            }
        }
    }
    
}

定义完后,使用时像这样:

CachedAsyncImage(
    url: URL(string: "<https://example.com/image.jpg>")!,
    content: { image in
        image
            .resizable()
            .scaledToFit()
    },
    placeholder: {
        ProgressView()
    }
)
.frame(width: 150, height: 150)

通过此实现, 首次加载时会从网络下载图片并缓存。 后续加载时,图片会立即从内存中显示。当内存压力较大时, NSCache 会自动处理内存管理 ,从而显著提升整体性能 ,重复图片的加载时间也显著缩短。这种方法以最小的复杂性提供了直接的性能优势,非常适合大多数 SwiftUI 应用程序。


图像修饰符索引

| --- | --- | --- |

图像尺寸设置