1.1 Backyard Birds:用 SwiftData 和小组件构建应用

原文链接: https://developer.apple.com/documentation/swiftui/backyard-birds-sample

1.1 Backyard Birds:用 SwiftData 和小组件构建应用

创建一个带持久化数据、可交互小组件和全新应用内购买体验的应用。

概述

Backyard Birds 提供了一个丰富的环境,让你观察飞临后院花园的鸟儿。你可以监控它们的水和食物供应,确保它们总有清水喝、总有东西吃;也可以借助应用内购买来升级游戏,为鸟儿提供更美味的食物。

这个示例用 SwiftData 实现数据模型以完成持久化,并通过 Observation 协议与 SwiftUI 无缝集成。游戏的小组件实现了 App Intents,从而提供可交互、可配置的小组件。应用内购买体验使用了 StoreKit 中的 ProductView 和 SubscriptionStoreView。

你可以在 GitHub 上获取这个示例的源代码。

注意:这个示例代码项目与 WWDC23 的 102 号场次 State of the Union 相关。

配置示例代码项目

要配置 Backyard Birds 应用以便在你的设备上运行,请按以下步骤操作:

  1. 用 Xcode 15 或更高版本打开该项目。
  2. 编辑多平台目标的 scheme,在 Options 标签页中选择 Store.storekit 文件作为 StoreKit 配置。
  3. 对 watchOS 目标的 scheme 重复上一步。
  4. 选中顶层的 Backyard Birds 项目。
  5. 对所有目标,在 Signing & Capabilities 面板的 Team 菜单中选择你的团队,让 Xcode 自动管理你的描述文件。

创建数据驱动的应用

应用通过 Model() 宏让模型对象遵循 PersistentModel,从而定义它的数据模型。把 Attribute(_:originalName:hashModifier:) 宏与 unique 选项一起使用,可以确保 id 属性是唯一的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Model public class BirdSpecies {
    @Attribute(.unique) public var id: String
    public var naturalScale: Double
    public var isEarlyAccess: Bool
    public var parts: [BirdPart]
    
    @Relationship(deleteRule: .cascade, inverse: \Bird.species)
    public var birds: [Bird] = []
    
    public var info: BirdSpeciesInfo { BirdSpeciesInfo(rawValue: id) }
    
    public init(info: BirdSpeciesInfo, naturalScale: Double = 1, isEarlyAccess: Bool = false, parts: [BirdPart]) {
        self.id = info.rawValue
        self.naturalScale = naturalScale
        self.isEarlyAccess = isEarlyAccess
        self.parts = parts
    }
}

构造可交互小组件

当水和食物即将耗尽时,Backyard Birds 会呈现一个用于补充后院物资的 Button,从而展示可交互小组件。应用的做法是把一个 Button 放进小组件的视图里,并把 ResupplyBackyardIntent 实例传给 init(intent:label:) 构造器:

1
2
3
4
5
6
7
8
Button(intent: ResupplyBackyardIntent(backyard: BackyardEntity(from: snapshot.backyard))) {
    Label("Refill Water", systemImage: "arrow.clockwise")
        .foregroundStyle(.secondary)
        .frame(maxWidth: .infinity)
        .padding(.vertical, 8)
        .padding(.horizontal, 12)
        .background(.quaternary, in: .containerRelative)
}

应用通过实现 WidgetConfigurationIntent 协议,让小组件可以配置:

 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
31
struct BackyardWidgetIntent: WidgetConfigurationIntent {
    static let title: LocalizedStringResource = "Backyard"
    static let description = IntentDescription("Keep track of your backyards.")
    
    @Parameter(title: "Backyards", default: BackyardWidgetContent.all)
    var backyards: BackyardWidgetContent
    
    @Parameter(title: "Backyard")
    var specificBackyard: BackyardEntity?
    
    init(backyards: BackyardWidgetContent = .all, specificBackyard: BackyardEntity? = nil) {
        self.backyards = backyards
        self.specificBackyard = specificBackyard
    }
    
    init() {
    }
    
    static var parameterSummary: some ParameterSummary {
        When(\.$backyards, .equalTo, BackyardWidgetContent.specific) {
            Summary {
                \.$backyards
                \.$specificBackyard
            }
        } otherwise: {
            Summary {
                \.$backyards
            }
        }
    }
}

提供全新的应用内购买体验

示例应用使用 ProductView 在货架上显示若干可以购买的不同鸟食升级项。为了突出展示某个应用内购买项,应用使用了 productViewStyle(_:) 修饰符:

1
2
3
4
5
6
7
ProductView(id: product.id) {
    BirdFoodProductIcon(birdFood: birdFood, quantity: product.quantity)
        .bestBirdFoodValueBadge()
}
.padding(.vertical)
.background(.background.secondary, in: .rect(cornerRadius: 20))
.productViewStyle(.large)

Backyard Birds Pass 页面使用 SubscriptionStoreView 视图显示可续期订阅。应用把 PassMarketingContent 视图用作 SubscriptionStoreView 的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SubscriptionStoreView(
    groupID: passGroupID,
    visibleRelationships: showPremiumUpgrade ? .upgrade : .all
) {
    PassMarketingContent(showPremiumUpgrade: showPremiumUpgrade)
        #if !os(watchOS)
        .containerBackground(for: .subscriptionStoreFullHeight) {
            SkyBackground()
        }
        #endif
}