Protocol oriented image picker

2017-06-05 林勇

Using the UIImagePickerController is pretty straightforward. The problem starts, when you have multiple controllers, and you need the picking ability in both of them. You can make a parent class with the core functionality, but that's old-school for a Swift developer, isn't it?

The final product

I imagine my protocol oriented image picker api something like this.

class ViewController: UIViewController {  
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        self.showImagePicker()
    }
}

extension ViewController: ImagePickerPresentable {  
    func selectedImage(data: Data?) {
        //here is your image data...
    }
}

The problem

TL;DR: @objc functions may not currently be in protocol extensions. You could create a base class instead, though that's not an ideal solution.

source: StackOverflow

The main idea is to have a protocol, that implements the UIImagePickerController-Delegate. This would be an easy thing, but the image picker protocol is an ancient one from the Objective-C times, so the system will never call your delegate methods if you do this. That's why we need a little trick, so we are going to use a helper class, but first, let's define our main protocol.

protocol ImagePickerPresentable: class {

    func showImagePicker()
    func selectedImage(data: Data?)
}

Next, we create a default implementation for the showImagePicker method, if our class is kind of a UIViewController. The helper class is going to be our delegate, so that instance will take care of the photo selection.

extension ImagePickerPresentable where Self: UIViewController {

    fileprivate func pickerControllerActionFor(for type: UIImagePickerControllerSourceType, title: String) -> UIAlertAction? {
        guard UIImagePickerController.isSourceTypeAvailable(type) else {
            return nil
        }
        return UIAlertAction(title: title, style: .default) { [unowned self] _ in
            let pickerController           = UIImagePickerController()
            pickerController.delegate      = ImagePickerHelper.shared
            pickerController.sourceType    = type
            pickerController.allowsEditing = true
            self.present(pickerController, animated: true)
        }
    }

    func showImagePicker() {
        ImagePickerHelper.shared.delegate = self

        let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)

        if let action = self.pickerControllerActionFor(for: .camera, title: "Take photo") {
            optionMenu.addAction(action)
        }
        if let action = self.pickerControllerActionFor(for: .photoLibrary, title: "Select from library") {
            optionMenu.addAction(action)
        }

        optionMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

        self.present(optionMenu, animated: true)
    }
}

As I mentioned before, we need a helper class. I really don't like singletons, but for this purpose one will do. It's going to be completely private, so no-one else can access it. After the photo is selected, you won't need this class in memory that's why I made a dispose method to clean it up after the job.

fileprivate class ImagePickerHelper: NSObject {

    weak var delegate: ImagePickerPresentable?

    fileprivate struct `Static` {
        fileprivate static var instance: ImagePickerHelper?
    }

    fileprivate class var shared: ImagePickerHelper {
        if ImagePickerHelper.Static.instance == nil {
            ImagePickerHelper.Static.instance = ImagePickerHelper()
        }
        return ImagePickerHelper.Static.instance!
    }

    fileprivate func dispose() {
        ImagePickerHelper.Static.instance = nil
    }

    func picker(picker: UIImagePickerController, selectedImage data: Data?) {
        picker.dismiss(animated: true, completion: nil)

        self.delegate?.selectedImage(data: data)
        self.delegate = nil
        self.dispose()
    }
}

Finally, we implement the UIImagePickerControllerDelegate methods, and tell the helper class to call back our protocol implementation with the final image data.

extension ImagePickerHelper: UIImagePickerControllerDelegate {

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        self.picker(picker: picker, selectedImage: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        guard let data = info[UIImagePickerControllerOriginalImage] as? UIImage else {
            return self.picker(picker: picker, selectedImage: nil)
        }

        self.picker(picker: picker, selectedImage: UIImagePNGRepresentation(data))
    }

    func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
        self.picker(picker: picker, selectedImage: UIImagePNGRepresentation(image))
    }
}

Yeah, almost forgot this... stupid thing, but we need this implementation as well.

extension ImagePickerHelper: UINavigationControllerDelegate {

}

From now on if your view controller conforms to the ImagePickerRepresentable protocol, you can pick images with just a few lines of code. I wanted to find an universal solution for this problem, but unfortunately the frameworks provided by Apple are not 100% Swift-ified, so I needed the singleton hack. I really hope that in the future we can just extend the UIImagePickerControllerDelegate protocol, and eliminate the need of this trick.


用户评论
开源开发学习小组列表