如何实现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
相关产品推荐
相关产品推荐

