1

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.

Rebecka
  • 1,213
  • 8
  • 14
  • Have you tried Bolts-Swift https://github.com/BoltsFramework/Bolts-Swift or https://github.com/BoltsFramework/Bolts-ObjC – Alfa Feb 15 '20 at 19:02
  • It's unclear, but you can't put the code after `url = properties?.url`? – Larme Feb 15 '20 at 20:15
  • @Larme Though it may be unlikely, any of those options could return nil, so would need else statement to account for them. There are also legitimate cases where nil can be returned for the URL (such as a blank tab) so this is an outcome from the whole block which needs to be accounted for anyway. – Rebecka Feb 15 '20 at 20:33
  • @Alfa Thank you, I will look into those. – Rebecka Feb 15 '20 at 20:33
  • Why not use this: https://pastebin.com/xTRSsuTe ? – Larme Feb 15 '20 at 21:43
  • @Larme Those solutions both still seems ugly, but are definitely a big improvement. I really like the reduction to the pyramid of doom, but whilst the first still seems messy in needing the multiple `handle()` calls, the way it separates the code feels much nicer than the second one. It has also given me an idea to try. Thank you. – Rebecka Feb 15 '20 at 23:38
  • You could clean this up by creating extensions for the `SFSafari...` classes with methods like `getActiveWindowURL((URL?) -> Void)` that call the completion handler with the URL or `nil`. That would be based on `getActiveTabURL((URL?) -> Void)`, etc. – Ken Thomases Feb 15 '20 at 23:53
  • @KenThomases That is essentially the idea that Large gave me, I have successfully tested it and posted it as an answer. – Rebecka Feb 16 '20 at 00:43
  • @Larme, if you want to post something as an answer I will accept it to give you the credit. Your first version made me think to turn the function into a completion handler to an extension. I can hide the extension in a separate file and have a clean method I can potentially reuse for other projects in the future. – Rebecka Feb 16 '20 at 00:49
  • I'm okay with that. I thought also about that (https://pastebin.com/JnejDkDT), in case you might need to retrieve the URL from different inner objects. Also, you hide nested handlers. – Larme Feb 16 '20 at 12:02

1 Answers1

0

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

}
Rebecka
  • 1,213
  • 8
  • 14