Search code examples
swiftanimationviewuiimagepickercontroller

Show a view in front of the imagePicker


In my application the user can either open the camera roll to select a picture or open the camera to take directly one by himself.

In both cases, the picture selected/taken will also be saved locally for further reference.

The downside is that the saving operation usually freeze the screen until it is finished.

I found an animation in this post and I want to display it in front of the imagePickerController but I can't manage to do so.

class SinglePageViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITextFieldDelegate, UINavigationBarDelegate {

    var spinner: UIActivityIndicatorView?

    lazy var showCameraImagePickerController: UIImagePickerController = {
        let imagePicker = UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = .camera
        imagePicker.allowsEditing = false
        return imagePicker
    }()

    lazy var showPhotoImagePickerController: UIImagePickerController = {
        let imagePicker = UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = .photoLibrary
        imagePicker.allowsEditing = false
        return imagePicker
    }()

    @IBOutlet weak var photoButton: UIButton!
    @IBAction func onPhotoButton(_ sender: Any) {
        self.present(self.showCameraImagePickerController, animated: true, completion: nil)
    }

    @IBOutlet weak var galleryButton: UIButton!
    @IBAction func onGalleryButton(_ sender: Any) {
        self.present(self.showPhotoImagePickerController, animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {

        //start animation
        let screenSize: CGRect = UIScreen.main.bounds

        spinner = UIActivityIndicatorView(frame: CGRect(x: screenSize.width / 2 - 150, y: screenSize.height / 2 - 150, width: 300, height: 300))
        spinner?.isHidden = false
        spinner?.startAnimating()
        spinner?.color = UIColor.red

        switch picker {
        case showCameraImagePickerController:
            // snap pic, save to doc, save to album

            self.showCameraImagePickerController.view.addSubview(spinner!)

            timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false, block: { _ in

                let image = info[UIImagePickerControllerOriginalImage] as? UIImage

                if self.saveImage(imageName: "\(self.titleLabel.text!).png", image: image) {
                    // additionally save to photo album
                    UIImageWriteToSavedPhotosAlbum(image!, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)

                    print("saved \(self.titleLabel.text!).png")

                    self.imageView.image = image
                }
            })

        case showPhotoImagePickerController:
            //switch pic, save to doc. no album

            self.showPhotoImagePickerController.view.addSubview(spinner!)

            timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false, block: { _ in

                let image = info[UIImagePickerControllerOriginalImage] as? UIImage

                if self.saveImage(imageName: "\(self.titleLabel.text!).png", image: image) {

                    print("saved new \(self.titleLabel.text!).png")

                    self.imageView.image = image

                    self.spinner?.stopAnimating()
                    self.spinner?.removeFromSuperview()
                    self.spinner = nil

                    self.showPhotoImagePickerController.dismiss(animated: true, completion: nil)
                } else {
                    self.spinner?.stopAnimating()
                    self.spinner?.removeFromSuperview()
                    self.spinner = nil

                    self.showPhotoImagePickerController.dismiss(animated: true, completion: nil)
                }
            })

        default:
            return
        }

    }

    @objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {

        spinner?.stopAnimating()
        spinner?.removeFromSuperview()
        spinner = nil

        self.showCameraImagePickerController.dismiss(animated: true, completion: nil)
    }

    func saveImage(imageName: String, image: UIImage?) -> Bool {

        //create an instance of the FileManager
        let fileManager = FileManager.default

        //get the image path
        let imagePath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent(imgDir + imageName)
        print(imagePath)

        //get the image we took with camera
        let image = rotateImage(image: image!)

        //get the PNG data for this image
        let data = UIImagePNGRepresentation(image)

        //store it in the document directory
        if fileManager.createFile(atPath: imagePath as String, contents: data, attributes: nil) {
            newItem?.image = true

            return true
        } else {
            print("error while saving")
            return false
        }
    }
}

as you can see I tried playing with bringSubView(toFront:) and also with the zPosition but with no results.

following this similar question I looked into the documentation for cameraOverlayView but it says that it only works when the imagePicker is presented in camera mode, which doesn't cover the case when I open the photo library

I also recently tried to use a workaround, meaning that I dismiss the imagePickerController as soon as possible and update the image afterwards, but that is not optimal anymore because of some changes in the structure of app.

EDIT

to make myself clearer I'll state again what I need: show the spinner animation in front of the imagePicker, as soon as I tap a photo to choose it, and until I finish saving, then dismiss the imagePicker. I do not want to first dismiss the picker and then save while showing the spinner in the main view.

EDIT2 updated the code with the new one from the answer. only problem is that if I don't put a timer the spinner shows itself only at the end of the saving process for a brief moment (checked with breakpoints). This results in no animation during the couple of seconds of saving process and just a brief apparition of the spinner at the end before dismissing the imagePicker. Just putting a 0.1sec delay triggers the spinner immediately and I get the expected behaviour (animation while saving). No idea why


Solution

  • Please see a complete example where spinner will show while the image is being saved and once finishes saving spinner will be removed.

        class ViewController: UIViewController {
    
        /// Image picker controller
        lazy var imagePickerController: UIImagePickerController  = {
            let imagePicker = UIImagePickerController()
            imagePicker.delegate = self
            imagePicker.sourceType = .photoLibrary;
            imagePicker.allowsEditing = false
            return imagePicker
        }()
    
        var spinner: UIActivityIndicatorView?
    
        @IBAction func imagePickerButton(_ sender: UIButton) {
            self.present(self.imagePickerController, animated: true, completion: nil)
        }
    }
    
    // MARK: ImagePicker Delegate to get the image picked by the user
    extension ViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
            //start animation
            let screenSize: CGRect = UIScreen.main.bounds
            spinner = UIActivityIndicatorView(frame: CGRect(x: screenSize.width/2 - 50, y: screenSize.height/2 - 50, width: 100, height: 100))
            spinner?.isHidden = false
            spinner?.startAnimating()
            spinner?.color = UIColor.black
            self.imagePickerController.view.addSubview(spinner!)
    
            let image = info[UIImagePickerControllerOriginalImage] as? UIImage
            UIImageWriteToSavedPhotosAlbum(image!, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    
        }
    
        @objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
    
            spinner?.stopAnimating()
            spinner?.removeFromSuperview()
            spinner  = nil
    
            self.imagePickerController.dismiss(animated: true, completion: nil)
    
            if  error == nil {
                let ac = UIAlertController(title: "Saved!", message: "Your altered image has been saved to your photos.", preferredStyle: .alert)
                ac.addAction(UIAlertAction(title: "OK", style: .default))
                present(ac, animated: true)
            }
        }
    
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            picker.dismiss(animated: true, completion: nil)
        }
    }