iOS Widget 开发-12:Widget 深度链接与导航

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。


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)
                }
            }
        }
    }
}
特性 .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)
            }
        }
    }
}

systemMediumsystemLarge 可以容纳多个 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()),但与 LinkwidgetURL 是独立的交互方式。


小结

  • 简单场景用 widgetURL,复杂多入口用 Link
  • 配合 App 侧的 Deep Link 系统实现精准导航
  • iOS 17+ 的 Button(intent:) + openAppWhenRun 可实现"操作后跳转"的复合流程
  • 使用 UserDefaults(App Group)传递跳转上下文,让 App 启动后知道应该导航到哪个页面

上一篇iOS Widget 开发-11:Widget 交互按钮实战(iOS 17+ App Intents)

相关推荐
Daniel_Coder12 小时前
iOS Widget 开发-11:Widget 交互按钮实战(iOS 17+ App Intents)
ios·swiftui·swift·widget·link·appintents
初雪云12 小时前
没有Mac电脑,如何完成iOS应用上架?三个方案的实战对比
macos·ios
白玉cfc13 小时前
OC底层原理:消息流程探索
ios
东坡肘子13 小时前
消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136
人工智能·swiftui·swift
敲代码的鱼哇13 小时前
NFC读卡能力 支持安卓/iOS/鸿蒙 UTS插件
android·ios·harmonyos
浩宇软件开发1 天前
SwiftUI入门 10 分钟学会做一个 App 引导页
ios·swiftui·swift
90后的晨仔1 天前
SwiftUI 完全指南:从声明式 UI 到响应式架构的终点回顾
ios
90后的晨仔1 天前
SwiftUI 多线程与并发编程深度总结
ios
90后的晨仔1 天前
Combine 与系统框架集成:将响应式编程融入 Apple 生态
ios