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

如何实现iOS相册APP媒体数据的动态更新?

问题描述

我开发了一款相册APP,初始可获取手机中的媒体数据,但当设备新增照片后,需重新安装APP或在viewWillAppear中重新获取(仅临时方案)才能看到新内容,希望实现媒体数据随手机内容动态更新。

相关代码

枚举定义

import UIKit
import Foundation
import Photos
import CoreData

enum MediaType{
case video, photo
}

ViewModel 代码

class GalleryPageViewModel : BaseViewModel{
    var mediaArray = Spectator<MediaArray?>(value: [])
    var title = "Gallery"
    var appDelegate:AppDelegate?
    var context : NSManagedObjectContext?
    let responder : AppResponder
    var tempMedia = [MediaModel]()
    
    init(with responder:AppResponder){
        self.responder = responder
        super.init()
    }
}

extension GalleryPageViewModel{
    func populatePhotos(){
        mediaArray.value = []
        tempMedia = []
        PHPhotoLibrary.requestAuthorization{ [weak self] status in
            if status == .authorized{
                let assets = PHAsset.fetchAssets(with: nil)
                assets.enumerateObjects{ (object,_,_) in
                    var image = UIImage()
                    image = self!.conversionPhotoToImage(asset: object)
                    var mediaType : MediaType = .photo
                    if object.mediaType == .video{
                        mediaType = .video
                    }
                    let media = MediaModel(img: image, asset: object, mediaType: mediaType)
                    self?.tempMedia.append(media)
                }
            }
            else{
                print("auth failed")
            }
            self?.setUpData()

        }
    }
    
    func conversionPhotoToImage(asset : PHAsset) -> UIImage{
        let manager = PHImageManager.default()
        var media = UIImage()
        let requestOptions=PHImageRequestOptions()
        requestOptions.isSynchronous=true
        requestOptions.deliveryMode = .highQualityFormat
        
        manager.requestImage(for: asset, targetSize: CGSize(width: 1000, height: 1000), contentMode:.aspectFill, options: requestOptions) { image,_ in
            media = image!
        }
        return media
    }
    
    func setUpData(){
        self.mediaArray.value = tempMedia
    }
}

extension GalleryPageViewModel{
    func numberOfItemsInSection() -> Int{
        return mediaArray.value?.count ?? 0
    }
    
    func didSelectAtRow(index:IndexPath){
        self.responder.pushToDetailVc(data: mediaArray.value![index.row])
    }
}

extension GalleryPageViewModel{
    func setUpCoreData(){
        appDelegate = UIApplication.shared.delegate as! AppDelegate
        context = appDelegate?.persistentContainer.viewContext
    }
    
    func storeInCoreData(){
        let entity = NSEntityDescription.entity(forEntityName: "MediaData", in: context!)
        let newUser = NSManagedObject(entity: entity!, insertInto: context)
        var i = 1
        for data in mediaArray.value!{
            newUser.setValue(data.img, forKey: "image")
            newUser.setValue(data.duration, forKey: "duration")
            newUser.setValue("data", forKey: "mediaType")
            newUser.setValue(data.location, forKey: "location")
            newUser.setValue(data.creationDate, forKey: "creationDate")
            var id = String()
            if data.mediaType == .photo{
                id = "imgEn\(i)"
            }
            else{
                id = "vidEn\(i)"
            }
            i+=1
            newUser.setValue(id, forKey: "id")
        }
    }
}

ViewController 代码

import UIKit
import Photos

class GalleryPageViewController: BaseViewController {
    
    var galleryCollectionView : UICollectionView?
    
    var galleryCollectionViewFlowLayout : UICollectionViewFlowLayout? = nil
    
    var viewModel: GalleryPageViewModel? {
        didSet {
            self.bindViewModel()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = viewModel?.title
        
        view.backgroundColor = .systemBackground
        setUpFlowLayout()
        galleryCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: galleryCollectionViewFlowLayout!)
        galleryCollectionView?.register(MediaCollectionViewCell.self, forCellWithReuseIdentifier: "cell")

        setUpCollectionView()
        viewConstraintsForGalleryPage()
        viewModel?.populatePhotos()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
}

// MARK: - UI WORKS
extension GalleryPageViewController{
    func setUpFlowLayout(){
        galleryCollectionViewFlowLayout = UICollectionViewFlowLayout()
        galleryCollectionViewFlowLayout?.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
        galleryCollectionViewFlowLayout?.itemSize = CGSize(width: (view.frame.width/2) - 20, height: (self.view.frame.width/2)-10)
    }
    func setUpCollectionView(){
        galleryCollectionView?.dataSource = self
        galleryCollectionView?.delegate = self
    }
    
    func viewConstraintsForGalleryPage(){
        view.addSubview(galleryCollectionView!)
        
        galleryCollectionView?.translatesAutoresizingMaskIntoConstraints = false
        galleryCollectionView?.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,constant: 10).isActive = true
        galleryCollectionView?.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
        galleryCollectionView?.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,constant: 10).isActive = true
        galleryCollectionView?.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
    }
}

