Rebecka
Rebecka

Reputation: 1212

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.

Upvotes: 1

Views: 717

Answers (2)

Maxim Rysevets
Maxim Rysevets

Reputation: 156

Some ideas for you - example of getting host from active tab

V1:

class SafariExtensionViewController: SFSafariExtensionViewController {

    override func viewDidAppear() {
        super.viewDidAppear()
        SafariExtensionViewController.domain_getCurrent { host in
            DispatchQueue.main.async {
                print(host)
                // ANY OTHER CODE
            }
        }
    }

    static func domain_getCurrent(completionHandler: @escaping (String) -> Void) {
        SFSafariApplication.getActiveWindow { window in
            window?.getActiveTab(completionHandler: { tab in
                tab?.getActivePage(completionHandler: { page in
                    page?.getPropertiesWithCompletionHandler({ properties in
                        let url = properties?.url
                        let host = url?.host
                        completionHandler(
                            host!
                        )
                    })
                })
            })
        }
    }

}

V2:

class SafariExtensionViewController: SFSafariExtensionViewController {

    override func viewDidAppear() {
        super.viewDidAppear()
        Task {
           if let host = await SafariExtensionViewController.domain_getCurrent() {
                print(host)
                // ANY OTHER CODE
            }
        }
    }

    static func domain_getCurrent() async -> String? {
        let windows = await SFSafariApplication.activeWindow()
        let tab = await windows?.activeTab()
        let page = await tab?.activePage()
        let properties = await page?.properties()
        let url = properties?.url
        let host = url?.host
        return host
    }

}

V3:

class SafariExtensionViewController: SFSafariExtensionViewController {

    override func viewDidAppear() {
        super.viewDidAppear()
        SFSafariApplication.getActiveWindow { window in
            window?.getActiveTab(completionHandler: { tab in
                tab?.getActivePage(completionHandler: { page in
                    page?.getPropertiesWithCompletionHandler({ properties in
                        DispatchQueue.main.async {
                            print(properties?.url?.host)
                            // ANY OTHER CODE
                        }
                    })
                })
            })
        }
    }

}

Upvotes: 0

Rebecka
Rebecka

Reputation: 1212

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")
        }
    }

}

Upvotes: 0

Related Questions