⚠️ English version is translated by ChatGPT. See Original Chinese version here
Exploring SwiftUI Link
Part 1 - Link
Recently, when using Link
, I found that it fails to open links on watchOS and instead displays a popup suggesting to open the url on the iPhone.
Although later I discovered a workaround using ASWebAuthenticationSession
to bypass this issue, it sparked my curiosity about the implementation of Link
and what we can achieve using it.
#if os(watchOS)
import AuthenticationServices
/// A workaround to open link in watchOS platform
struct WatchLink<Label>: View where Label: View {
init(destination: URL, @ViewBuilder label: () -> Label) {
self.destination = destination
self.label = label()
}
let destination: URL
let label: Label
var body: some View {
Button {
let session = ASWebAuthenticationSession(
url: destination,
callbackURLScheme: nil
) { _, _ in
}
session.prefersEphemeralWebBrowserSession = true
session.start()
} label: {
label
}
}
}
typealias Link = WatchLink
#endif
Through Swift's reflection mechanism and examining SwiftUI.swiftinterface, we can obtain the following information about Link
.
public struct Link: View {
var label: Label
var destination: LinkDestination
public var body: some View {
Button {
destination.open()
} label: {
label
}
}
}
struct LinkDestination {
struct Configuration {
var url: URL
var isSensitive: Bool
}
var configuration: Configuration
@Environment(\.openURL)
private var openURL: OpenURLAction
@Environment(\._openSensitiveURL)
private var openSensitiveURL: OpenURLAction
func open() {
let openURLAction = configuration.isSensitive ? openSensitiveURL : openURL
openURLAction(configuration.url)
}
public init(destination: URL, @ViewBuilder label: () -> Label) {
self.label = label()
self.destination = LinkDestination(configuration: .init(url: destination, isSensitive: false))
}
}
In simple terms, the essence of Link
is to use @Environment(\.openURL)
to obtain the OpenURLAction
for invocation.
What caught my attention is the presence of @Environment(\._openSensitiveURL)
. Currently, in the exposed Link API of SwiftUI, there is no scenario where isSensitive
is set to true.
So, how do we test/use openSensitiveURL
?
Part 2 - openSensitiveURL
The first method is quite straightforward: since LinkDestination
internally uses @Environment(\._openSensitiveURL)
, it suggests that SwiftUI internally has something similar as the following code, it just does not be exposed to our third-party developer.
extension EnvironmentValues {
var _openSensitiveURL: OpenURLAction {
get { ... }
set { ... }
}
}
We can directly modify the corresponding SwiftUI.swiftinterface in the SDK to add this API. In fact, for _openSensitiveURL
case here, SwiftUI.swiftinterface has already marked it as public, so we don't need to make any modifications to use it.
// SwiftUI.swiftinterface
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension SwiftUI.EnvironmentValues {
public var openURL: SwiftUI.OpenURLAction {
get
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
set
}
public var _openURL: SwiftUI.OpenURLAction {
get
set
}
}
extension SwiftUI.EnvironmentValues {
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public var _openSensitiveURL: SwiftUI.OpenURLAction {
get
set
}
}
Once we have access to this API, we can directly call OpenURLAction.callAsFunction(_:)
for testing.
The second method is more versatile: we use pointers provided by Unsafe Swift for direct manipulation of isSensitive
.
First, convert the SwiftUI.Link
object to an UnsafeMutable*Pointer
, then cast it to our custom-aligned DemoLink
type which matches the layout of SwiftUI.Link
. Finally, we mutate the pointee to true and return the original link object to the SwiftUI system.
let url = URL(string: "https://example.com")!
var link = SwiftUI.Link(destination: url) {
SwiftUI.Text("Example")
}
withUnsafeMutablePointer(to: &link) { pointer in
let linkPointer = UnsafeMutableRawPointer(pointer).assumingMemoryBound(to: DemoLink<DemoText>.self)
let isSensitivePointer = linkPointer.pointer(to: \.destination.configuration.isSensitive)!
isSensitivePointer.pointee = true
}
return link
Part 3 - OpenSensitiveURLActionKey
After testing, it was found that for regular URLs (such as https://example.com
), both openURL
and _openSensitiveURL
can open them. However, for sensitive URLs (such as the Setting's app privacy page schema prefs:root=Privacy
), neither of them can open them. The error log is as follows:
Failed to open URL prefs:root=Privacy&path=contacts:
Error Domain=NSOSStatusErrorDomain
Code=-10814 "(null)"
UserInfo={
_LSLine=225,
_LSFunction=-[_LSDOpenClient openURL:options:completionHandler:]
}
The implementation for them on the iOS platform goes through the open
method of UIApplication
and the open
method of LSApplicationWorkspace
.
struct OpenURLActionKey: EnvironmentKey {
static let defaultValue = OpenURLAction(
handler: .system { url, completion in
UIApplication.shared.open(url, options: [:], completionHandler: completion)
},
isDefault: false
)
}
struct OpenSensitiveURLActionKey: EnvironmentKey {
static let defaultValue = OpenURLAction(
handler: .system { url, completion in
let config = _LSOpenConfiguration()
config.isSensitive = true
let scene = UIApplication.shared.connectedScenes.first
config.targetConnectionEndpoint = scene?._currentOpenApplicationEndpoint
guard let workspace = LSApplicationWorkspace.default() else {
return
}
workspace.open(url, configuration: config, completionHandler: completion)
},
isDefault: false
)
}
The inability to open the former is as expected. For the latter, further investiation revealed that it is due to the absence of the com.apple.springboard.opensensitiveurl
entitlement.
After adding this entitlement and running on the simulator, openSensitiveURL(url)
successfully navigates to the corresponding page.
Linking to Private Framework
OpenSensitiveURLActionKey
on iOS relies heavily on private APIs.
For a normal mockup implementation of iOS, it is recommended to use the ObjectiveC Runtime's message mechanism (NSStringFromClass, performSelector etc.).
For the macOS platform, we can easily link to the Private Framework by adding the relevant flags and system library search paths.
(An example can be referenced from my previous macOS Keychain Data Extraction Package)
However, the private frameworks on the iOS platform do not exist in our macOS development environment. So adding header as a fake framework can pass the compiler but not the linker.
And in the case of OpenSwiftUI, the implementation of OpenSensitiveURLActionKey
on iOS actually uses the macOS approach and works. But it was just coincidentally and not scale here.
The reason is as following: 1. The dependent CoreServices framework on macOS happens to have the same private framework and corresponding implementation. 2. A class in the dependent BoardServices is only used for parameter passing and can be processed as id/Any. 3. UIScene is defined in UIKit which is part of the public SDK. And our fake UIKitCore framework only extend a method, so linking of UIScene would not require UIKitCore.
Part 4 - Conclusion
Returning to the initial question of Link issues on watchOS, the problem arises from the implementation of OpenURLActionKey.defaultValue
on watchOS.
Compared to typealias Link = WatchLink
, a better solution to the original problem is override @Environment(\.openURL)
.
- Seamless use of other APIs of Link, eliminating the need to duplicate implementations for WatchLink.
- Enjoying the built-in accessibility support of Link (or forwarding accessibility behavior to Link using accessibilityRepresentation).
- Working through a runtime-based Environment system rather than compile-time typealias.
#if os(watchOS)
import AuthenticationServices
extension OpenURLAction {
static let authenticationSessionAction = OpenURLAction {
let session = ASWebAuthenticationSession(
url: $0,
callbackURLScheme: nil
) { _, _ in
}
session.prefersEphemeralWebBrowserSession = true
session.start()
return .handled
}
}
#endif
struct ContentView: View {
var body: some View {
...
#if os(watchOS)
.environment(\.openURL, .authenticationSessionAction)
#endif
}
}
Through the exploration of SwiftUI Link in this article, I hope it would be helpful for those struggling SwiftUI.Link
References
- The complete code related to Link implementation can be found in this PR.
- For more sensitiveURLs, you can refer to this article.