//MARK: - COLLECTION VIEW DELEGATE AND DATA SOURCE
extension GalleryPageViewController : UICollectionViewDelegate{
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        viewModel?.didSelectAtRow(index: indexPath)
    }
}

extension GalleryPageViewController : UICollectionViewDataSource{
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return viewModel?.numberOfItemsInSection() ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCollectionViewCell
        let cellData = viewModel?.mediaArray.value?[indexPath.row]
        cell.imageView.image = cellData?.img
        if cellData?.mediaType == .video{
            cell.playBtn.isHidden = false
            cell.durationLabel.isHidden = false
            let manager = PHImageManager.default()
            manager.requestAVAsset(forVideo: (cellData?.asset)!, options: nil){asset,_,_ in
                let duration = asset?.duration
                let durationTime = CMTimeGetSeconds(duration!)
                let finalTime = durationTime/60
                
                DispatchQueue.main.async {
                    cell.durationLabel.text = "\(Int(finalTime)) : \(Int(durationTime) % 60)"
                }
            }
        }
        else{
            cell.playBtn.isHidden = true
            cell.durationLabel.isHidden = true
        }
        return cell
    }
}

//MARK: - BINDING VARIABLES
extension GalleryPageViewController{
    private func bindViewModel(){
        viewModel?.mediaArray.bind { [weak self] _ in
            print("i am gonna reload")
            print(self?.viewModel?.mediaArray.value?.isEmpty)
            DispatchQueue.main.async {
                self?.galleryCollectionView?.reloadData()
            }
        }
    }
}
解决方案

要实现相册内容动态更新,核心是利用Photos框架的照片库变更监听机制,具体实现步骤如下:

1. 让ViewModel监听照片库变更

让GalleryPageViewModel遵循PHPhotoLibraryChangeObserver协议,并实现回调方法:

extension GalleryPageViewModel: PHPhotoLibraryChangeObserver {
    func photoLibraryDidChange(_ changeInstance: PHChange) {
        // 收到变更通知后重新拉取媒体数据
        populatePhotos()
    }
}

2. 注册与销毁观察者

在ViewModel的初始化和销毁阶段,完成观察者的注册与移除,避免内存泄漏:

class GalleryPageViewModel : BaseViewModel{
    // ... 原有代码 ...
    
    init(with responder:AppResponder){
        self.responder = responder
        super.init()
        // 注册照片库变更观察者
        PHPhotoLibrary.shared().register(self)
    }
    
    deinit {
        // 销毁时移除观察者
        PHPhotoLibrary.shared().unregisterChangeObserver(self)
    }
}

3. 优化数据刷新的线程处理

修改populatePhotos方法,将耗时的图片转换操作放到子线程,主线程更新UI:

func populatePhotos(){
    mediaArray.value = []
    tempMedia = []
    PHPhotoLibrary.requestAuthorization{ [weak self] status in
        guard let self = self else { return }
        if status == .authorized{
            let assets = PHAsset.fetchAssets(with: nil)
            DispatchQueue.global().async {
                assets.enumerateObjects{ (object,_,_) in
                    let image = self.conversionPhotoToImage(asset: object)
                    let mediaType : MediaType = object.mediaType == .video ? .video : .photo
                    let media = MediaModel(img: image, asset: object, mediaType: mediaType)
                    self.tempMedia.append(media)
                }
                // 回到主线程更新数据
                DispatchQueue.main.async {
                    self.setUpData()
                }
            }
        }
        else{
            print("auth failed")
        }
    }
}

4. 修复图片转换的潜在崩溃

修改conversionPhotoToImage方法,避免强制解包导致崩溃:

func conversionPhotoToImage(asset : PHAsset) -> UIImage{
    let manager = PHImageManager.default()
    var media = UIImage()
    let requestOptions=PHImageRequestOptions()
    requestOptions.isSynchronous=true
    requestOptions.deliveryMode = .highQualityFormat
    
    manager.requestImage(for: asset, targetSize: CGSize(width: 1000, height: 1000), contentMode:.aspectFill, options: requestOptions) { image,_ in
        if let img = image {
            media = img
        }
    }
    return media
}

5. 修复CoreData存储的逻辑错误

原storeInCoreData方法中循环复用同一个NSManagedObject实例,会导致数据覆盖,修改为每个媒体数据创建独立实例:

func storeInCoreData(){
    guard let context = context, let mediaArray = mediaArray.value else { return }
    var i = 1
    for data in mediaArray{
        let entity = NSEntityDescription.entity(forEntityName: "MediaData", in: context)!
        let newMedia = NSManagedObject(entity: entity, insertInto: context)
        newMedia.setValue(data.img, forKey: "image")
        newMedia.setValue(data.duration, forKey: "duration")
        newMedia.setValue(data.mediaType == .photo ? "photo" : "video", forKey: "mediaType")
        newMedia.setValue(data.location, forKey: "location")
        newMedia.setValue(data.creationDate, forKey: "creationDate")
        let id = data.mediaType == .photo ? "imgEn\(i)" : "vidEn\(i)"
        newMedia.setValue(id, forKey: "id")
        i+=1
    }
    // 保存上下文
    do {
        try context.save()
    } catch {
        print("CoreData保存失败: \(error)")
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 11:15:38