大多数浏览器和
Developer App 均支持流媒体播放。
-
优秀小组件的原则
通过保持小组件的相关性且可定制来探索强大小组件的基础。学习如何通过时间线条目和TimelineReloadPolicies 保持小组件最新。了解如何修改您的小组件,以用于不同的演示环境和物理位置。最后,还要了解如何创建可自定义的小组件,以便用户可以根据喜好进行个性化。
资源
相关视频
WWDC22
WWDC21
WWDC20
-
下载
♪ ♪ 哈啰 我是布雷特柯托 是系统体验团队的工程师 今天我们要谈谈设计出强大小组件的 原则 深入探讨一些热门主题 协助你创造最棒的小组件 我们在iOS 14引进了WidgetKit WidgetKit让你创造出美丽 动态、适用于多平台的小组件 可以 出现在用户iOS iPadOS及macOS的主屏幕上
开始之前 如果你还没观看 WWDC 2020的影片 有一些真的很棒的内容 如《为您介绍WidgetKit》便提供了 WidgetKit框架和核心概念的介绍 《小组件边看边写》系列分三个部分 从基础到进阶渐渐增加 设计小组件的复杂度 《为小组件构建 SwiftUI视图》详细说明 如何充分利用SwiftUI 为小组件制作自适应的视图 最后 《设计绝妙小组件》 使用许多例子 探索了人类界面团队的设计考量
今天 我们会触及 强大小组件的两个主题 保持小组件的关联性 以及允许自定小组件 先从关联性开始讲起 我们将深入讨论 三种关联性 让你的小组件跟上趋势 并且可以适应所处环境 我们会谈到时间、呈现 及位置的关联性 时间轴是WidgetKit的核心 这是让小组件一整天 都保持关联性的核心机制 时间轴是由一或多个时间轴条目组成 在这个例子里 你可以看到三个条目 分别在早上9:00、9:30和10:00 系统向你的小组件要求时间轴 得到的时间轴会被归档 各条目 未来会在你所指定的特定时间点呈现 这让系统随时预备好你的用户界面 在用户需要时 就可以马上秀出来 我们来看看一些时间轴的例子 从最简单的开始 看看里面包含哪些内容
这里列出了一个最简易的时间轴例子 只有一个条目:屏幕使用时间 由于屏幕使用时间无法预测或预知 未来的数据 完全是以用户装置 被使用的数据历史为依据 所以真的很难好好在时间轴 运用多条目 因此就只用了单一条目 这是你所能拥有最简易的时间轴 话虽如此 并非所有的小组件 都只能有单一条目 大部分的情况下 我会鼓励你考虑思考 何不 在时间轴提供多个条目 假如你有未来展望性质的内容 有重要的日期或期限 或是内容 可以预测未来 那么你的小组件真的 应该在时间轴利用多个条目 我们来看天气小组件的例子 可以看到 天气的时间轴提供了 一整天的多个条目 第一个条目最准确 因为显示的是当下的天气 其他后续的条目所显示的 则是当天稍晚的预报数据 这个额外的预报数据超级有用 因为小组件刷新不保证会刚刚好 在你指定的时间进行刷新 如果刷新没有刚刚好在理想的 目标时间出现 现在系统也有 额外的预报内容 可以应付用户体验
这是照片的范例 你可以看见 照片的时间轴 提供了好几张会在一天当中特定时间 出现的个人与相关照片 这些照片都是我非常美好的回忆 小组件虽然一天只刷新几次 却真的会让人觉得 它很有生命 那是因为 时间轴的多个条目一直提供新鲜内容 这个例子显示 即便你没有 像天气那样可以预测的数据 从照片的例子就可以看出 你还是能 加入用户觉得有关联的内容 带来惊喜的感受 方法就是利用 时间轴的多个条目
某些小组件被观看的次数 比其他小组件多 因此我们决定给小组件 一个更新公平因子 我们把这称作更新预算 预算在一整天的期间分配、累积 并大大受到用户的观看习惯影响 常被观看的小组件一天大约 会收到40到70则之间的 后台更新 若平均分布的话 约莫等于用户清醒时间 每15到30分钟就会更新一次 然而 当然不是所有刷新都必须 这样平均分布 我们的目标是要 为不同要求支持 不同的更新节奏 例如 你可能有一个运动小组件 平常大致上闲置 但如果最爱的球队 即将进行一场比赛 可能就会在赛前 赛中、甚至赛后收到爆量的分数更新
WidgetKit很聪明 可能会在 用户长时间未使用 装置时暂停更新 像是用户在睡觉时 除此之外 小组件更新也可能 在特定小组件出现可用预算之前暂停 但是说了这么多 刷新并不是 每秒钟都在进行 小组件的重点不是要创造 一个主屏幕上的实时运作体验 你的小组件有很多方式 会在一天当中进行刷新 我们会快速一一带过 了解这些方式 其运作的机制 还有 刚刚介绍的预算会不会受到影响 第一个是TimelineReloadPolicy 也就是WidgetKit的API 这是让刷新 自动出现的核心机制 当你提供一个时间轴 你也会顺带提供刷新政策 刷新政策会告知系统你何时希望 小组件在后台自动刷新 这些自动更新会计入 小组件目前可用的预算
下一个是WidgetCenter刷新API 这个API会在某个 让小组件现有的数据 变得无效的事件发生时刷新小组件 正常来说 要求使用此API 会消耗可用预算 表示更新 只会在有可用预算时发生 然而 有一些特殊例外情况 会让这类刷新立刻发生且不消耗预算 这指的是你的容器app在用户前台 或你的app正在参与用户会话 像是导航或现正播放音讯的时候
这个API真的很能补充 TimelineReloadPolicy API的 自动后台更新 小组件也会在 位置发生重大改变时进行更新 系统侦测到重大的位置改变 且你的小组件有使用位置时 系统就会给你不用消耗预算的更新 刷新会发生在用户 下次观看小组件的时候 这样位置才能适当获得解决 注意 这不保证会发生在 位置出现改变的那一刻 而是会发生在下次用户观看小组件时 我们稍后会提到更多关于位置的部分 如果系统的表现环境改变了 也会刷新小组件 例如 可能用户改变了 某个无障碍的喜好设定 如动态文字或粗体文字 语言或地区出现改变 iCloud或App Store出现改变 重大的时间改变等等 最后 基于预算的缘故 如果用户有一个小组件 很少被看到 它收到更新的次数 可能没有你这位开发者希望的还要多 系统知道你的时间轴的日期 你偏好的刷新时间 以及用户的整体观看历史 系统如果认为用户观看数据时 数据已经过时 有可能会进行 不用消耗预算的刷新 当然 这些系统主动进行的更新 都不会消耗预算 刚刚才提过 每一个时间轴都有搭配 一个刷新政策 描述 何时应该在后台自动更新 WidgetKit提供了三个选择:atEnd afterDate及never 这三个刷新政策哪一个 适合你的小组件? 我们就来一一说明 使用一些例子阐述 然后谈谈 每一个应该留意什么陷阱
第一个要讨论的是atEnd刷新政策 此政策会让小组件在时间轴 来到尾声时合乎刷新的条件 也就是最后一个条目出现的时候 在这个例子中 指的就是10:30 AM 注意 这个时间只是 小组件合乎刷新条件的时间起始点 不代表小组件一定会 刚好在这个时间刷新 此外 如果是时间轴 只有单一条目的小组件 使用atEnd 像是前面提过的 屏幕使用时间 那么系统就会 为小组件选择适当的时间 假如小组件本来就有 超出目前时间轴寿命 以外的内容 就适合使用atEnd 我喜欢把这比喻成 从窗口观看内容 我们来看一个例子 这里列出了6月7日 日历小组件目前的时间轴 总共有四个条目 我有收录过去和未来的内容 以便展示窗口 我们把时间轴换成6月8日 你就可以知道 我说从窗口观看内容是什么意思
6月9日的时间轴也是 我们只是在改变 对用户所提供 原本就可用的数据的观点 这真的是能让atEnd政策 大放异彩的一类内容
使用atEnd政策的小组件包括 提醒、日历、照片、提示等 同样 这些小组件全都拥有无限内容 从一个窗口就能观看之 这不是很适合单一条目的时间轴 因为系统会替你选一个刷新时间 而那可能不是你希望的 如果你的时间轴具有随着时间过去 会失去关联性或准确性的 投射内容 也不建议使用atEnd 我们希望你的内容 能尽量有最大的关联性 如果等到时间轴尾声 事件已经不再具有关联性了 才可以进行更新 那就无法带来最棒的用户体验 以上讲的是atEnd 现在来谈谈afterDate AfterDate刷新政策会让小组件 在指定的日期过后合乎刷新条件 这个政策使你可以完全掌控 合乎刷新条件的时间 在这个例子里 我们假设时间轴 从9:00 AM到大约 11:00 AM之间是有效的 由于小组件的预报 数据在9:45 AM左右 开始失去准确性 所以选择了 9:30 AM做为小组件的刷新政策日期 这让小组件在9:30后还有不少时间 可以显示预报数据 即使它没有马上在9:30刷新
在一整天的期间可能出现无法预测 或难以预料的改变的内容 抑或是准确性 或关联性会出现周期性改变的数据 最适合afterDate
使用afterDate的小组件例子 包括股票、天气、新闻、邮件等 这些小组件的内容全都可能 在一整天的期间出现 无法预测或难以预料的改变 关于afterDate 有一些 潜在的问题真的要很小心 小心几近立即的刷新 指定一分钟左右以后的日期可能有效 但通常只限定在非常狭窄的时间窗口 要求过多这种解析程度的刷新 可能会使你之后没了刷新预算
跨装置设定小组件的刷新日期 也要小心 举例来说 美国股市 在东岸的9:30 AM开市 你排定刚好9:30 AM进行刷新 以便从服务器撷取当天的初始数据 别忘了 你的小组件可能装在数千 甚至数百万个设备上 而且还可能 每一个设备都有好几次 假如你真的非得要拥有像这样 时间对齐的数据 你真的应该考虑 除了具快取服务器之外 还要在这些数据 添加某些程度的随机抖动 这些都是特别重要的考虑因素 可避免任何意外或不必要的支出 最后来谈谈我最喜欢的刷新政策 也就是never Never刷新政策是最简单的 因为它从不自动刷新 假如小组件的内容只在容器app 在用户前台时或者只通过 推送通知之类的 离散事件才会发生改变 那么never就很适合你的小组件 使用never政策时 要让小组件 更新到最新状态 只能使用容器 或其他配件外挂的 WidgetCenter刷新API 这会让刷新只在必要时发生 对预算和用户的电池寿命 造成最小的影响 如果小组件要求用户在app内达成 一个尚未满足的明确条件 这时 也很适合使用never 像是登入一项服务 或是购买特定内容 如果你的小组件 在这类app内的条件达成前 无法产出有意义的内容 就可以考虑never 使用never政策的小组件例子 包括TV、笔记、音乐 Podcasts、联络人等等 这些小组件和app都要求 用户跟app互动来带动内容改变 或者他们接收这些内容更新的推播 总而言之 请善加利用时间轴条目 为你的小组件选择正确的刷新政策 使用WidgetCenter的刷新API 刷新离散事件的时间轴事件 好 接着来谈谈呈现的关联性 你的小组件可能会要在iOS或macOS的 特定背景下呈现 导致 小组件的外观改变 小组件有时可能在没有任何时间轴 更新的情况下被重新表现 强大的小组件一定要能 恰当地适应这些表现环境 我们会谈到色彩的部分 即浅色和深色模式 还有iOS 15 新推出的部分隐私隐匿 最后是全面的隐私隐匿
随着系统的设定改变 WidgetKit 会自动处理浅色与深色 模式切换所造成的内容变更 这是因为我们运用了SwiftUI的力量 想想你希望你的小组件 在这些背景下分别长什么样子 这里以笔记和日历为例 看看它们在浅色和深色模式中的样子 但是要记住 不适所有的小组件都得 通过改变背景和文字色彩 来顺应浅色和深色模式 有几个小组件不会改变颜色 像是音乐和股票 如果你的小组件设计对于高对比度 内容和深色及浅色风格具有高兼容性 那你可以继续 使用适合你的小组件的颜色
你也可以用Xcode Previews 预览小组件在Xcode的色彩变更 这里快速地示范预览 浅色模式的systemSmall小组件
切换成深色模式 只需要加入colorScheme 环境置换码 就能预览 小组件在这个环境的样子 如果你想要跟这个 例子一样 使用系统针对 浅色和深色模式设计的标准背景色 就用BackgroundStyle的色块
小组件也会受制于隐私敏感的 表现环境 如iOS的锁定屏幕 在iOS 15 小组件可以 在这些情况下隐匿部分内容 这听起来可能很难理解 所以我举了一个例子 让你明白我到底在讲什么
假设我们有一个银行小组件 显示了某个户头的可用余额 在这个例子中 我的账户有$128.45可以用
现在 我们到锁定屏幕锁定装置 账户余额还是会显示在iOS 14 因为WidgetKit没办法在装置锁定时 动态隐匿余额信息 然而 这在iOS 15已经不同了 你现在可以 在这样的情况下标注特定视图来隐匿 我来示范要怎么做 你只需要 在余额的文字插入 视图修饰符.privacySensitive 就像这里显示的 没有密码锁定时 视图仍会如你预期地 显示出余额 但是当我们锁定了装置 余额就被遮蔽起来
另外也要注意 这个修饰符 可以用在任何视图 包括hstack 和vstack等容器视图 如果用在容器 整个容器都会隐匿
假如你的app运用了完全的数据保护 在iOS装置使用密码锁定时不可碰触 例如你的app可能有使用到健康数据 所以你就不制作或害怕包含小组件 因为你以为数据一定 会显示在锁定屏幕上 那么这个功能就真的很适合你
WidgetKit可以自动 以占位内容取代活跃的时间轴内容 在装置被密码锁定时 完整地隐匿内容 它甚至可以在装置 被密码锁定的期间阻挡更新 当你采取这里所列出的 default-data-protection权限 就能做到这些 你的时间轴数据会根据 你所希望的储存在装置的数据类 我们真的很认真看待你的数据隐私 最后 我想谈谈位置的关联性 就像app一样 小组件也能提供 跟目前实际位置 或纯粹在某方面与用户 有关联性的位置 相关的背景信息
如果你的app有在正常使用位置功能 那么你的小组件也应该这么做 由于小组件可能在主屏幕 和Mac的通知中心拥有多个实例 可以考虑让小组件除了使用 当下位置之外 也提供预先选好 甚至是可搜寻的位置 就像天气小组件那样 借助Intent的力量 用小组件撷取当下位置 只需要几个步骤 首先 你需要在 Info.plist指定 NSWidgetUsesLocation 这会让系统知道 你将在你的小组件外挂使用位置 第二 照正常方式使用 CLLocationManager 但这次是从小组件 外挂的TimelineProvider 考量小组件所需的分辨率 解析粗略位置比较迅速 而且不需要超精准位置的话 用户体验也会较好 大体上 位置要求得越精准 解析时间就会越久
最后 在CLLocationManager 使用isAuthorizedForWidgetUpdates API检查小组件是否授权位置更新 这会告诉你用户是否 已授权让小组件使用位置 既然说到授权 我们来深入探讨小组件的 位置授权 看看是怎么运作的 小组件的授权通常是跟 app容器共享的 这里列出了设定app 所有的位置授权选项
如果用户选择“使用App时” 就只有在容器app 位于用户前台或导航期间等 把app视为使用中的情况下 小组件才能取得用户位置
如果用户选择 “使用App或小组件时” 那么可取得位置的时机就跟选择 “使用App时”一样 但是还另外 容许了小组件取得 这授权小组件在最后一次 被观看之后 还有15分钟可接收位置 如果设定这两种授权的任一种 都无法解析位置 可以考虑替代内容 甚至有办法取得的话 考虑先前内容 或是干脆表明 位置无法解析
如果用户选择“一律允许”授权 小组件就永远被授权 可取得位置 最后 我们来谈谈制作 可自定的小组件这件事 我们会讲到大小、类型和配置 等让用户能个人化 小组件体验的方式 小组件有各种大小 我们建议你支持越多种大小越好 这样用户摆放小组件时才有选择 要记住 装置之间 在大小上的确存在些微差异 可以的话最好使用系统标准填充 边距、字型和字体大小
看看这个iPad 真的就像是被改造成一个内容展示柜 如果你还没看出来 iOS 15多了一种新的大小 专门为iPad设计 我们叫它“特大”
抓出来让你看清楚一点 它的高度跟大号的一样 但宽度更宽 能展现更多iPad可用的内容 这就是把它加到小组件的方法 我把它加进本就支持systemLarge 家族的现有小组件配置中 在预设的情况下 如果你不在 小组件配置中指定支持的家族 只要你是用iOS 15 SDK 或之后的版本来制作 就会自动支持这个新大小
下一个个人化的面向 是小组件的类型 小组件的类型为数据和内容 提供了不同的观点 你要想想什么类型的小组件 最适合你的app 说明这点最好的方法 就是举例 我们以时钟为例 左边的是城市小组件 会追踪单一城市的时间 右边则是世界时钟小组件 可以在同一个视图 容纳许多城市 股票是另一个例子 左边的是股票符号小组件 显示某一股票的价格 这里显示Apple的股价 右边则是总览小组件 总览小组件会显示反映在 用户股票app的一系列股票 要怎么发表不同类型的小组件? 发表多种类型的小组件超级简单
以前面的股票为例 我在左上方有一个符号小组件 和一个总览小组件 要发表这些类型 你只要把不同的 小组件配置加到WidgetBundle对象 并把@main属性放入WidgetBundle 就这么简单 注意 小组件在组合里 被定义的顺序也很重要 这个顺序会影响小组件 在小组件画廊呈现的方式 所以务必确保列出来的第一个小组件 是你的英雄使用案例 最后 一旦app安装好了 就不可能动态发表或动态撤回 小组件的可用选择 因此 你应该特别谨慎思考 小组件长时间的支持
自定的最后一个面向 就是配置 小组件支持两种类型: 静态和Intent配置 静态小组件配置 会为所有小组件实例传递同样的内容 还记得前面提到的股票总览例子吗? 每一个都会显示一模一样的内容 就算是在用户装置里的不同位置 静态小组件超级简易 也是很优雅的小组件定义 因为用户完全不需要进行配置或设定
另一方面 Intent小组件配置 为每一个实例传递用户配置的内容 看看右边的这些股票符号小组件 这位用户配置了许多不同的实例 每一个都显示不同的股票 这东西很美妙 用户甚至可以把这全部组成一个叠放 放在主屏幕滑动浏览 节省空间 你可能会问用户要怎么进行配置 以用户的观点来说 系统会提供所有的周边UI物件 轻点小组件进入编辑模式后 若有支持Intent 就会出现配置盘 上面会列出用户可以配置的 Intent参数 在这个例子里 符号小组件有一个参数 可以追踪 这个值 目前是AAPL这个字符串 代表Apple股票 用户轻点参数 就会出现输出控制器 搜集用户的输入 完成后 小组件就会自动更新了 Intent可以让你进行一堆自定 WWDC 2020有一个很棒的影片 我等一下会分享给你 想要 Intent配置的话 可以从那里着手 现在 我们先快速地点出 撰写Intent配置的小组件 跟撰写静态配置的小组件有哪些差异
静态配置真的很简单 只要使用标准的TimelineProvider 放入StaticConfiguration对象 我们来比较一下Intent配置有何不同 唯一的差别是用IntentConfiguration 取代StaticConfiguration 用IntentTimelineProvider 取代TimelineProvider 这些都会稍微更动界面 以便支持Intent 这样在填入 时间轴时 就可以 收到用户配置的Intent 想要更深入了解如何为小组件创造 及配置Intent 包括支持的 各种数据类型总览 和在配置UI中看起来是什么样子 请观看WWDC 2020的 《为小组件添加配置和智能》影片
总结而言 我们讨论了强大的小组件 如何运用时间轴的条目 掌握刷新政策以及顺应表现 和可能的实体环境 还有 提供不同的大小、类型 及配置 让客户拥有动态 且可以个人化的体验 谢谢
-
-
15:46 - Xcode Previews for Widget Views with Color Scheme Overrides
struct MyWidgetEntryView : View { var date: Date var body: some View { ZStack { Rectangle().fill(BackgroundStyle()) VStack { Text("Hello") } } } } struct MyWidget_Previews: PreviewProvider { static var previews: some View { MyWidgetEntryView(date: Date()) .previewContext(WidgetPreviewContext(family: .systemSmall)) .environment(\.colorScheme, .dark) } }
-
16:34 - Widget Partial Privacy Redactions - Banking Example
struct MyWidgetEntryView : View { var body: some View { ZStack { Rectangle().fill(BackgroundStyle()) VStack(alignment: .leading) { Text("Balance") .font(.largeTitle) .fontWeight(.bold) .foregroundColor(Color.blue) Text("$128.45") .privacySensitive() .font(.title2) .foregroundColor(Color.gray) } } } }
-
23:08 - WidgetBundle Example
struct IndividualSymbolWidget : Widget { var body: some WidgetConfiguration { … } } struct StocksOverviewWidget : Widget { var body: some WidgetConfiguration { … } } @main struct MyWidgetBundle: WidgetBundle { var body: some Widget { // Order of these widgets defines the order in the Widget Gallery IndividualSymbolWidget() StocksOverviewWidget() } }
-
25:43 - Static Widget Configuration Example
@main public struct SampleWidget: Widget { public var body: some WidgetConfiguration { StaticConfiguration(kind: "com.sample.myStaticSampleWidgetKind", provider: Provider()) { entry in SampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } public struct Provider: TimelineProvider { public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let entry = SimpleEntry(date: Date()) // TODO: Generate a timeline entry completion(timeline) } }
-
25:55 - Intent Widget Configuration Example
@main public struct SampleWidget: Widget { public var body: some WidgetConfiguration { IntentConfiguration(kind: "com.sample.myIntentSampleWidgetKind", intent: SampleConfigurationIntent.self provider: Provider()) { entry in SampleWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } } public struct Provider: IntentTimelineProvider { public func timeline(for configuration: SampleConfigurationIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) // generate a timeline completion(timeline) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。