Reputation: 1212
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
Reputation: 156
Some ideas for you - example of getting host
from active tab
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!
)
})
})
})
}
}
}
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
}
}
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
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