使用PhotosPicker选择视频并播放时线程崩溃问题求助
问题:选择相册视频后App冻结崩溃,疑似大文件处理问题
我想实现让用户从相册选择视频并在App内播放的功能,参考相关教程后稍作修改,将每个视频保存到唯一URL,但选择视频后App总是冻结且线程崩溃,怀疑是FileManager无法处理大视频文件,附上代码寻求解决方案:
import SwiftUI struct Movie: Transferable { let url: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .movie) { movie in SentTransferredFile(movie.url) } importing: { received in let id = UUID().uuidString let copy1 = URL.documentsDirectory.appending(path: id) let copy = copy1.appending(path: "movie.mov") if FileManager.default.fileExists(atPath: copy.path()) { try FileManager.default.removeItem(at: copy) } try FileManager.default.copyItem(at: received.file, to: copy) return Self.init(url: copy) } } } PhotosPicker( selection: $selectedItems, maxSelectionCount: 3, matching:.any(of:[.videos]), photoLibrary: .shared() ){ Image("videos") .resizable() .frame(width: 35, height: 35) .padding(.trailing, 18) } .onChange(of:selectedItems){oldValue, newValue in Task { for value in newValue { do { loadState = .loading if let movie = try await value.loadTransferable(type: Movie.self) { loadState = .loaded(movie) noteVideoUrls.append(movie.url) } else { loadState = .failed } } catch { loadState = .failed } } } } ForEach(noteVideoUrls, id: \.self){url in ZStack(alignment:.topTrailing){ switch loadState { case .loading: ProgressView() case .loaded: VideoPlayer(player: AVPlayer(url: url)) .scaledToFit() .frame(width: 300, height: 300) case .failed: Text("Import failed") } } }
核心问题分析与修复方案
1. 阻塞主线程的同步操作
FileManager.copyItem(at:to:)是同步耗时操作,处理大视频时会卡住主线程,直接导致App冻结崩溃。需要把文件操作移到后台队列执行。
2. 未创建目标目录
代码中构造了目标目录路径,但没有实际创建目录,直接复制文件会抛出错误,引发崩溃。
3. 状态管理混乱
单个loadState变量对应多个视频,会导致UI状态显示错误,同时无法准确追踪每个视频的加载状态。
修复后的完整代码
import SwiftUI import PhotosUI import AVKit struct Movie: Transferable { let url: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .movie) { movie in SentTransferredFile(movie.url) } importing: { received in // 后台队列执行文件操作,避免阻塞主线程 return try await Task.detached(priority: .background) { let id = UUID().uuidString let targetDir = URL.documentsDirectory.appending(path: id) let targetUrl = targetDir.appending(path: "movie.mov") // 确保目标目录存在 try FileManager.default.createDirectory(at: targetDir, withIntermediateDirectories: true) // 清理已存在的旧文件 if FileManager.default.fileExists(atPath: targetUrl.path()) { try FileManager.default.removeItem(at: targetUrl) } // 复制文件 try FileManager.default.copyItem(at: received.file, to: targetUrl) return Self.init(url: targetUrl) }.value } } } // 为每个视频单独维护加载状态 struct VideoLoadState: Identifiable { let id = UUID() var state: LoadState let url: URL? enum LoadState { case idle, loading, loaded, failed } } struct ContentView: View { @State private var selectedItems: [PhotosPickerItem] = [] @State private var videoLoadStates: [VideoLoadState] = [] var body: some View { VStack { PhotosPicker( selection: $selectedItems, maxSelectionCount: 3, matching: .videos, photoLibrary: .shared() ) { Image("videos") .resizable() .frame(width: 35, height: 35) .padding(.trailing, 18) } .onChange(of: selectedItems) { oldValue, newValue in // 过滤已处理的视频,避免重复加载 let unprocessedItems = newValue.filter { item in !videoLoadStates.contains { state in guard let stateUrl = state.url else { return false } return stateUrl.lastPathComponent.contains(item.itemIdentifier ?? "") } } for item in unprocessedItems { // 添加初始加载状态 let newState = VideoLoadState(state: .loading, url: nil) videoLoadStates.append(newState) Task { let stateId = newState.id do { if let movie = try await item.loadTransferable(type: Movie.self) { // 更新对应视频的加载状态为已完成 if let index = videoLoadStates.firstIndex(where: { $0.id == stateId }) { videoLoadStates[index] = VideoLoadState(state: .loaded, url: movie.url) } } else { updateState(for: stateId, to: .failed) } } catch { updateState(for: stateId, to: .failed) } } } } ScrollView { VStack(spacing: 20) { ForEach(videoLoadStates) { state in ZStack(alignment: .topTrailing) { switch state.state { case .loading: ProgressView() .frame(width: 300, height: 300) case .loaded: if let url = state.url { VideoPlayer(player: AVPlayer(url: url)) .scaledToFit() .frame(width: 300, height: 300) } case .failed: Text("导入失败") .frame(width: 300, height: 300) .background(Color.gray.opacity(0.2)) case .idle: EmptyView() } } } } } } } private func updateState(for stateId: UUID, to newState: VideoLoadState.LoadState) { if let index = videoLoadStates.firstIndex(where: { $0.id == stateId }) { videoLoadStates[index].state = newState } } }
关键修复点说明
- 后台文件操作:使用
Task.detached(priority: .background)将文件复制、目录创建等耗时操作移到后台队列,彻底避免主线程阻塞。 - 目录创建校验:添加
createDirectory方法确保目标目录存在,防止复制文件时抛出路径不存在的错误。 - 独立状态管理:为每个视频创建单独的
VideoLoadState实例,确保UI状态与单个视频一一对应,避免状态混乱。 - 重复加载过滤:通过
itemIdentifier判断视频是否已处理,防止重复加载同一视频导致的资源浪费。
内容的提问来源于stack exchange,提问作者Tracy9988 Xu
相关产品推荐
相关产品推荐

