+2

Sử dụng UIImagePickerController hiệu quả hơn với Closure.

Chắc hẳn làm việc với iOS, một lập trình viên chắc chắn ít nhất 1 lần làm việc với UIImagePickerController

Thông thường để chọn Image từ thư viện ta làm như sau:

import UIKit

class OpenLibraryViewController: UIViewController {
    
    @IBOutlet weak var avatar: UIImageView!
    var imagePicker: UIImagePickerController?
    @IBAction func btnChooseImage(_ sender: Any) {
        let alertViewController = UIAlertController(title: "Choose Image", message: "Choose your option", preferredStyle: .alert)
        let camera = UIAlertAction(title: "Camera", style: .default, handler: { (_) in
            self.openCamera()
        })
        let gallery = UIAlertAction(title: "Gallery", style: .default) { (_) in
            self.openGallary()
        }
        let cancel = UIAlertAction(title: "Cancel", style: .cancel) { (_) in
            //cancel
        }
        alertViewController.addAction(camera)
        alertViewController.addAction(gallery)
        alertViewController.addAction(cancel)
        self.present(alertViewController, animated: true, completion: nil)
    }

}

extension OpenLibraryViewController {
    fileprivate func openCamera() {
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) {
            self.imagePicker = UIImagePickerController()
            if let imagePicker = self.imagePicker {
                imagePicker.delegate = self
                imagePicker.sourceType = UIImagePickerControllerSourceType.camera
                imagePicker.allowsEditing = true
                self.present(imagePicker, animated: true, completion: nil)
            }
        } else {
            let alertWarning = UIAlertController(title: "Error", message: "Divice not have camera", preferredStyle: .alert)
            let cancel = UIAlertAction(title: "Cancel", style: .cancel) { (_) in
                print("Cancel")
            }
            alertWarning.addAction(cancel)
            self.present(alertWarning, animated: true, completion: nil)
        }
    }
    
    fileprivate func openGallary() {
        if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
            self.imagePicker = UIImagePickerController()
            if let imagePicker = self.imagePicker {
                imagePicker.delegate = self  as UIImagePickerControllerDelegate & UINavigationControllerDelegate
                imagePicker.sourceType = .photoLibrary
                imagePicker.allowsEditing = true
                self.present(imagePicker, animated: true, completion: nil)
            }
            
        }
    }
}

extension OpenLibraryViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    @available(iOS 2.0, *)
    public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
            //handle image
            self.avatar.image = image
        } else if let image = info[UIImagePickerControllerEditedImage] as? UIImage {
            //handle image
            self.avatar.image = image
        }
        imagePicker?.dismiss(animated: true, completion: nil)
    }
    
    @available(iOS 2.0, *)
    public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        imagePicker?.dismiss(animated: true, completion: nil)
    }
}

Khá là dài và rối, hơn nữa nó còn có 1 số nhược điểm như sau:

  1. Giả sử ta có 10 màn hình sử dụng muốn chọn ảnh từ thư viện -> việc lặp đi lặp lại đoạn code trên là không tốt(vi phạm nguyên tắc DRY).
  2. Viết 1 base chung để sử dụng, giả sử ta có 10 màn hinh, 5 màn hình muốn sử dụng tới chức năng này và 5 màn hình còn lại thì không cần sử dụng tới.

Mình sẽ làm cách khác hiệu quả hơn.

import UIKit

public typealias FinishPickingMediaClosure = (UIImagePickerController, UIImage?) -> Void
public typealias CancelClosure = (UIImagePickerController) -> Void

private var associatedEventHandle: UInt8 = 0
extension UIImagePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    private var closuresWrapper: ClosuresWrapper {
        get {
            if let wrapper = objc_getAssociatedObject(self, &associatedEventHandle) as? ClosuresWrapper {
                return wrapper
            }
            let closuresWrapper = ClosuresWrapper()
            self.closuresWrapper = closuresWrapper
            return closuresWrapper
        }
        set {
            self.delegate = self
            objc_setAssociatedObject(self, &associatedEventHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
    // MARK: - KVO
    public var didFinishPickingMedia: FinishPickingMediaClosure? {
        set { self.closuresWrapper.didFinishPickingMedia = newValue }
        get { return self.closuresWrapper.didFinishPickingMedia }
    }
    
    public var didCancel: CancelClosure? {
        set { self.closuresWrapper.didCancel = newValue }
        get { return self.closuresWrapper.didCancel }
    }
    
    // MARK: - UIImagePickerControllerDelegate implementation
    open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        self.closuresWrapper.didFinishPickingMedia?(picker, info["UIImagePickerControllerOriginalImage"] as? UIImage)
    }
    
    open func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        self.closuresWrapper.didCancel?(picker)
    }
}

fileprivate final class ClosuresWrapper {
    fileprivate var didFinishPickingMedia: FinishPickingMediaClosure?
    fileprivate var didCancel: CancelClosure?
}

Ok, phân tích việc dùng Closure ở trên chút nhé

Mình muốn UIImagePickerController adopted UIImagePickerControllerDelegate, UINavigationControllerDelegate và bắt 2 sự kiện:

  • Khi người sử dụng choose image trong thư viện didFinishPickingMedia
  • Khi người sử dụng cancel thông qua didCancel

Chúng ta tạo ra 2 Closure tương ứng FinishPickingMediaClosureCancelClosure

Sử dụng

class ExampleOpenUIPickerController: UIViewController {

    
    @IBOutlet weak fileprivate var avatar: UIImageView!
    
    @IBAction fileprivate func btnChooseAvatar(_ sender: Any) {
        let picker = UIImagePickerController()
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            picker.sourceType = .camera
        }
        picker.didCancel = { picker in
            picker.dismiss(animated: true, completion: nil)
        }
        picker.didFinishPickingMedia = { picker, image in
            self.avatar.image = image
            picker.dismiss(animated: true, completion: nil)
        }
        self.present(picker, animated: true, completion: nil)
    }
}

Kết quả tương tự như trên. Với cách thiết kế như trên việc sử dụng UIImagePickerController hoàn toàn đơn giản và hiệu quả hơn rất nhiều, khắc phục nhược điểm của cách truyền thống.

Trên đây chỉ là 1 tip nhỏ để tối ưu hoá việc code sao cho ngắn, gọn và dễ sử dụng.

Hy vọng bài viết có ích cho bạn. Happy Coding!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí