SwiftUI ScrollView结合SwiftData存照片时卡顿问题求助
SwiftUI照片列表滚动卡顿问题排查与优化
问题背景
实现了一个SwiftUI照片管理视图,核心功能包括:
- 通过PhotosPicker选图并存储到SwiftData
- 支持长按选中照片、批量选择/删除操作
- 使用LazyVGrid在ScrollView中展示照片网格
当照片数量较多时,ScrollView出现滚动卡顿,暂未明确触发卡顿的具体阈值。
相关代码
ContentView
struct ContentView: View { @Environment(\.modelContext) private var context @Query private var photoItems: [PhotoItem] static private let numberOfColumns: Int = UIDevice.current.userInterfaceIdiom == .phone ? 3 : 7 private var columns: [GridItem] = Array(repeating: GridItem(.flexible()), count: numberOfColumns) @State private var isPhotosPickerPresented: Bool = false @State private var photosPickerPickedItems: [PhotosPickerItem] = [] @State private var isEditing: Bool = false @State private var allSelected: Bool = false @State private var numberOfSelectedItems: Int = 0 var body: some View { ScrollView { LazyVGrid(columns: columns) { ForEach(photoItems) { photoItem in PhotoItemCellView(photoItem: photoItem) .listRowSeparator(.hidden) .listRowBackground(Color.clear) .onTapGesture(perform: { if isEditing { photoItem.isSelected.toggle() numberOfSelectedItems += photoItem.isSelected ? 1 : -1 return } print("open photo") }) .simultaneousGesture( LongPressGesture() .onEnded({ _ in photoItem.isSelected = true numberOfSelectedItems += 1 }) ) } } } .onChange(of: numberOfSelectedItems, { oldValue, newValue in isEditing = newValue > 0 allSelected = newValue == photoItems.count }) .onChange(of: photoItems, { oldValue, newValue in numberOfSelectedItems = newValue.filter({ $0.isSelected }).count }) //MARK: - Photo Picker .photosPicker(isPresented: $isPhotosPickerPresented, selection: $photosPickerPickedItems, matching: .any(of: [.images]), preferredItemEncoding: .compatible, photoLibrary: .shared()) .onChange(of: photosPickerPickedItems, { oldValue, newValue in guard !newValue.isEmpty else {return} for item in newValue { Task { if let data = try? await item.loadTransferable(type: Data.self) { let photoItem = PhotoItem(data: data) savePhotoItem(photoItem) } } } photosPickerPickedItems.removeAll() }) //MARK: - Toolbar .toolbar { if isEditing { ToolbarItem(placement: .topBarTrailing){ Button { deleteSelectedPhotos() } label: { Image(systemName: "trash") } } ToolbarItem(placement: .topBarLeading){ Button { toggleSelection(selected: !allSelected) } label: { Text(allSelected ? "Deselect all" : "Select all") } } } ToolbarItem(placement: .topBarTrailing){ Button { isPhotosPickerPresented = true } label: { Image(systemName: "photo.badge.plus.fill") } } } //MARK: - Content Unavailable .overlay { if photoItems.isEmpty { ContentUnavailableView { Label("No Photos", systemImage: "photo.badge.plus") } description: { Text("Start adding photos & videos to see your list.") } actions: { Button { isPhotosPickerPresented = true } label: { Text("Add Photos & Videos") } } } } } private func savePhotoItem(_ item: PhotoItem) { context.insert(item) } private func deleteSelectedPhotos() { photoItems.filter(\.isSelected).forEach(context.delete) } private func toggleSelection(selected: Bool) { photoItems.forEach { $0.isSelected = selected } numberOfSelectedItems = selected ? photoItems.count : 0 } }
PhotoItemCellView
struct PhotoItemCellView: View { @Bindable var photoItem: PhotoItem @State private var image: UIImage = UIImage(systemName: "photo.artframe")! var body: some View { Image(uiImage: image) .resizable() .aspectRatio(1, contentMode: .fit) .overlay(alignment: .bottomTrailing) { if photoItem.isSelected { Image(systemName: "checkmark.circle.fill") .resizable() .frame(width: 25, height: 25) .foregroundStyle(.white, .green) .padding([.bottom, .trailing], 8) } } .onAppear { if let loadedImage = UIImage(data: photoItem.data) { image = loadedImage } } } }
PhotoItem
@Model class PhotoItem { let data: Data @Attribute(.ephemeral) var isSelected: Bool = false init(data: Data) { self.data = data } }
临时优化发现
移除.toolbar修饰符后,ScrollView的滚动性能得到明显提升。
进一步优化建议
- 图片异步解码与缓存:当前在
onAppear中直接从Data加载UIImage,大图片会阻塞主线程。建议使用异步解码,配合ImageRenderer处理图片加载,避免主线程卡顿。 - 减少视图刷新:当前
numberOfSelectedItems的更新可能触发不必要的视图重绘,可考虑直接从photoItems计算选中数量,避免冗余状态维护。 - Toolbar优化:尝试将Toolbar拆分为独立视图组件,或使用
ToolbarItemGroup减少条件渲染的开销,避免每次状态变化都重建Toolbar。 - SwiftData查询优化:给
@Query添加排序规则,避免数据无序导致的视图频繁更新;同时考虑分页加载大量照片,减少一次性渲染的视图数量。
内容的提问来源于stack exchange,提问作者Sameh Salama
相关产品推荐
相关产品推荐

