本文首发于个人博客 kyleye.top
初探 SwiftUI Link
起 – Link
最近在运用 Link 的时分发现在 watchOS 上无法成功翻开链接,而是会弹窗提示需求在 iPhone 端翻开
尽管后续发现能够经过 ASWebAuthenticationSession 来绕过翻开,但仍是对 Link 的完成以及咱们能运用 Link 做什么产生了好奇。
#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
经过 Swift 的反射机制和检查 SwiftUI.swiftinterface等办法,咱们能够得到关于 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))
}
}
简单的说 Link 的实质就是经过 @Environment(.openURL)
来拿到 OpenURLAction 进行引发翻开。
引起我注意的是这里的 @Environment(._openSensitiveURL)
,目前 SwiftUI 露出出来的 Link API 中并没有将 isSensitive
设置为 true 的状况。
那咱们需求怎么测验/运用 openSensitiveURL
呢?
承 – openSensitiveURL
第一种办法比较直观:既然 LinkDestination 内部在运用 @Environment(._openSensitiveURL)
,说明 SwiftUI 内部是有类似的如下代码,仅仅或许没有露出给外部第三方运用
extension EnvironmentValues {
var _openSensitiveURL: OpenURLAction {
get { ... }
set { ... }
}
}
咱们直接修改 SDK 中对应的 SwiftUI.swiftinterface 增加上这个 API 即可 (事实上,关于这里的 _openSensitiveURL 而言,SwiftUI.swiftinterface 已经将其符号为了 public,因而咱们不需求进行任何修改即可运用)
// 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
}
}
拿到这个 API 后咱们就能够直接调用 OpenURLAction.callAsFunction(_:)
来进行测验
第二种办法愈加通用:咱们经过 Unsafe Swift 提供的指针操作直接修改 isSensitive
先将 SwiftUI.Link
对象转为 UnsafeMutable*Pointer
,再将其 cast 到咱们自定义对齐 SwiftUI.Link
布局的 DemoLink
类型,最终完成修改,并返回原 link 对象给到 SwiftUI 体系
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
转 – OpenSensitiveURLActionKey
测验后发现关于普通的 url(比方 https://example.com
),openURL
和 _openSensitiveURL
都能翻开,可是关于敏感 URL (比方设置隐私页schema prefs:root=Privacy
)
二者仍是都无法翻开,对应错误日志如下
Failed to open URL prefs:root=Privacy&path=contacts:
Error Domain=NSOSStatusErrorDomain
Code=-10814 "(null)"
UserInfo={
_LSLine=225,
_LSFunction=-[_LSDOpenClient openURL:options:completionHandler:]
}
二者在 iOS 渠道的完成别离走了 UIApplication 的 open 办法和 LSApplicationWorkspace 的 open 办法
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
)
}
关于前者无法翻开是契合预期的,关于后者持续跟踪定位发现是因为咱们缺少了 com.apple.springboard.opensensitiveurl
这项 entitlement 导致的
增加该 entitlement 后在模拟器上运行,openSensitiveURL(url) 即可成功跳转到设置隐私页。
链接 Private Framework
OpenSensitiveURLActionKey 在 iOS 上依靠大量私有 API,正常的模拟完成主张经过 ObjectiveC Runtime 的消息机制(NSStringFromClass performSelector)来完成。
关于 macOS 渠道,咱们能够经过增加相关 flag 和增加体系库的查找路径来比较简单地完成对 Private Framework 的 link
(例子能够参阅之前写的macOS 提取 Keychain 数据的 Package)
可是 iOS 渠道的私有库在咱们的编译环境 macOS 上并不存在,导致增加相关 header 封装成 framework 后能够顺畅 build,可是无法完成 link
OpenSwiftUI 这里对 iOS 的 OpenSensitiveURLActionKey 完成运用了 macOS 计划而且能够正常作业仅仅巧合
- 依靠的 CoreServices 在 macOS 上刚好有相同的私有库和对应完成
- 依靠的 BoardServices 中的某个类只用作参数传递,运用 id/Any 即可
- 依靠的 UIKitCore 的 UIScene 在揭露 SDK 的 UIKit 中有定义,这里只需扩展办法,无需link UIKitCore
合 – 定论
回到本文最初的问题,watchOS 上 Link 的问题是因为 OpenURLActionKey.defaultValue
在 watchOS 上的完成导致的
相比 typealias Link = WatchLink
咱们经过掩盖 @Environment(.openURL)
能够更好地处理原问题
- 无缝运用 Link 的 其他API,而不是给 WatchLink 都重复完成一次
- 享用 Link 自带的无障碍支持(或者运用 accessibilityRepresentation 将无障碍行为转发到 Link)
- 基于运行时的 Environment 体系,而非编译期的 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
}
}
经过本文的对 SwiftUI Link 的探究,希望能为不熟悉 SwiftUI.Link 的同学带来一点点帮助
参阅
-
本文相关的示例代码 – Kyle-Ye/SwiftUILinkDemo
-
Link 的相关完整代码能够参阅该 PR – Kyle-Ye/OpenSwiftUI#5
-
更多的 sensitiveURL 能够参阅这篇文章 – Complete List of iOS URL Schemes for Apple Settings