You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 21:37:09