← Blog

Open Third-Party URLs in a SwiftUI App Using a Share Extension

iOS
SwiftUI
published on:

There are two widespread ways of opening an URL with a SwiftUI app, namely custom URL schemes and universal links. I do not cover their implementation in this article but there are a lot of great explanations readily available (like the one by Benoit Pasquier). Despite the great coverage for both features, I found myself struggling to implement a way of opening third-party URLs in catalyst.

Curtain up for share extensions

For my case, it is necessary to open URLs from GitLab (i.e., https://gitlab.com/) and from self-hosted instances of GitLab (i.e., https://gitlab.example.com/). In both cases, these URLs belong to a third party such that universal links are not an option. Likewise, custom URL schemes do not work since HTTP(S) is already an established URL scheme.

Hence, I need to come up with another solution for opening HTTP(S) URLs in a SwiftUI app. Luckily, Apple provides other means to interact with third-party content through app extensions. The action and share extensions are two candidates that fit for opening content from another app. While the action extension sounds like a better fit (“view or transform content originating in a host app”), the share extension provides a better user experience since the app’s icon is displayed directly at the top above the available actions.

In XCode, adding a share extension is possible from the app’s project targets. After selecting the “+” icon, choose the “Share Extension” template for iOS. In the next step, select a product name, select a team, and finish the setup. Since I do not work with Storyboard, I remove MainInterface.storyboard from the share extension’s folder and replace the key NSExtensionMainStoryboard from the extension’s Info.plist with the key NSExtensionPrincipalClass inside the NSExtension dictionary having a value of $(PRODUCT_MODULE_NAME).ShareViewController. Afterward, I replace the content of ShareViewController.swift with the following:

ShareViewController.swift

_43
import UIKit
_43
import SwiftUI
_43
_43
class ShareViewController: UIViewController {
_43
override func viewWillAppear(_ animated: Bool) {
_43
super.viewWillAppear(animated)
_43
_43
for item in extensionContext!.inputItems as! [NSExtensionItem] {
_43
if let attachments = item.attachments {
_43
for itemProvider in attachments {
_43
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
_43
itemProvider.loadItem(forTypeIdentifier: "public.url", options: nil, completionHandler: { (item, error) in
_43
let url = (item as! NSURL).absoluteURL!
_43
_43
self.open(url: url.toCatalystURL())
_43
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
_43
})
_43
}
_43
}
_43
}
_43
}
_43
}
_43
_43
private func open(url: URL) {
_43
var responder: UIResponder? = self as UIResponder
_43
let selector = #selector(openURL(_:))
_43
_43
while responder != nil {
_43
if responder!.responds(to: selector) && responder != self {
_43
responder!.perform(selector, with: url)
_43
_43
return
_43
}
_43
_43
responder = responder?.next
_43
}
_43
}
_43
_43
@objc
_43
private func openURL(_ url: URL) {
_43
return
_43
}
_43
}

@objc private func openURL(_ url: URL) is a declaration required by the compiler for private func open(url: URL) which mimics the lack of open(url: URL) from UIApplication. override func viewWillAppear(_ animated: Bool) implements the behavior necessary to handle the third-party URL shared from another application (e.g., a browser). The for loop may look intimidating but simply unpacks the shared third-party URL. Arriving at the shared third-party url, I perform the actual trick: converting the HTTPS URL into a custom URL scheme that I can open with the app. In my case, url.toCatalystURL() performs this conversion and passes the result to open(url: URL).

Caveats

As per App Store review guidelines, apps should only use public APIs. As such, using open(url: URL) in a share extension would violate the mentioned guidelines because ShareViewController does not derive UIApplication where open(url: URL) would be available. Apple also explicitly states this in their documentation: “A Today widget (and no other app extension type) can ask the system to open its containing app by calling the openURL:completionHandler: method of the NSExtensionContext class.” Since I haven’t published catalyst to the App Store, yet, I am not sure if such usage is prohibited in practice as well.

Code sharing

A share extension is a separate target, meaning that it does not share any source code with the corresponding app. If you still want to share source code between the app and the share extension, you have to change the target membership of the source code to be shared. In XCode, you can easily select the target membership by ticking the corresponding boxes under Target Membership from the inspector on the right-hand side after opening the desired source code file.

Debugging

Debugging an app extension is not straightforward but possible, regardless. While you cannot debug the app and its share extension at the same time, you can determine the target to be debugged. In XCode, navigate to Debug, Attach to process, and select the app extension’s process. Now, breakpoints should be triggered as you are used to with the containing app. Courtesy to mfaani for pointing this out on StackOverflow.