Search code examples
mvvmuiimagerx-swift

Passing data MVVM and RxSwift


I'm currently learning MVVM and RxSwift. I have an image I want to passed in, in my main view controller. I was successful using mvc and RxSwift, but since MVVM is new to me. I don't know how implemented in MVVM. This is my code.

// This is the work version MVC RxSwift
    class GProfilePickAlertVC: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    private let selectedSubjectPhoto = PublishSubject<UIImage>()
    var selectedPhoto: Observable<UIImage> {
        return selectedSubjectPhoto.asObservable()
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let editedImage     = info[.editedImage] as? UIImage {
            self.selectedSubjectPhoto.onNext(editedImage)
            dismiss(animated: true, completion: nil)
        }
        dismiss(animated: true, completion: nil)
    }
}

    class BasicInfoViewController: UITableViewController {
    let profileImageView    = GProfileImage(img: #imageLiteral(resourceName: "ic-tab-profile"), renderingMode: .alwaysTemplate)

    @objc private func handlePickPhoto() {
        let pickPhotoVC = GProfilePickAlertVC()
        pickPhotoVC.selectedPhoto.subscribe(onNext: { image in
            self.profileImageView.clipsToBounds = true
            self.profileImageView.image = image
            }).disposed(by: disposeBag)
        pickPhotoVC.modalPresentationStyle = .overFullScreen
        pickPhotoVC.modalTransitionStyle = .crossDissolve
        present(pickPhotoVC, animated: true)
    }
}

// This is my MVVM setup
    struct BasicInfoViewModel {

    let selectedImage: BehaviorRelay<UIImage>
}

    class BasicInfoViewController: UITableViewController {

    let disposeBag          = DisposeBag()

    var BasicInfoViewModel: BasicInfoViewModel!
}

Solution

  • Here's what I ended up with. There really isn't much for a view model to do in this instance since there is only one line of logic.

    The basic structure of the code is the same as your MVC example except my code is using RxImagePickerDelegateProxy instead of GProfilePickAlertVC. The former is a generic delegate that will automatically be attached to any UIImagePickerController by the Rx system as needed.

    The imagePickerScene(on:modalPresentationStyle:modalTransitionStyle:) function takes care of presenting and dismissing the image picker. It acts as a coordinator.

    The castOrThrow(_:_:) function is a generic helper to handle casting. You can use it all over the place.

    final class BasicInfoViewController: UIViewController {
    
        private let profileImageView = GProfileImage(img: #imageLiteral(resourceName: "ic-tab-profile"), renderingMode: .alwaysTemplate)
        private let disposeBag = DisposeBag()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let imagePicker = imagePickerScene(
                on: self, 
                modalPresentationStyle: .overFullScreen, 
                modalTransitionStyle: .crossDissolve
            )
    
            pickPhotoButton.rx.tap
                .flatMapLatest { Observable.create(imagePicker) }
                .compactMap { $0[.editedImage] as? UIImage }
                .bind { image in
                    self.profileImageView.clipsToBounds = true
                    self.profileImageView.image = image
                }
                .disposed(by: disposeBag)
        }
    }
    
    func imagePickerScene(on presenter: UIViewController, modalPresentationStyle: UIModalPresentationStyle? = nil, modalTransitionStyle: UIModalTransitionStyle? = nil) -> (_ observer: AnyObserver<[UIImagePickerController.InfoKey: AnyObject]>) -> Disposable {
        return { [weak presenter] observer in
            let controller = UIImagePickerController()
            if let presentationStyle = modalPresentationStyle {
                controller.modalPresentationStyle = presentationStyle
            }
            if let transitionStyle = modalTransitionStyle {
                controller.modalTransitionStyle = transitionStyle
            }
            presenter?.present(controller, animated: true)
            return controller.rx.didFinishPickingMediaWithInfo
                .do(onNext: { _ in
                    presenter?.dismiss(animated: true)
                })
                .subscribe(observer)
        }
    }
    
    final class RxImagePickerDelegateProxy: DelegateProxy<UIImagePickerController, UINavigationControllerDelegate & UIImagePickerControllerDelegate>, DelegateProxyType, UINavigationControllerDelegate & UIImagePickerControllerDelegate {
    
        static func currentDelegate(for object: UIImagePickerController) -> (UIImagePickerControllerDelegate & UINavigationControllerDelegate)? {
            return object.delegate
        }
    
        static func setCurrentDelegate(_ delegate: (UIImagePickerControllerDelegate & UINavigationControllerDelegate)?, to object: UIImagePickerController) {
            object.delegate = delegate
        }
    
        static func registerKnownImplementations() {
            self.register { RxImagePickerDelegateProxy(parentObject: $0, delegateProxy: RxImagePickerDelegateProxy.self) }
         }
    }
    
    extension Reactive where Base: UIImagePickerController {
    
        var didFinishPickingMediaWithInfo: Observable<[UIImagePickerController.InfoKey: AnyObject]> {
            return RxImagePickerDelegateProxy.proxy(for: base)
                .methodInvoked(#selector(UIImagePickerControllerDelegate.imagePickerController(_:didFinishPickingMediaWithInfo:)))
                .map({ (a) in
                    return try castOrThrow(Dictionary<UIImagePickerController.InfoKey, AnyObject>.self, a[1])
                })
        }
    }
    
    func castOrThrow<T>(_ resultType: T.Type, _ object: Any) throws -> T {
        guard let returnValue = object as? T else {
            throw RxCocoaError.castingError(object: object, targetType: resultType)
        }
    
        return returnValue
    }