12

WWDC20 - Build SwiftUI views for widgets

 3 years ago
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.
neoserver,ios ssh client

WWDC20 Session 10033 - Build SwiftUI views for widgets

本文知识目录

Build SwiftUI views for widgets

本文属于 WWDC20 中 Widgets 系列的文章。Widgets 是使用 SwiftUI 进行开发的,如果您对 SwiftUI 还不是很了解请翻看 Introduction to SwiftUI。如果希望先了解更多 Widgets 请移步文末的相关链接。

SwiftUI in widgets

00-opportunityToLearnSwiftUI

我们可以为 Widgets 提供基于时间线的内容,它将在适当的时机展示在 iOS 14 或 macOS Big Sur 系统的主屏幕上。得益于 SwiftUI 良好的屏幕自适应能力,Widgets 可以在不同的系统上展示出不错的效果。因此,尽管我们的应用程序发布在旧版本的系统中 (无法使用 SwiftUI),Widgets 也将是我们学习和使用 SwiftUI 的绝佳机会。

Widgets 的展示效果如下:

01-SwiftUI&Widgets

事不宜迟,让我们直接通过 Demo 来体验如何使用 SwiftUI 来开发一个 Widgets

Caffiine Drink Demo

我们将要创建一个应用程序,用于记录和跟踪我们所喝的含咖啡因的饮料,并估算出我们体内的咖啡因含量。 应用如下:

03-coffeineDrinks

而 Widgets 正好可以作为该应用程序的重要补充,以便能够一目了然地知道我们当前的咖啡因含量。

这个是我们希望展示的 Widget 效果:

04-coffeineDrinksItem

首先,可以看到它的视觉和配色方案与我们的应用程序相同。

在顶部,我们显示了我们体内目前的咖啡因含量,底部记录的是我们喝的最后一杯咖啡的时间。请注意 ⚠️,咖啡因含量的背景形状与 widget 形状是同心圆的关系,它可是使用我们新出的 API 来实现的哦。

最后,也希望 widget 底部的持续时间能够实时更新以始终正确。

Hello Widget!

我们先新建一个项目,可以是 iOS、macOS 或 Multiplatform:

mutliplatform

接着需要再新建一个 widge 的 target,这里我们选择基于 iOS 项目的 widget:

01-xcode-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 😄 。

02-HelloWidget

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 LazyVStackText 来承载我们新添的数据。效果如下:

04-previewData

有了基本结构后,我们再丰富一下。先加点背景色,这里我们用 ZStack 来包装:

1
2
3
4
5
6
7
var body: some View {
  	ZStack {
      Color("cappuccino")

      LazyVStack(alignment: .leading) { ... }
   }
}

让我们把 drinkNamedrinkDate 也展示出来:

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
}

再来看一眼效果:

05-allDataUsage

Restructuring

接下来,我们稍微调整一下代码结构,将内部的两个 LazyStack 抽离成单独的 View。这里可以用 Xcode 的 Extra Subview 来帮我们完成工作,我们只需将 🖱️移动到 LazyStack 上,按住 Command 并点击 🖱️ 就可以弹出如下窗口:

06-Extrack Subview

调整后,我们的结构就清爽很多了:

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

记得为我们新加的 CaffineAmountViewDrinkView 加上 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

07-LazyVStack

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 的倒角变化效果如下:

08-adjust padding

##PreviewProvider

我们来看看 Widge 在不同场景下的实际效果。

DarkMode

首先是 .envrioment 修饰符在 DarkMode 下的效果,这个其实完全由我们配置的 ColorSet 来保证的。

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

让我们先来看看这两种情况的效果:

09-previewSize

Placeholder

今年的 SwiftUI 还提供了一个特别棒的功能,我们可以通过 isPlaceholder 修饰符来设置骨架图。

11-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 的情况下,为其添加了一张最后喝过的咖啡的图片。

效果如下:

12-widgetFamily

New APIs

最后一节,再来回顾一下我们使用到的新 API

DateStyle

06-dateTeimAPI

ContainerRelativeShape

05-relativeShape

文末福利

由于本文的 Session 并未提供相应的 SampleCode。本人在文章撰写的过程中,顺带写了完整的 Demo,如有需要的同学可以自取:Github。⚠️ 注意,在有的 macOS 系统上通过 Xcode 12 Beta 运行后可能无法在模拟器上查看 widget,不过在 Xcode 的 sidebar 预览,完整效果建议在真实设备体验。

widget.gif
  • 苹果通过独立的 taget 将 Widget 与主项目隔离开来,很好的支持了 Widget 发展,因为不会有历史包袱。
  • Widge 的定位还是比较清晰的,用于弥补宿主应用程序无法及时展示用户所关心的数据,而 Widget 正好很适合展示。
  • 配合越来越丰富的 SwiftUI 的功能和性能的提升,Widget 也能够与现有的苹果生态更好的集成。
  • Widget 也使得苹果在 iOS 和 macOS 生态的融合了也更进了一步。

知识点问题梳理

  1. Widget 支持在所有的苹果系统中使用吗 ?
  2. 本文的示例中使用了哪几种新 API,试着描述它们的功能 ?
  3. 本文的示例展示了几种环境描述符 ?

Widges 相关视频


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK