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

SwiftUI中如何修改代码同时访问相册的图片与视频?

问题:相册清理应用如何同时访问图片和视频资产

我正在开发一款相册清理应用,但在访问设备相册中的视频时遇到了瓶颈。以下是我用于访问和编辑图片的代码:

import Foundation
import Photos

class PhotosModel: ObservableObject {
    @Published var errorString : String = ""
    @Published var photoArray: [BetterUIImage] = []
    
    private var currentIndex = 0
    private var totalInitialPhotos = 0
    
    private var photosToDelete = [PHAsset]()
    
    init() {
        PHPhotoLibrary.requestAuthorization { (status) in
            DispatchQueue.main.async {
                switch status {
                case .authorized, .limited:
                    self.errorString = ""
                    self.getInitialPhotos()
                case .denied, .restricted:
                    self.errorString = "Photo access permission denied"
                case .notDetermined:
                    self.errorString = "Photo access permission not determined"
                @unknown default:
                    fatalError()
                }
            }
        }
    }
    
    fileprivate func getInitialPhotos() {
        let manager = PHImageManager.default()
        let requestOptions = PHImageRequestOptions()
        requestOptions.isSynchronous = false
        requestOptions.deliveryMode = .highQualityFormat
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        
        let results: PHFetchResult = PHAsset.fetchAssets(with: fetchOptions)
        totalInitialPhotos = results.count
        
        if results.count > 2 {
            for i in 0..<3 {
                let asset = results.object(at: i)
                let size = CGSize(width: 700, height: 700) //You can change size here
                manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { (image, _) in
                    DispatchQueue.main.async {
                        if let image = image {
                            let betterImage = BetterUIImage(image: image, asset: asset)
                            self.photoArray.append(betterImage)
                        } else {
                            print("error asset to image")
                        }
                    }
                }
            }
            currentIndex = 3
        } else if results.count > 0 {
            for i in 0..<results.count {
                let asset = results.object(at: i)
                let size = CGSize(width: 700, height: 700) //You can change size here
                manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { (image, _) in
                    DispatchQueue.main.async {
                        if let image = image {
                            let betterImage = BetterUIImage(image: image, asset: asset)
                            self.photoArray.append(betterImage)
                        } else {
                            print("error asset to image")
                        }
                    }
                }
            }
        } else {
            DispatchQueue.main.async {
                self.errorString = "No photos to display"
            }
        }
        self.photoArray = self.photoArray.reversed()
        print("getAllPhotos() completed")
    }
    
    func appendToPhotosToDelete(_ asset: PHAsset) {
        photosToDelete.append(asset)
        print("appendToPhotosToDelete() completed")
    }
    
    fileprivate func getNextPhoto() {
        let manager = PHImageManager.default()
        let requestOptions = PHImageRequestOptions()
        requestOptions.isSynchronous = false
        requestOptions.deliveryMode = .highQualityFormat
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        
        // TODO: Set change with .image to with fetchOptions: Gets all types
        let results: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
        
        // TODO: Change photoArray so that it imcludes all three types
        self.photoArray = photoArray.shiftRight()
        
        let asset = results.object(at: currentIndex)
        let size = CGSize(width: 700, height: 700) //You can change size here
        
        // TODO: Make this so that it applies to Images, Lives, and Videos
        manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { (image, _) in
            DispatchQueue.main.async {
                if let image = image {
                    self.photoArray.removeLast()
                    self.photoArray.append(BetterUIImage(image: image, asset: asset))
                } else {
                    print("error asset to image")
                }
            }
        }
    }
    
    func next() {
        if currentIndex == totalInitialPhotos {
            self.photoArray = []
            return
        } else if currentIndex == totalInitialPhotos - 1 {
            self.photoArray.removeLast()
        } else if currentIndex == totalInitialPhotos - 2 {
            self.photoArray.removeLast()
        }
        
        getNextPhoto()
        
        currentIndex += 1
        print(currentIndex)
    }
    
    func deletePhoto() {
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.deleteAssets(NSArray(array: self.photosToDelete))
        })
    }
}

请问是否可以修改现有代码以同时返回图片和视频?我曾尝试将PHAsset.fetchAssets(with: .image, options: fetchOptions)改为PHAsset.fetchAssets(with: fetchOptions),但似乎没有效果,不过也没有报错。

有没有其他实现方式?我见过其他类似的应用,但网上找到的资料大多使用PhotosPicker,这不符合我的应用需求。


解决方案

你遇到的核心问题是:视频资产无法通过requestImage方法稳定获取有效图像,即便修改fetchAssets获取所有类型,视频对应的回调里image会为nil,导致无法加入数组。以下是具体修改方案:

1. 定义通用媒体类型替代BetterUIImage

首先需要一个能同时承载图片和视频缩略图的类型,替换现有的BetterUIImage:

enum MediaType {
    case image
    case video
}

struct MediaItem: Identifiable {
    let id = UUID()
    let thumbnail: UIImage
    let asset: PHAsset
    let type: MediaType
}

然后将PhotosModel里的数组类型替换:

@Published var mediaArray: [MediaItem] = []

2. 封装通用缩略图请求方法

统一处理图片和视频的缩略图获取,避免重复逻辑:

private func requestThumbnail(for asset: PHAsset, targetSize: CGSize, completion: @escaping (UIImage?) -> Void) {
    let manager = PHImageManager.default()
    let options = PHImageRequestOptions()
    options.isSynchronous = false
    options.deliveryMode = .highQualityFormat
    options.resizeMode = .exact
    
    switch asset.mediaType {
    case .image:
        manager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, _ in
            completion(image)
        }
    case .video:
        // 直接用requestImage获取视频封面图,满足大部分场景需求
        manager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, _ in
            completion(image)
        }
    default:
        completion(nil)
    }
}

3. 修改初始化资产获取逻辑

更新getInitialPhotos,确保只获取图片和视频,并用新的通用方法请求缩略图:

fileprivate func getInitialPhotos() {
    let fetchOptions = PHFetchOptions()
    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
    // 明确过滤出图片和视频,避免获取音频等其他类型
    fetchOptions.predicate = NSPredicate(format: "mediaType == %d || mediaType == %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue)
    
    let results: PHFetchResult = PHAsset.fetchAssets(with: fetchOptions)
    totalInitialPhotos = results.count
    
    if results.count == 0 {
        DispatchQueue.main.async {
            self.errorString = "No media to display"
        }
        return
    }
    
    let fetchCount = min(results.count, 3)
    for i in 0..<fetchCount {
        let asset = results.object(at: i)
        let size = CGSize(width: 700, height: 700)
        
        requestThumbnail(for: asset, targetSize: size) { [weak self] image in
            guard let self = self, let image = image else {
                print("Failed to get thumbnail for asset \(asset.localIdentifier)")
                return
            }
            DispatchQueue.main.async {
                let mediaType: MediaType = asset.mediaType == .image ? .image : .video
                let mediaItem = MediaItem(thumbnail: image, asset: asset, type: mediaType)
                self.mediaArray.append(mediaItem)
                // 异步请求完成后统一反转数组保证顺序
                if self.mediaArray.count == fetchCount {
                    self.mediaArray = self.mediaArray.reversed()
                }
            }
        }
    }
    currentIndex = fetchCount
    print("getInitialPhotos() completed")
}

4. 更新分页加载逻辑

修改getNextPhoto,适配新的媒体类型和请求方法:

fileprivate func getNextPhoto() {
    let fetchOptions = PHFetchOptions()
    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
    fetchOptions.predicate = NSPredicate(format: "mediaType == %d || mediaType == %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue)
    
    let results: PHFetchResult = PHAsset.fetchAssets(with: fetchOptions)
    guard currentIndex < results.count else { return }
    
    self.mediaArray = mediaArray.shiftRight()
    
    let asset = results.object(at: currentIndex)
    let size = CGSize(width: 700, height: 700)
    
    requestThumbnail(for: asset, targetSize: size) { [weak self] image in
        guard let self = self, let image = image else {
            print("Failed to get thumbnail for asset \(asset.localIdentifier)")
            return
        }
        DispatchQueue.main.async {
            self.mediaArray.removeLast()
            let mediaType: MediaType = asset.mediaType == .image ? .image : .video
            self.mediaArray.append(MediaItem(thumbnail: image, asset: asset, type: mediaType))
        }
    }
}

5. 其他细节调整

  • 将所有原来使用photoArray的地方替换为mediaArray(比如next()方法中的数组操作)
  • 删除逻辑无需修改,PHAssetChangeRequest.deleteAssets支持删除任意类型的PHAsset

为什么之前的修改没效果?

你只修改了fetchAssets的调用,但没有处理视频资产的缩略图请求逻辑。部分视频资产用requestImage无法返回有效图像,导致回调中image为nil,无法加入原有的photoArray。上述方案通过明确判断媒体类型、统一处理缩略图请求,解决了这个核心问题。

内容的提问来源于stack exchange,提问作者garrett

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 00:35:04