如何异步构建SwiftUI视图以解决照片加载UI卡顿问题
解决SwiftUI中异步构建堆叠照片视图的UI卡顿问题
你的卡顿问题核心是在主线程同步加载PHAsset缩略图——每次构建myImageStack时,都会同步调用getAssetThumbnail(),这会阻塞UI线程,尤其是照片数量多的时候。下面是具体的解决方案,既保留堆叠效果,又实现异步加载避免冻结:
核心思路
- 将缩略图加载逻辑移到后台线程,完成后再更新UI
- 用状态对象缓存已加载的缩略图,避免重复请求
- 主视图异步初始化照片数据,先显示加载状态再展示内容
修改后的代码
1. 自定义缩略图加载器(管理异步加载与缓存)
import SwiftUI import Photos class PhotoThumbnailLoader: ObservableObject { @Published var thumbnails: [String: UIImage] = [:] func loadThumbnail(for asset: PHAsset) { // 避免重复加载已缓存的缩略图 guard thumbnails[asset.localIdentifier] == nil else { return } Task { let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.isSynchronous = false options.resizeMode = .fast PHImageManager.default().requestImage( for: asset, targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFit, options: options ) { [weak self] image, _ in guard let image = image, let self = self else { return } DispatchQueue.main.async { self.thumbnails[asset.localIdentifier] = image } } } } }
2. 重构图片栈视图(异步加载+占位符)
struct MyImageStack: View { let resultsPhotos: PHFetchResult<PHAsset> @StateObject private var loader = PhotoThumbnailLoader() var body: some View { let collection = PHFetchResultCollection(fetchResult: resultsPhotos) ZStack { ForEach(collection, id: \.localIdentifier) { asset in if let thumbnail = loader.thumbnails[asset.localIdentifier] { Image(uiImage: thumbnail) .resizable() .scaledToFit() .border(Color.white, width: 10) .shadow(color: Color.black.opacity(0.2), radius: 5, x: 5, y: 5) .frame(width: 200, height: 200) .rotationEffect(.degrees(Double.random(in: -25...25))) } else { // 占位符:灰色背景,加载时显示 RoundedRectangle(cornerRadius: 8) .fill(Color.gray.opacity(0.3)) .frame(width: 200, height: 200) .onAppear { loader.loadThumbnail(for: asset) } } } } } }
3. 主视图异步初始化照片数据
struct PhotosOverviewView: View { @State private var resultPhotos: [String: PHFetchResult<PHAsset>] = [:] @State private var isLoading = true var body: some View { NavigationStack { if isLoading { ProgressView("加载照片中...") .onAppear { loadPhotosAsync() } } else { ScrollView(.vertical) { VStack(alignment: .center) { ForEach(Array(resultPhotos.keys).sorted(), id: \.self) { key in NavigationLink(destination: PhotosYearView(year: key, resultsPhotos: resultPhotos[key]!)) { MyImageStack(resultsPhotos: resultPhotos[key]!) .padding() } } } .frame(maxWidth: .infinity) } } } } private func loadPhotosAsync() { Task { let photosModel = PhotosModel() // 异步请求权限 await PHPhotoLibrary.requestAuthorization(for: .readWrite) // 获取目标日期的照片 let fetchedPhotos = photosModel.getAllPhotosOnDay(monthAbbr: "Feb", day: 16) // 回到主线程更新状态 DispatchQueue.main.async { self.resultPhotos = fetchedPhotos self.isLoading = false } } } }
关键优化点
- 异步加载隔离:缩略图请求放在后台线程,不会阻塞主线程,UI始终保持响应
- 缓存机制:已加载的缩略图存在
thumbnails字典里,滚动时不会重复请求 - 加载状态反馈:主视图先显示进度条,避免用户看到空白或冻结的界面
- 保留堆叠效果:每个图片加载完成后自动替换占位符,随机旋转的视觉效果完全保留
可选优化方向
如果照片数量特别多,可以进一步优化:
- 限制同时加载的缩略图数量(比如用信号量控制并发数)
- 对ZStack底层的图片延迟加载(比如只加载前N层,滚动到视图时再加载底层)
- 降低缩略图的目标尺寸(如果视觉效果允许的话)
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

