Widget 虽然不能直接展示完整的 App 界面,但通过深度链接(Deep Link)和 URL 机制,可以实现从 Widget 精准跳转到 App 内部特定页面的功能,提升用户体验的连贯性。
本篇将讲解 Widget 中所有可用的跳转方式及其实现细节。
1. Widget 跳转方式概览
| 方式 | 引入版本 | 说明 |
|---|---|---|
.widgetURL(_:) |
iOS 14 | 整Widget 唯一的点击目标,不支持多点 |
Link(destination:) |
iOS 14 | 支持多个区域各有独立的跳转链接 |
Button(intent:) + openAppWhenRun |
iOS 17 | Intent 中返回 .opensAppWhenRun,执行后打开 App |
2. widgetURL - 整Widget 单链接
最简单的跳转方式,整个 Widget 点击后跳转到同一个 URL:
swift
struct MyWidgetView: View {
var entry: MyEntry
var body: some View {
VStack {
Text("今日天气")
Text(entry.temperature)
}
.widgetURL(URL(string: "myapp://weather"))
}
}
在主 App 中接收 URL
swift
// App 入口(SwiftUI App)
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
handleDeepLink(url)
}
}
}
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
let path = components.host
switch path {
case "weather":
// 导航到天气页面
navigateToWeather()
case "todo":
// 导航到待办页面
navigateToTodo()
default:
break
}
}
}
注意:
.widgetURL()应用于整个 Widget 视图的根容器。一个 Widget 只能有一个 widgetURL。
3. Link - 多区域独立跳转
Link 允许在 Widget 的不同区域设置不同的跳转目标:
swift
struct MultiLinkWidgetView: View {
var entry: TodoEntry
var body: some View {
VStack(spacing: 0) {
// 区域 1:点击跳转到待办列表
Link(destination: URL(string: "myapp://todos")!) {
HStack {
Image(systemName: "checklist")
Text("全部待办 (\(entry.totalCount))")
.font(.headline)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
Divider()
// 区域 2:点击跳转到具体待办
ForEach(entry.todos.prefix(3)) { todo in
Link(destination: URL(string: "myapp://todo/\(todo.id)")!) {
HStack {
Image(systemName: todo.isDone ? "checkmark.circle.fill" : "circle")
Text(todo.title)
.font(.caption)
.lineLimit(1)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
}
}
}
Link vs widgetURL 对比
| 特性 | .widgetURL() |
Link |
|---|---|---|
| 跳转目标个数 | 1 个 | 多个(不限) |
| 作用范围 | 整个 Widget | 单个区域 |
| 优先级 | 低 | 高(Link 会覆盖 widgetURL) |
| 适用场景 | 简单跳转、内容单一 | 内容列表、多入口 |
规则:如果 Widget 同时使用了
Link和.widgetURL(),则Link区域点击时使用 Link 的 URL,非 Link 区域点击时使用.widgetURL()的 URL。
4. Button(intent:) + openAppWhenRun
iOS 17+ 中,Button(intent:) 也可以在执行业务逻辑后打开 App:
swift
struct OpenDetailIntent: AppIntent {
static var title: LocalizedStringResource = "查看详情"
static var openAppWhenRun: Bool = true
@Parameter(title: "项目ID")
var itemID: String
func perform() async throws -> some IntentResult {
// 可以通过共享容器传递上下文,让 App 启动时知道要导航到哪里
let defaults = UserDefaults(suiteName: "group.com.example.app")
defaults?.set(itemID, forKey: "pending_deeplink_id")
return .result()
}
}
swift
// View 中使用
Button(intent: OpenDetailIntent(itemID: "123")) {
Text("查看详情")
}
5. 系统尺寸下的小屏适配
systemSmall 空间有限,只适合放一个跳转入口:
swift
struct SmallWidgetView: View {
var entry: Entry
var body: some View {
Link(destination: URL(string: "myapp://main")!) {
VStack {
Image(systemName: getIcon(for: entry.type))
.font(.title)
Text(entry.title)
.font(.caption)
Text(entry.value)
.font(.headline)
}
}
}
}
systemMedium 和 systemLarge 可以容纳多个 Link,实现更丰富的导航结构。
6. 主 App 侧的完整导航处理
swift
class DeepLinkManager: ObservableObject {
static let shared = DeepLinkManager()
@Published var targetRoute: AppRoute?
func handle(url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host else { return }
// 解析路径
let pathItems = components.path.split(separator: "/").map(String.init)
switch host {
case "todo":
if let todoID = pathItems.first {
targetRoute = .todoDetail(id: todoID)
} else {
targetRoute = .todoList
}
case "weather":
targetRoute = .weather
case "settings":
targetRoute = .settings
default:
targetRoute = .home
}
}
}
enum AppRoute {
case home
case todoList
case todoDetail(id: String)
case weather
case settings
}
// 在 App 入口使用
struct MyApp: App {
@StateObject private var deepLinkManager = DeepLinkManager.shared
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(deepLinkManager)
.onOpenURL { url in
deepLinkManager.handle(url: url)
}
}
}
}
7. 常见问题
Q:Widget 中可以放多个 widgetURL 吗?
A:不能。一个 Widget 视图中只能应用一个 .widgetURL()。如果需要多个跳转目标,请使用 Link。
Q:Link 和 Button(intent:) 能共存吗?
A:可以共存。Link 负责跳转,Button(intent:) 负责操作。但如果 Button 嵌套在 Link 内部,Button 会拦截点击事件(同 UIKit 的 Hit Testing 层级规则)。
Q:点击 Widget 后如何回到主屏幕?
A:无法直接在 Widget 中实现。用户点击 Widget 后必然打开 App。如果不需要打开 App,考虑使用 Button(intent:) 而不设置 openAppWhenRun。
Q:长按 Widget 菜单中的操作和 Link 有什么区别?
A:长按菜单(contextMenu)是 iOS 17+ 的新功能,可以提供额外的快捷操作入口(通过 WidgetConfiguration.supportsContextMenuInteraction()),但与 Link 和 widgetURL 是独立的交互方式。
小结
- 简单场景用
widgetURL,复杂多入口用Link - 配合 App 侧的 Deep Link 系统实现精准导航
- iOS 17+ 的
Button(intent:)+openAppWhenRun可实现"操作后跳转"的复合流程 - 使用
UserDefaults(App Group)传递跳转上下文,让 App 启动后知道应该导航到哪个页面