Search code examples
ioscocoa-touchios11uidocument

How to present the error message when opening UIDocument?


I have a document based iOS app, and when it first opens a document, the main view controller calls UIDocument.open.

document.open { success in
    if success { ... set up UI ... }
    else { ??? }
}

The problem here is that if success is false, I don't have access to the error. Often, Apple's APIs will pass an optional Error parameter to the callback in these situations, but for some reason they don't here.

I found this method that I can override in my application's subclass of UIDocument:

override func handleError(_ error: Error, userInteractionPermitted: Bool) {

Now in that method I have the Error but I don't have easy access to the view controller that called document.open, which I need to present something like a UIAlertController to display the error message. This handleError method is also called on a non-main thread.

It looks like I need to coordinate by passing information in instance or global variables. Because that seems more awkward than the Apple's usual design -- where I expect the Error to be available in the open's completion handler, I thought I might be missing something.

Is there a another recommended way to get the error object and present a message to the user?


Solution

  • Rob,

    If you really want to be "swifty", you could implement a closure to do exactly this without the need for static / global variables.

    I would start by defining an enum that models the success and failure cases of an API call to UIDocument. The generic Result enum is a pretty common way of doing this.

    enum Result<T> {
        case failure(Error)
        case success(T)
    }
    

    From there I would define an optional closure in your class that handles the outcome of UIDocument.open

    The implementation I would do is something like so:

    class DocumentManager: UIDocument {
    
        var onAttemptedDocumentOpen: ((Result<Bool>) -> Void)?
    
        func open(document: UIDocument){
            document.open { result in
                guard result else { return } // We only continue if the result is successful
    
                // Check to make sure someone has set a function that will handle the outcome
                if let onAttemptedDocumentOpen = self.onAttemptedDocumentOpen {
                    onAttemptedDocumentOpen(.success(result))
                }
            }
        }
    
        override func handleError(_ error: Error, userInteractionPermitted: Bool) {
            // Check to make sure someone has set a function that will handle the outcome
            if let onAttemptedDocumentOpen = self.onAttemptedDocumentOpen {
                onAttemptedDocumentOpen(.failure(error))
            }
        }
    
    }
    

    Then I from whatever class will be using the DocumentManager you would do something like this:

    class SomeOtherClassThatUsesDocumentManager {
    
        let documentManger = DocumentManager()
    
        let someViewController = UIViewController()
    
        func someFunction(){
            documentManger.onAttemptedDocumentOpen = { (result) in
                switch result {
                case .failure(let error):
                    DispatchQueue.main.async {
                        showAlert(target: self.someViewController, title: error.localizedDescription)
                    }
    
                case .success(_):
                    // Do something
                    return
                }
            }
        }
    }
    

    Bonus: This is a static function I wrote to display a UIAlertController on some view controller

    /** Easily Create, Customize, and Present an UIAlertController on a UIViewController
    
     - Parameters:
        - target: The instance of a UIViewController that you would like to present tye UIAlertController upon.
        - title: The `title` for the UIAlertController.
        - message: Optional `message` field for the UIAlertController. nil by default
        - style: The `preferredStyle` for the UIAlertController. UIAlertControllerStyle.alert by default
        - actionList: A list of `UIAlertAction`. If no action is added, `[UIAlertAction(title: "OK", style: .default, handler: nil)]` will be added.
    
     */
    func showAlert(target: UIViewController, title: String, message: String? = nil, style: UIAlertControllerStyle = .alert, actionList: [UIAlertAction] = [UIAlertAction(title: "OK", style: .default, handler: nil)] ) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: style)
        for action in actionList {
            alert.addAction(action)
        }
        // Check to see if the target viewController current is currently presenting a ViewController
        if target.presentedViewController == nil {
            target.present(alert, animated: true, completion: nil)
        }
    }