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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 09:59:51