WWDC20 - Build SwiftUI views for widgets
source link: https://looseyi.github.io/post/swiftui/wwdc20-session-10033/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
WWDC20 Session 10033 - Build SwiftUI views for widgets
本文知识目录
本文属于 WWDC20 中 Widgets 系列的文章。Widgets 是使用 SwiftUI 进行开发的,如果您对 SwiftUI 还不是很了解请翻看 Introduction to SwiftUI。如果希望先了解更多 Widgets 请移步文末的相关链接。
SwiftUI in widgets
我们可以为 Widgets 提供基于时间线的内容,它将在适当的时机展示在 iOS 14 或 macOS Big Sur 系统的主屏幕上。得益于 SwiftUI 良好的屏幕自适应能力,Widgets 可以在不同的系统上展示出不错的效果。因此,尽管我们的应用程序发布在旧版本的系统中 (无法使用 SwiftUI),Widgets 也将是我们学习和使用 SwiftUI 的绝佳机会。
Widgets 的展示效果如下:
事不宜迟,让我们直接通过 Demo 来体验如何使用 SwiftUI 来开发一个 Widgets
Caffiine Drink Demo
我们将要创建一个应用程序,用于记录和跟踪我们所喝的含咖啡因的饮料,并估算出我们体内的咖啡因含量。 应用如下:
而 Widgets 正好可以作为该应用程序的重要补充,以便能够一目了然地知道我们当前的咖啡因含量。
这个是我们希望展示的 Widget 效果:
首先,可以看到它的视觉和配色方案与我们的应用程序相同。
在顶部,我们显示了我们体内目前的咖啡因含量,底部记录的是我们喝的最后一杯咖啡的时间。请注意 ⚠️,咖啡因含量的背景形状与 widget 形状是同心圆的关系,它可是使用我们新出的 API 来实现的哦。
最后,也希望 widget 底部的持续时间能够实时更新以始终正确。
Hello Widget!
我们先新建一个项目,可以是 iOS、macOS 或 Multiplatform:
接着需要再新建一个 widge 的 target,这里我们选择基于 iOS 项目的 widget:
生成之后,让我们配上 Hello Widget!
,并在 PreviewProvider
处添加上 WidgetPreviewContext:
1
2
3
4
5
6
7
8
9
struct CafeineDrinkWidget_Previews: PreviewProvider {
static var previews: some View {
Group {
CafeineDrinkWidgetEntryView(entry: .preivewEntry, data: .previewData)
}
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
我们目前提供的 widget 样式分为三种:
1
2
3
4
5
6
7
8
public enum WidgetFamily : Int, RawRepresentable, CustomDebugStringConvertible, CustomStringConvertible {
/// A small widget.
case systemSmall
/// A medium-sized widget.
case systemMedium
/// A large widget.
case systemLarge
}
让我们一起来看看最简版本的 widget 😄 。
Data Configuration
接着,让我们把 CaffeineWidgetData
配置起来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct CaffeineWidgetData {
public let drinkName: String
public let drinkData: Date
public let caffeineAmount: Measurement<UnitMass>
}
extension CaffeineWidgetData {
public static let previewData = CaffeineWidgetData(
drinkName: "Cappuccino",
drinkData: Date().advanced(by: -60 * 29 + 5),
caffeineAmount: Measurement<UnitMass>(value: 56.23, unit: .milligrams)
)
}
接着再加上简单的 View LazyVStack
和 Text
来承载我们新添的数据。效果如下:
有了基本结构后,我们再丰富一下。先加点背景色,这里我们用 ZStack
来包装:
1
2
3
4
5
6
7
var body: some View {
ZStack {
Color("cappuccino")
LazyVStack(alignment: .leading) { ... }
}
}
让我们把 drinkName
和 drinkDate
也展示出来:
1
2
3
4
5
6
7
8
9
Text("\(data.drinkName) ☕️")
.font(.body)
.bold()
.foregroundColor(Color("milk"))
/// NOTE: *New* Date provider API
Text("\(data.drinkDate, style: .relative) ago")
.font(.caption)
.foregroundColor(Color("milk"))
注意,这里我们使用了 SwiftUI 新出的 DateStyle
API
DateStyle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public struct DateStyle {
/// A style displaying only the time component for a date.
/// Example output:
/// 11:23PM
public static let time: Text.DateStyle
/// A style displaying a date.
/// Example output:
/// June 3, 2019
public static let date: Text.DateStyle
/// A style displaying a date as relative to now.
/// Example output:
/// 2 hours, 23 minutes
/// 1 year, 1 month
public static let relative: Text.DateStyle
/// A style displaying a date as offset from now.
/// Example output:
/// +2 hours
/// -3 months
public static let offset: Text.DateStyle
/// A style displaying a date as timer counting from now.
/// Example output:
/// 2:32
/// 36:59:01
public static let timer: Text.DateStyle
}
再来看一眼效果:
Restructuring
接下来,我们稍微调整一下代码结构,将内部的两个 LazyStack
抽离成单独的 View。这里可以用 Xcode 的 Extra Subview
来帮我们完成工作,我们只需将 🖱️移动到 LazyStack
上,按住 Command
并点击 🖱️ 就可以弹出如下窗口:
调整后,我们的结构就清爽很多了:
1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
ZStack {
Color("cappuccino")
LazyVStack(alignment: .leading) {
CaffineAmountView(data: data)
Spacer()
DrinkView(data: data)
}
.padding(.all)
}
}
记得为我们新加的 CaffineAmountView
和 DrinkView
加上 CaffeineWidgetData
数据。
CaffineAmountView
最后再用我们的另一个新的 API 来修改 CaffineAmountView
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var body: some View {
LazyHStack {
VStack(alignment: .leading) {
Text("Caffeine")
.font(.body)
.foregroundColor(Color("espresso"))
.bold()
Text(Formatter.measurement.string(from: data.caffeineAmount))
.font(.title)
.bold()
.foregroundColor(Color("espresso"))
.minimumScaleFactor(0.8)
}
Spacer(minLength: 0) // #1
}
.padding(.all, 8.0)
.background(ContainerRelativeShape().fill(Color("latte"))) // #2
}
先注意一下,这里将 Spacer
的 minLength 设置为 0 是为了将整个 AmountView 的内容撑满。然后在编写 Demo 的过程中发现新增的 LazyVStack
在 layout 过程中有 bug,无法正常将内容撑满,所以这里改回用 VStack
。
ContainerRelativeShape
A shape that is replaced by an inset version of the current container shape. If no container shape was defined, is replaced by a rectangle.
由于不同大小的设备可能对其 widget 使用不同的半径,这可能会导致情况变得有些麻烦。 而新出的 ContainerRelativeShape
是一种新的形状类型,它将采用最接近父视图容器形状的路径,并根据形状的位置使用适当的倒角半径。
通过添加 ContainerRelativeShape
,我们的 AmountView 会采用与系统为 widget 定义容器形状相同的同心的角半径。如果更改主 VStack
的 padding 的值,则 AmountView 的倒角将发生变化,以使围绕它的边框在拐角曲线周围保持恒定的厚度。
例如,我们修改容器的 padding 为 10,AmountView 的倒角变化效果如下:
##PreviewProvider
我们来看看 Widge 在不同场景下的实际效果。
DarkMode
首先是 .envrioment
修饰符在 DarkMode 下的效果,这个其实完全由我们配置的 ColorSet 来保证的。
然后我们在 View 上添加 colorScheme
的环境配置就可以了。
1
2
3
CafeineDrinkWidgetEntryView(entry: .preivewEntry, data: .previewData)
.previewContext(WidgetPreviewContext(family: .systemSmall))
.environment(\.colorScheme, .dark)
SizeCategory
可通过 .envrioment
修饰符的 sizeCategory
来控制 widget 的字体大小。
1
2
3
CafeineDrinkWidgetEntryView(entry: .preivewEntry, data: .previewData)
.previewContext(WidgetPreviewContext(family: .systemSmall))
.environment(\.sizeCategory, .extraExtraLarge)
让我们先来看看这两种情况的效果:
Placeholder
今年的 SwiftUI 还提供了一个特别棒的功能,我们可以通过 isPlaceholder
修饰符来设置骨架图。
.isPlaceholder
还支持对单个视图的控制等。不过,目前的 Xcode 12 Beta1 版本还无法使用 😅。大家就且看一下吧。
WidgetFamily
最后一个预览,让我们一起看看如何通过 WidgetFamily
来控制 Widge 的展示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct CafeineDrinkWidgetEntryView : View {
...
@Environment(\.widgetFamily) var widgetFamily
var body: some View {
ZStack {
...
LazyHStack {
LazyVStack(alignment: .leading) {
CaffineAmountView(data: data)
Spacer()
DrinkView(data: data)
}
.padding(.all, 10)
if widgetFamily == .systemMedium, let phoneName = data.phoneName {
Image(phoneName).resizable()
}
}
}
}
}
这里我们在 widgetFamily
为 .systemMedium
的情况下,为其添加了一张最后喝过的咖啡的图片。
效果如下:
New APIs
最后一节,再来回顾一下我们使用到的新 API
DateStyle
ContainerRelativeShape
文末福利
由于本文的 Session 并未提供相应的 SampleCode。本人在文章撰写的过程中,顺带写了完整的 Demo,如有需要的同学可以自取:Github。⚠️ 注意,在有的 macOS 系统上通过 Xcode 12 Beta 运行后可能无法在模拟器上查看 widget,不过在 Xcode 的 sidebar 预览,完整效果建议在真实设备体验。
- 苹果通过独立的 taget 将
Widget
与主项目隔离开来,很好的支持了 Widget 发展,因为不会有历史包袱。 Widge
的定位还是比较清晰的,用于弥补宿主应用程序无法及时展示用户所关心的数据,而 Widget 正好很适合展示。- 配合越来越丰富的 SwiftUI 的功能和性能的提升,Widget 也能够与现有的苹果生态更好的集成。
- Widget 也使得苹果在 iOS 和 macOS 生态的融合了也更进了一步。
知识点问题梳理
Widget
支持在所有的苹果系统中使用吗 ?- 本文的示例中使用了哪几种新 API,试着描述它们的功能 ?
- 本文的示例展示了几种环境描述符 ?
Widges 相关视频
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK