iOS开发:如何在UIImagePickerController前端显示加载动画?
解决UIImagePickerController上层加载动画不显示及保存卡顿问题
问题根源分析
你碰到的两个核心问题其实是关联的:
- 加载动画无法显示在Picker前端:UIImagePickerController的视图层级由系统严格管理,直接给它的
view加子视图很容易被系统自带的控件覆盖;更关键的是,你原来的保存操作在主线程执行,导致主线程被阻塞,动画根本没机会完成渲染。 - 保存图片时屏幕卡顿:图片旋转、压缩、写入文件/相册都是耗时任务,放在主线程会阻塞UI刷新,自然会出现卡顿。
解决方案
1. 用官方推荐的cameraOverlayView添加加载动画
cameraOverlayView是UIImagePickerController专门提供的上层自定义视图容器,不管是相机还是相册模式,都能确保你的加载动画显示在最前端,完全不用纠结视图层级的问题。
2. 将耗时的保存操作移到后台线程
用DispatchQueue.global().async把图片处理、保存的逻辑放到后台队列执行,主线程只负责UI操作(显示/隐藏动画、更新ImageView、关闭Picker),从根源解决卡顿。
修改后的完整代码示例
class SinglePageViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITextFieldDelegate, UINavigationBarDelegate { var spinner: UIActivityIndicatorView? // 统一创建带加载动画的Picker实例 private func createImagePicker(sourceType: UIImagePickerController.SourceType) -> UIImagePickerController { let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = sourceType imagePicker.allowsEditing = false // 创建加载动画并添加到overlayView let screenSize = UIScreen.main.bounds spinner = UIActivityIndicatorView(frame: CGRect(x: screenSize.width/2 - 50, y: screenSize.height/2 - 50, width: 100, height: 100)) spinner?.style = .large spinner?.color = .red spinner?.isHidden = true // 初始化overlayView并添加动画 let overlayView = UIView(frame: screenSize) overlayView.addSubview(spinner!) imagePicker.cameraOverlayView = overlayView return imagePicker } lazy var showCameraImagePickerController: UIImagePickerController = { createImagePicker(sourceType: .camera) }() lazy var showPhotoImagePickerController: UIImagePickerController = { createImagePicker(sourceType: .photoLibrary) }() @IBOutlet weak var photoButton: UIButton! @IBAction func onPhotoButton(_ sender: Any) { present(showCameraImagePickerController, animated: true) } @IBOutlet weak var galleryButton: UIButton! @IBAction func onGalleryButton(_ sender: Any) { present(showPhotoImagePickerController, animated: true) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { // 立即显示加载动画 spinner?.isHidden = false spinner?.startAnimating() // 获取选中的原始图片 guard let image = info[.originalImage] as? UIImage else { hideSpinnerAndDismiss(picker: picker) return } let imageName = "\(titleLabel.text!).png" // 后台执行耗时的保存操作 DispatchQueue.global().async { [weak self] in guard let self = self else { return } var saveSuccess = false // 保存到文档目录 if self.saveImage(imageName: imageName, image: image) { saveSuccess = true // 相机拍摄的图片额外保存到相册 if picker.sourceType == .camera { UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil) } } // 回到主线程更新UI DispatchQueue.main.async { if saveSuccess { self.imageView.image = image print("saved \(imageName)") } else { print("error while saving") } self.hideSpinnerAndDismiss(picker: picker) } } } @objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) { if let error = error { print("Save to album failed: \(error.localizedDescription)") } else { print("Saved to album successfully") } } // 封装隐藏动画和关闭Picker的逻辑 private func hideSpinnerAndDismiss(picker: UIImagePickerController) { spinner?.stopAnimating() spinner?.isHidden = true picker.dismiss(animated: true) } func saveImage(imageName: String, image: UIImage?) -> Bool { guard let image = image else { return false } let fileManager = FileManager.default let imgDir = "your_image_folder/" // 替换成你的自定义目录名 let imagePath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent(imgDir + imageName) let rotatedImage = rotateImage(image: image) guard let data = rotatedImage.pngData() else { return false } // 替换过时的UIImagePNGRepresentation if fileManager.createFile(atPath: imagePath, contents: data, attributes: nil) { newItem?.image = true return true } else { print("error while saving to document directory") return false } } // 你的图片旋转方法 func rotateImage(image: UIImage) -> UIImage { // 这里保留你原有的旋转逻辑 return image } }
关键改进点说明
- 统一Picker创建逻辑:封装
createImagePicker方法避免重复代码,同时把加载动画统一添加到cameraOverlayView,确保动画始终在最上层。 - 后台执行耗时操作:图片旋转、写入文件的操作都放到全局队列,主线程只处理UI,彻底解决卡顿。
- 优化动画显示时机:在
didFinishPickingMediaWithInfo开头立即显示动画,避免主线程阻塞导致动画无法渲染。 - 更新过时API:用
pngData()替代废弃的UIImagePNGRepresentation,符合iOS新版本开发规范。
内容的提问来源于stack exchange,提问作者MaX
相关产品推荐
相关产品推荐

