2

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?

Rob N
  • 15,024
  • 17
  • 92
  • 165
  • Who is calling document.open? Is it a view controller? Is it in the AppDelegate? If I get a better understanding, I think I can help. If it is just a view controller, you can override that function within your view controller and be able to show an alert. Can you share a bit more code? – Rudy B Jul 26 '18 at 23:32
  • Okay, I've edited the question to hopefully improve clarity a bit. – Rob N Jul 27 '18 at 01:05
  • Probably duplicate of https://stackoverflow.com/questions/26554894/how-to-present-uialertcontroller-when-not-in-a-view-controller Everything except the problem of how to present an alert when you are a UIDocument (not a UIViewController) seems to be dross. – matt Jul 27 '18 at 16:46
  • Can't you get the root ViewController from your AppDelegate and use that to display the alert? – onnoweb Jul 27 '18 at 17:15
  • Maybe. When I tried that I got an error that I guess should be a separate question if I need to discuss it. It said `Warning: Attempt to present on whose view is not in the window hierarchy!` – Rob N Jul 27 '18 at 17:43
  • @RobN I posted an answer bellow. Tell me if this works for you. – Rudy B Jul 28 '18 at 19:56

1 Answers1

1

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)
    }
}
Rudy B
  • 579
  • 1
  • 3
  • 16
  • Thank you, I’m accepting this answer. I agree with your main point: give `handleError` a callback of some kind to the view controller that is calling `UIDocument.open`. However, I'm uncomfortable with the design of `DocumentManager`. You don’t want an *is-a* relationship between `DocumentManager` (with it’s new `open(doc)` method) and `Document`, which already has an `open()` method. – Rob N Aug 03 '18 at 16:05
  • I wonder if my `handleError()` should invoke the superclass implementation? And if I should call `finishedHandlingError(_:recovered:)`. – neoneye Jul 02 '19 at 19:53