2

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!

4

2 回答 2

2

The basic premise behind Clean Architecture is that the "business rules" i.e., the logic of your app, is not dependent on, or executed by, the UI. Instead the Logic of your app is in control.

This means that some part of the logic of the app knows when the user can send an email, but it has no idea exactly how that happens.

If you are using RxSwift, you could think of the user interaction as a model transformation. So your example would become:

func sendMail(recipients: [String], tile: String, message: String, isHTML: Bool) -> Observable<Bool>

The above could be passed to your logic as a closure or it could be embedded in a protocol that your logic uses.


If you want to use Robert Martin's specific structure, then things are a bit different because you wouldn't use Rx in your model objects at all. (He recommends that your Interactors &al. don't depend on outside libraries.)

In which case, an Interactor would send a message to the presenter to display the email view controller through a Response Model object and the Controller would send the success/failure result back to the Interactor, or more likely to a different Interactor.

Here is how Uncle Bob says he structures things: https://camo.githubusercontent.com/c34f4ed0203238af6e43b44544b864dffac6bc08/687474703a2f2f692e696d6775722e636f6d2f576b42414154792e706e67 However in the one iOS Swift app he has publicly presented, he didn't use this structure. https://github.com/unclebob/MACS_GOMOKU


To elaborate after your comment, the signature does work, but it requires some supporting structure...

First, a nice to have but not strictly necessary piece, we make view controller presentation reactive:

extension Reactive where Base: UIViewController {

    func present(_ viewControllerToPresent: UIViewController, animated: Bool) -> Observable<Void> {
        return Observable.create { observer in
            self.base.present(viewControllerToPresent, animated: animated, completion: {
                observer.onNext()
                observer.onCompleted()
            })
            return Disposables.create()
        }
    }
}

It's not just that a view controller can only be presented by another view controller, but also that it must be the one view controller in the system that isn't currently presenting anything. We can find that view controller by starting at the root and walking up the presentation stack:

extension UIViewController {

    static func top() -> UIViewController? {
        var result = UIApplication.shared.delegate.flatMap { $0.window??.rootViewController }
        while let child = result?.presentedViewController {
            result = child
        }
        return result
    }
}

Now, instead of having some view controller conform to the MFMailComposeViewControllerDelegate protocol, we make a dedicated Reactive class.

class MailComposeViewControllerDelegate: NSObject, UINavigationControllerDelegate, MFMailComposeViewControllerDelegate {

    let subject = PublishSubject<MFMailComposeResult>()

    func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
        if let error = error {
            subject.onError(error)
        }
        else {
            subject.onNext(result)
        }
    }
}

Once all these pieces are in place, writing the sendMail function is easy:

func sendMail(recipients: [String], tile: String, message: String, isHTML: Bool) -> Observable<MFMailComposeResult> {
    let delegate = MailComposeViewControllerDelegate()
    let controller = MFMailComposeViewController()
    controller.delegate = delegate
    return UIViewController.top()!.rx.present(controller, animated: true)
        .flatMap { delegate.subject }
}

And like I said, you should not call this function directly. Instead you should inject it into the object that will call it so that you can mock it out for testing.

This same pattern works for UIImagePickerController and even UIAlertController!

You might find this article I wrote an interesting read. It uses promises instead of Rx, but the philosophy is the same: https://medium.com/@danielt1263/encapsulating-the-user-in-a-function-ec5e5c02045f

于 2017-07-29T02:28:42.580 回答
0

It depends on the way you decide to manage your project.

Eventually the mail composer element is a UI element, so presenting it should be performed in the UI handling classes - such as your VC, some kind of extension like you made etc..

In my opinion, what you can do is subclass your mail composer, and create a completion block response from it when it completes, and then handles the error in the UI accordingly, this way it will manage itself (due to it being a global controller, having a general VM for that is a waste of code).

Then when you present the mail composer you let the user add a completion and a failure blocks / use the signaling from the Rx to return the result.

于 2017-07-27T11:36:31.413 回答