SwiftUIViews

图片(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 图像
  • 空间音频图标
  • 深度效果
  • 沉浸式图像显示

最佳实践

  1. 选择合适的图像源: 优先使用 Asset Catalog 管理图像资源
  2. 使用 SF Symbols: 对于图标,优先使用 SF Symbols 而非自定义图片
  3. 提供多分辨率: 在 Asset Catalog 中提供 @1x、@2x、@3x 版本
  4. 合理使用 resizable: 仅在需要调整大小时使用 resizable()
  5. 保持宽高比: 使用 aspectRatio 避免图像变形
  6. 辅助功能: 为控件图像提供标签,装饰图像使用 decorative
  7. 性能优化: 使用 LazyVStack/LazyHStack 延迟加载大量图像
  8. 错误处理: AsyncImage 应处理加载失败情况
  9. 缓存策略: 合理配置网络图像缓存
  10. 内存管理: 对大图进行降采样处理

总结

SwiftUI 的 Image 提供了强大而灵活的图像显示能力,从基础的资源加载到高级的异步加载和图像导出。AsyncImage 简化了网络图像加载,ImageRenderer 则使视图导出变得简单。通过合理使用各种修饰符和渲染模式,可以创建美观、高效且具有良好用户体验的图像界面。掌握这些特性是构建现代 SwiftUI 应用的重要基础。

在 GitHub 上编辑

上次更新于