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)
            }
        }
    }
}

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 启动后知道应该导航到哪个页面

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

相关推荐
福兮说18 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
茶底世界之下1 天前
时间线效果层,为什么不能只复用一个处理器?
ios·swift
ZZH_AI项目交付1 天前
实时字幕负责快,本地录音负责不丢:一个 iOS 录音功能为什么需要两条链路
ios·ai编程
Cc.Y1 天前
Swift深度链接的极简之道
ios·cocoa·swift
AI砖家1 天前
iPhone Duo 上架新规:旧 App 怎么兼容,还能不能继续更新?
ios·cocoa·iphone·ios上架·ios上架新规
黑科技iOS上架1 天前
小蟹iOS混淆:Objective-C项目实战
ios·审核·ios混淆·执行程序差异化
茶底世界之下1 天前
照片边缘太抢眼?用 Awaking 做一次 1:1 裁切
ios
Digitally1 天前
如何在iPhone上永久删除文件而无需恢复
ios·iphone
黑科技iOS上架1 天前
iOS二进制混淆实战:工程级指纹防护彻底解决4.3同质化拒审
ios·审核·ios混淆·执行程序差异化
传奇开心果编程2 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui