简体   繁体   中英

Executing code after nested completion handlers

I am writing a Safari app extension and want to fetch the URL for the active page in my view controller.

This means nested completion handlers to fetch the window, to fetch the tab, to fetch the page, to access its properties. Annoying but simple enough. It looks like this:

func doStuffWithURL() {

    var url: URL?

    SFSafariApplication.getActiveWindow { (window) in
        window?.getActiveTab { (tab) in
            tab?.getActivePage { (page) in
                page?.getPropertiesWithCompletionHandler { (properties) in
                    url = properties?.url
                }
            }
        }
    }

    // NOW DO STUFF WITH THE URL
    NSLog("The URL is \(String(describing: url))")

}

The obvious problem is it does not work. Being completion handlers they will not be executed until the end of the function. The variable url will be nil, and the stuff will be done before any attempt is made to get the URL.

One way around this is to use a DispatchQueue . It works, but the code is truly ugly:

func doStuffWithURL() {

    var url: URL?

    let group = DispatchGroup()
    group.enter()
    SFSafariApplication.getActiveWindow { (window) in
        if let window = window {
            group.enter()
            window.getActiveTab { (tab) in
                if let tab = tab {
                    group.enter()
                    tab.getActivePage { (page) in
                        if let page = page {
                            group.enter()
                            page.getPropertiesWithCompletionHandler { (properties) in
                                url = properties?.url
                                group.leave()
                            }
                        }
                        group.leave()
                    }
                }
                group.leave()
            }
        }
        group.leave()
    }

    // NOW DO STUFF WITH THE URL
    group.notify(queue: .main) {
        NSLog("The URL is \(String(describing: url))")
    }

}

The if blocks are needed to know we are not dealing with a nil value. We need to be certain a completion handler will return, and therefore a .leave() call before we can call a .enter() to end up back at zero.

I cannot even bury all that ugliness away in some kind of getURLForPage() function or extension (adding some kind of SFSafariApplication.getPageProperties would be my preference) as obviously you cannot return from a function from within a .notify block.

Although I tried creating a function using queue.wait and a different DispatchQueue as described in the following answer to be able to use return…

https://stackoverflow.com/a/42484670/2081620

…not unsurprisingly to me it causes deadlock, as the .wait is still executing on the main queue.

Is there a better way of achieving this? The "stuff to do," incidentally, is to update the UI at a user request so needs to be on the main queue.

Edit: For the avoidance of doubt, this is not an iOS question. Whilst similar principles apply, Safari app extensions are a feature of Safari for macOS only.

Thanks to Larme's suggestions in the comments, I have come up with a solution that hides the ugliness, is reusable, and keep the code clean and standard.

The nested completion handlers can be replaced by an extension to the SFSafariApplication class so that only one is required in the main body of the code.

extension SFSafariApplication {

    static func getActivePageProperties(_ completionHandler: @escaping (SFSafariPageProperties?) -> Void) {

        self.getActiveWindow { (window) in
            guard let window = window else { return completionHandler(nil) }
            window.getActiveTab { (tab) in
                guard let tab = tab else { return completionHandler(nil) }
                tab.getActivePage { (page) in
                    guard let page = page else { return completionHandler(nil) }
                    page.getPropertiesWithCompletionHandler { (properties) in
                        return completionHandler(properties)
                    }
                }
            }
         }

    }

}

Then in the code it can be used as:

func doStuffWithURL() {

    SFSafariApplication.getActivePageProperties { (properties) in
        if let url = properties?.url {
            // NOW DO STUFF WITH THE URL
            NSLog("URL is \(url))")
        } else {
            // NOW DO STUFF WHERE THERE IS NO URL
            NSLog("URL ERROR")
        }
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM