图片(Images)
SwiftUI 图片组件
概述
Image 是 SwiftUI 中用于显示图像的视图。它支持从多种来源加载图像,包括资源目录、SF Symbols、UIKit/AppKit 图像以及自定义绘制。Image 是一个延迟绑定令牌,系统仅在即将使用图像时才解析其实际值。
平台可用性: iOS 13.0+、iPadOS 13.0+、macOS 10.15+、watchOS 6.0+、tvOS 13.0+、visionOS 1.0+
创建图像
从资源加载
最常见的方式是从 Asset Catalog 加载图像:
// 从主 bundle 加载
Image("landscape")
// 从指定 bundle 加载
Image("icon", bundle: .module)系统符号图像
使用 SF Symbols 创建可缩放的符号图像:
// 基础符号
Image(systemName: "star.fill")
// 带变量值的符号(用于进度指示)
Image(systemName: "wifi", variableValue: 0.5)
.foregroundStyle(.blue)装饰性图像
对于纯装饰性图像,使用 decorative 初始化器,辅助功能系统会忽略这些图像:
// 装饰性图像不需要辅助功能标签
Image(decorative: "background-pattern")
// 带变量值的装饰性符号
Image(decorative: "circle.fill", variableValue: 0.75)从平台图像创建
从 UIKit 或 AppKit 图像创建 SwiftUI Image:
// iOS/iPadOS - 从 UIImage
let uiImage = UIImage(named: "photo")!
Image(uiImage: uiImage)
// macOS - 从 NSImage
let nsImage = NSImage(named: "photo")!
Image(nsImage: nsImage)
// 从 CGImage
let cgImage: CGImage = // ...
Image(cgImage, scale: 2.0, orientation: .up, label: Text("Photo"))自定义绘制图像
使用绘制闭包创建自定义图像:
Image(size: CGSize(width: 100, height: 100)) { context in
// 绘制圆形
context.fill(
Path(ellipseIn: CGRect(x: 0, y: 0, width: 100, height: 100)),
with: .color(.blue)
)
// 绘制文本
context.draw(
Text("Custom"),
at: CGPoint(x: 50, y: 50),
anchor: .center
)
}调整图像大小
resizable 修饰符
默认情况下,图像以原始尺寸显示。使用 resizable() 使图像可调整大小:
// 基础可调整大小
Image("landscape")
.resizable()
.frame(width: 300, height: 200)
// 使用 capInsets 保持边缘不变形
Image("button-background")
.resizable(capInsets: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))调整大小模式
ResizingMode 控制图像如何填充空间:
// 拉伸模式(默认)
Image("pattern")
.resizable(resizingMode: .stretch)
.frame(width: 200, height: 200)
// 平铺模式
Image("tile")
.resizable(resizingMode: .tile)
.frame(width: 200, height: 200)保持宽高比
使用 aspectRatio 保持图像的原始宽高比:
// 适应容器(可能留白)
Image("photo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.background(.gray.opacity(0.2))
// 填充容器(可能裁剪)
Image("photo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 300)
.clipped()
// 指定具体宽高比
Image("photo")
.resizable()
.aspectRatio(16/9, contentMode: .fit)便捷缩放方法
// 缩放以适应
Image("photo")
.resizable()
.scaledToFit()
.frame(height: 200)
// 缩放以填充
Image("photo")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipped()渲染行为
渲染模式
控制图像是按原样渲染还是作为模板:
// 原始模式 - 保持原始颜色
Image("colored-icon")
.renderingMode(.original)
// 模板模式 - 使用前景色
Image("icon")
.renderingMode(.template)
.foregroundStyle(.blue)符号渲染模式
SF Symbols 支持多种渲染模式:
// 单色模式
Image(systemName: "heart.fill")
.symbolRenderingMode(.monochrome)
.foregroundStyle(.red)
// 多色模式(使用符号的内置颜色)
Image(systemName: "person.crop.circle.fill.badge.plus")
.symbolRenderingMode(.multicolor)
// 层级模式(使用不透明度层次)
Image(systemName: "square.stack.3d.up.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// 调色板模式(自定义多色)
Image(systemName: "person.crop.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .green, .red)插值质量
控制图像缩放时的插值质量:
// 无插值(像素化效果)
Image("small-icon")
.resizable()
.interpolation(.none)
.frame(width: 200, height: 200)
// 低质量插值
Image("icon")
.resizable()
.interpolation(.low)
// 中等质量(默认)
Image("icon")
.resizable()
.interpolation(.medium)
// 高质量插值
Image("photo")
.resizable()
.interpolation(.high)抗锯齿
控制是否应用抗锯齿:
// 禁用抗锯齿(像素艺术)
Image("pixel-art")
.antialiased(false)
// 启用抗锯齿(默认)
Image("icon")
.antialiased(true)图像缩放
imageScale 修饰符
控制图像相对于文本的缩放:
HStack {
Image(systemName: "star.fill")
.imageScale(.small)
Text("Small")
}
HStack {
Image(systemName: "star.fill")
.imageScale(.medium)
Text("Medium")
}
HStack {
Image(systemName: "star.fill")
.imageScale(.large)
Text("Large")
}环境值
通过环境值设置所有子视图的图像缩放:
VStack {
Image(systemName: "star")
Image(systemName: "heart")
Image(systemName: "bell")
}
.environment(\.imageScale, .large)符号变体
变量值模式
控制符号的变量值如何显示:
// 累积模式(默认)
Image(systemName: "speaker.wave.3", variableValue: 0.5)
.symbolVariableValueMode(.cumulative)
// 迭代模式
Image(systemName: "wifi", variableValue: 0.66)
.symbolVariableValueMode(.iterative)动态变量值
结合动画创建动态效果:
struct WifiIndicator: View {
@State private var signalStrength: Double = 0.0
var body: some View {
Image(systemName: "wifi", variableValue: signalStrength)
.font(.largeTitle)
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever()) {
signalStrength = 1.0
}
}
}
}动态范围
设置动态范围
控制图像的动态范围(HDR 支持):
// 标准动态范围
Image("photo")
.resizable()
.allowedDynamicRange(.standard)
// 高动态范围
Image("hdr-photo")
.resizable()
.allowedDynamicRange(.high)
// 受限高动态范围
Image("photo")
.resizable()
.allowedDynamicRange(.constrainedHigh)AsyncImage
AsyncImage 用于异步加载网络图像,适用于 iOS 15.0+。
基础用法
最简单的异步图像加载:
AsyncImage(url: URL(string: "https://example.com/image.jpg"))
.frame(width: 200, height: 200)自定义占位符和内容
提供自定义占位符和修改加载的图像:
AsyncImage(url: URL(string: "https://example.com/photo.jpg")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))阶段处理
处理加载的不同阶段(空、成功、失败):
AsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in
switch phase {
case .empty:
// 加载中
ProgressView()
case .success(let image):
// 加载成功
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
// 加载失败
Image(systemName: "photo.fill")
.foregroundStyle(.gray)
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)完整示例
带错误处理和重试的异步图像加载器:
struct RemoteImageView: View {
let url: URL?
var body: some View {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ZStack {
Color.gray.opacity(0.1)
ProgressView()
}
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
VStack {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundStyle(.red)
Text("加载失败")
.font(.caption)
}
@unknown default:
EmptyView()
}
}
.frame(width: 300, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}ImageRenderer
ImageRenderer 用于从 SwiftUI 视图创建位图图像,适用于 iOS 16.0+。
基础用法
将视图渲染为 CGImage:
let view = Text("Hello, World!")
.font(.largeTitle)
.foregroundStyle(.blue)
.padding()
let renderer = ImageRenderer(content: view)
// 获取 CGImage
if let cgImage = renderer.cgImage {
// 使用 cgImage
}
// iOS - 获取 UIImage
#if os(iOS)
if let uiImage = renderer.uiImage {
// 保存或分享
}
#endif
// macOS - 获取 NSImage
#if os(macOS)
if let nsImage = renderer.nsImage {
// 保存或分享
}
#endif设置渲染参数
自定义渲染尺寸和缩放:
let renderer = ImageRenderer(content: myView)
// 设置建议尺寸
renderer.proposedSize = ProposedViewSize(width: 400, height: 300)
// 设置缩放比例
renderer.scale = 2.0 // 2x 分辨率自定义渲染
使用渲染闭包进行自定义绘制:
let renderer = ImageRenderer(content: myView)
renderer.render { size, renderFunction in
// 创建 PDF 上下文
var mediaBox = CGRect(origin: .zero, size: size)
guard let pdfContext = CGContext(
URL(fileURLWithPath: "/path/to/output.pdf") as CFURL,
mediaBox: &mediaBox,
nil
) else { return }
pdfContext.beginPDFPage(nil)
renderFunction(pdfContext)
pdfContext.endPDFPage()
pdfContext.closePDF()
}实际应用示例
创建可分享的成就徽章:
struct AchievementBadge: View {
let title: String
let date: Date
var body: some View {
VStack(spacing: 20) {
Image(systemName: "trophy.fill")
.font(.system(size: 80))
.foregroundStyle(.yellow)
.shadow(color: .orange, radius: 10)
Text(title)
.font(.title.bold())
Text(date.formatted(date: .long, time: .omitted))
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(40)
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
struct ContentView: View {
var body: some View {
VStack {
let badge = AchievementBadge(
title: "完成挑战",
date: Date()
)
badge
Button("导出图像") {
exportBadge(badge)
}
}
}
func exportBadge(_ view: some View) {
let renderer = ImageRenderer(content: view)
renderer.scale = 3.0 // 高分辨率
#if os(iOS)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
#endif
}
}图像适配最佳实践
缩放策略
根据需求选择合适的缩放方式:
// 场景1: 头像 - 填充圆形
Image("avatar")
.resizable()
.scaledToFill()
.frame(width: 60, height: 60)
.clipShape(Circle())
// 场景2: 照片预览 - 适应容器
Image("photo")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.frame(height: 300)
// 场景3: 背景图 - 填充并模糊
Image("background")
.resizable()
.scaledToFill()
.ignoresSafeArea()
.blur(radius: 10)裁剪技巧
// 圆角裁剪
Image("photo")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 16))
// 圆形裁剪
Image("avatar")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
// 自定义形状裁剪
Image("photo")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Capsule())平铺模式
使用平铺创建背景图案:
// 小图案平铺
Image("pattern")
.resizable(resizingMode: .tile)
.frame(width: 300, height: 300)
.opacity(0.3)插值选择
根据图像类型选择插值:
// 像素艺术 - 无插值
Image("pixel-art")
.resizable()
.interpolation(.none)
.scaledToFit()
// 照片 - 高质量插值
Image("photo")
.resizable()
.interpolation(.high)
.scaledToFit()
// 图标 - 中等插值
Image("icon")
.resizable()
.interpolation(.medium)
.scaledToFit()辅助功能
控件图像 vs 装饰图像
为控件图像提供标签,装饰图像使用 decorative:
// 控件图像 - 需要标签
Button {
// 操作
} label: {
Image(systemName: "trash")
}
.accessibilityLabel("删除")
// 装饰图像 - 无需标签
HStack {
Image(decorative: "decoration")
Text("内容")
}使用 label 参数
创建图像时提供辅助功能标签:
// 从 CGImage 创建时提供标签
Image(cgImage, scale: 1.0, orientation: .up, label: Text("产品照片"))
// 自定义绘制时提供标签
Image(size: CGSize(width: 100, height: 100), label: Text("自定义图标")) { context in
// 绘制内容
}实际应用示例
响应式图片网格
struct PhotoGrid: View {
let photos = ["photo1", "photo2", "photo3", "photo4", "photo5", "photo6"]
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 200))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(photos, id: \.self) { photo in
Image(photo)
.resizable()
.scaledToFill()
.frame(minWidth: 100, maxWidth: 200)
.frame(height: 150)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding()
}
}
}带加载状态的网络图片
struct NetworkImageView: View {
let url: URL?
var body: some View {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
Rectangle()
.fill(.gray.opacity(0.2))
.overlay {
ProgressView()
}
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.transition(.opacity)
case .failure:
Rectangle()
.fill(.gray.opacity(0.2))
.overlay {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
Text("加载失败")
.font(.caption)
}
.foregroundStyle(.secondary)
}
@unknown default:
EmptyView()
}
}
}
}SF Symbols 动画
struct AnimatedSymbol: View {
@State private var isAnimating = false
var body: some View {
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundStyle(.red)
.symbolEffect(.bounce, value: isAnimating)
.onTapGesture {
isAnimating.toggle()
}
}
}图像滤镜效果
struct FilteredImage: View {
var body: some View {
VStack(spacing: 20) {
// 原图
Image("photo")
.resizable()
.scaledToFit()
.frame(height: 150)
// 灰度
Image("photo")
.resizable()
.scaledToFit()
.frame(height: 150)
.grayscale(1.0)
// 模糊
Image("photo")
.resizable()
.scaledToFit()
.frame(height: 150)
.blur(radius: 5)
// 饱和度
Image("photo")
.resizable()
.scaledToFit()
.frame(height: 150)
.saturation(2.0)
}
}
}性能优化
图像缓存
AsyncImage 自动使用 URLSession 的缓存机制:
// 默认缓存行为
AsyncImage(url: imageURL)
// 自定义 URLSession 配置
let configuration = URLSessionConfiguration.default
configuration.urlCache = URLCache(
memoryCapacity: 50_000_000, // 50 MB
diskCapacity: 100_000_000 // 100 MB
)异步加载策略
// 使用 LazyVStack/LazyHStack 延迟加载
ScrollView {
LazyVStack {
ForEach(imageURLs, id: \.self) { url in
AsyncImage(url: url) { image in
image.resizable().scaledToFit()
} placeholder: {
ProgressView()
}
.frame(height: 200)
}
}
}内存管理
// 对于大图,考虑降采样
Image(uiImage: downsampledImage)
.resizable()
.scaledToFit()
// 辅助函数
func downsample(imageAt url: URL, to size: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
let options = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) * scale
] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) else {
return nil
}
return UIImage(cgImage: image)
}平台差异
iOS/iPadOS
- 完整的 SF Symbols 支持
- 支持 Live Photos
- 支持 HDR 图像显示
- 完整的辅助功能支持
macOS
- 支持 NSImage
- 支持拖放操作
- 支持上下文菜单
- 高分辨率显示支持
watchOS
- 自动优化小屏幕显示
- 限制的 SF Symbols 集
- 内存限制更严格
- 支持圆角显示优化
tvOS
- 优化远距离观看
- 支持焦点效果
- 较大的默认尺寸
- 支持视差效果
visionOS
- 支持 3D 图像
- 空间音频图标
- 深度效果
- 沉浸式图像显示
最佳实践
- 选择合适的图像源: 优先使用 Asset Catalog 管理图像资源
- 使用 SF Symbols: 对于图标,优先使用 SF Symbols 而非自定义图片
- 提供多分辨率: 在 Asset Catalog 中提供 @1x、@2x、@3x 版本
- 合理使用 resizable: 仅在需要调整大小时使用
resizable() - 保持宽高比: 使用
aspectRatio避免图像变形 - 辅助功能: 为控件图像提供标签,装饰图像使用
decorative - 性能优化: 使用 LazyVStack/LazyHStack 延迟加载大量图像
- 错误处理: AsyncImage 应处理加载失败情况
- 缓存策略: 合理配置网络图像缓存
- 内存管理: 对大图进行降采样处理
总结
SwiftUI 的 Image 提供了强大而灵活的图像显示能力,从基础的资源加载到高级的异步加载和图像导出。AsyncImage 简化了网络图像加载,ImageRenderer 则使视图导出变得简单。通过合理使用各种修饰符和渲染模式,可以创建美观、高效且具有良好用户体验的图像界面。掌握这些特性是构建现代 SwiftUI 应用的重要基础。
上次更新于