sergdort / CleanArchitectureRxSwift

Example of Clean Architecture of iOS app using RxSwift
MIT License
3.91k stars 494 forks source link

From where should I present an MFMailComposeViewController? #13

Closed nmdias closed 7 years ago

nmdias commented 7 years ago

Hi,

Could you give some advice on where to place an MFMailComposeViewController?

In a non RxSwift and non Clean Architecture project, I would implement it in some view controller, like this:

extension ViewController: MFMailComposeViewControllerDelegate {

    func presentMailComposer() {

        if !MFMailComposeViewController.canSendMail() {
            // TODO: - Handle error here
            return
        }

        DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {

            let mailComposeViewController = MFMailComposeViewController()
            mailComposeViewController.mailComposeDelegate = self
            mailComposeViewController.setToRecipients(["mail@example.com"])
            mailComposeViewController.setMessageBody("Message body", isHTML: false)

            DispatchQueue.main.async(execute: {
                self.present(mailComposeViewController, animated: true, completion: nil)
            })

        }

    }

    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        if result == MFMailComposeResult.failed {
            // TODO: - Handle error here
        }
    }

}

Within the Clean Architecture, where would you place the Mail Composer?

Would you present this from the Navigator/Router? It is after all a "Scene", even if we don't necessarily have a Navigator/Router and a ViewModel dedicated to the MailComposer.

There are 2 distinct places where errors might occur, and I really don't think the Navigator should handle these.

Thanks!

sergdort commented 7 years ago

Hi, @nmdias

To me it seems that mail flow is pretty much similar to Image picker flow. So I would probably do something similar to this example from the main repo. Sure this may sound little bit weird but IMO both of these examples are in a way pickers. I mean we do not manually build UI for these components, we just respond to the user input.

nmdias commented 7 years ago

Thanks, @sergdort

That example was helpful. I managed to adapt it to the MFMailComposeViewController. Didn't test it thoroughly, but should do the job.

I'll leave it here for reference:

enum MFMailComposeError: CustomNSError {
    case cantSendEmail
    static let domain = "Custom MFMailCompose error"
    var errorCode: Int { return -9999 }
    var errorUserInfo: [String : Any] {
        return [
            NSLocalizedDescriptionKey: "Device cannot send email.",
            NSLocalizedFailureReasonErrorKey: "The device doesn't have an email account setup.",
            NSLocalizedRecoverySuggestionErrorKey: "Setup an email accound on the device."
        ]
    }
}

extension Reactive where Base: MFMailComposeViewController {

    /// Reactive wrapper for `delegate` message.
    public var didFinishWith: Observable<(MFMailComposeResult, NSError?)> {
        return delegate
            .methodInvoked(#selector(MFMailComposeViewControllerDelegate.mailComposeController(_:didFinishWith:error:)))
            .map { result in
                let composeResult = try castOrThrow(MFMailComposeResult.self, result[1])
                let error = try castOptionalOrThrow(NSError.self, result[2])
                return (composeResult, error)
            }
    }

}

extension Reactive where Base: MFMailComposeViewController {

    static func create(
        with parentViewController: UIViewController?,
        animated: Bool = true,
        configure: @escaping (MFMailComposeViewController) throws -> () = { x in })
        -> Observable<MFMailComposeViewController>
    {
        return Observable.create({ [weak parentViewController] observer -> Disposable in

            if !MFMailComposeViewController.canSendMail() {
                observer.on(.error(MFMailComposeError.cantSendEmail))
                return Disposables.create()
            }

            let mailComposer = MFMailComposeViewController()

            do {
                try configure(mailComposer)
            } catch let error {
                observer.on(.error(error))
                return Disposables.create()
            }

            guard let parentViewController = parentViewController else {
                observer.on(.completed)
                return Disposables.create()
            }

            parentViewController.present(mailComposer, animated: animated)
            observer.on(.next(mailComposer))

            return Disposables.create {
                dismissViewController(mailComposer, animated: animated)
            }

        })

    }

}

fileprivate 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

}

fileprivate func castOptionalOrThrow<T>(_ resultType: T.Type, _ object: Any) throws -> T? {

    if NSNull().isEqual(object) {
        return nil
    }

    guard let returnValue = object as? T else {
        throw RxCocoaError.castingError(object: object, targetType: resultType)
    }

    return returnValue

}

fileprivate func dismissViewController(_ viewController: UIViewController, animated: Bool) {

    if viewController.isBeingDismissed || viewController.isBeingPresented {
        DispatchQueue.main.async {
            dismissViewController(viewController, animated: animated)
        }
        return
    }

    if viewController.presentingViewController != nil {
        viewController.dismiss(animated: animated, completion: nil)
    }

}

Finally:

MFMailComposeViewController.rx.create(with: self) { (composer) in
    composer.setToRecipients(["mail@example.com"])
    composer.setMessageBody("Message body", isHTML: false)
}