精通-IOS18-开发-全-
精通 IOS18 开发(全)
原文:
zh.annas-archive.org/md5/a4345f119d5ed6596d7b88df6838014c译者:飞龙
第一章:前言
<st c="279">UITableView</st>
本书面向对象
-
希望紧跟最新苹果技术的 iOS 高级开发者 -
希望利用团队技能的 iOS 团队技术负责人 -
希望晋升到高级开发领域的中级开发者
本书涵盖的内容
为了充分利用本书
下载示例代码文件
使用的约定
<st c="7535">文本中的代码</st><st c="7725">NavigationStack</st><st c="7777">navigationDestination</st>
enum Screen: Hashable {
case signin
case onboarding
case mainScreen
case settings
}
@State var path: [Screen] = []
struct CoordinatorView: View {
@ObservedObject private var coordinator = Coordinator()
var body: some View { <st c="8259">NavigationStack</st>(path: $coordinator.path) {
AlbumListView()
.navigationDestination(for: PageAction.self, destination: { pageAction in
coordinator.buildView(forPageAction: pageAction)
})
}
.environmentObject(coordinator)
}
}
@testable import Chapter14
取得联系
分享您的想法
下载此书的免费 PDF 副本
扫描下面的二维码或访问 以下链接

-
提交您的购买 证明 -
这就完了! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件
第一部分:iOS 18 开发入门
-
第一章 , iOS 18 的新功能 -
第二章 , 使用 SwiftData 简化我们的实体 -
第三章 , 理解 SwiftUI Observation -
第四章 , 使用 SwiftUI 进行高级导航 -
第五章 , 使用 WidgetKit 提升 iOS 应用程序 -
第六章 , SwiftUI 动画和 SF 符号 -
第七章 , 使用 TipKit 提高特性探索 -
第八章 , 从网络连接和获取数据 -
第九章 , 使用 Swift Charts 创建动态图表
第二章:1
iOS 18 新增功能
-
理解 iOS 18 背景 -
探索 Swift 测试 -
了解新的 Swift 数据改进 -
尝试新的 缩放过渡效果 -
在我们的 iPad 应用中添加浮动标签栏 -
在 SwiftUI 中对滚动视图有更多控制 -
更改文本渲染行为 -
从另一个视图定位子视图 -
进入 人工智能革命
技术要求
理解 iOS 18 背景
-
SwiftUI 正在变得更加成熟和强大。 然而,一些功能,如复杂的动画或过渡、手势处理、导航和绘图,仍然使用 SwiftUI 实现起来具有挑战性 。 -
Core Data 是大多数 iOS 开发者作为存储 持久数据的解决方案的首选框架。 -
虽然 XCTest 被认为是一个强大且方便的测试框架,但它缺乏在其他平台上常见的功能,例如参数化测试和更好的 测试组织。 -
WidgetKit 的流行证明了在当今世界,能够一目了然地展示信息的能力至关重要。
介绍 Swift 测试
<st c="4003">@Test("Test view model increment function", .enabled(if: AppSettings.CanDecrement), .tags(.critical))</st> func testViewModelIncrement() async throws {
// preparation
let viewmodel = CounterViewModel()
viewmodel.count = 5
// execution
viewmodel.increment(by: 1)
// verification <st c="4277">#expect(viewmodel.count == 6)</st> }.
介绍 Swift 数据改进
<st c="5504">Book</st>
<st c="5516">@Model</st> class Book {
var author: String
var title: String
var publishedDate: Date
}
<st c="5641">Book</st> <st c="5689">@Model</st>
唯一值
<st c="6273">Book</st> class’s unique identifier is based on combining the <st c="6330">name</st> and <st c="6339">publicationName</st> attributes.
<st c="6366">History API</st>
<st c="6378">Another new and exciting feature</st> <st c="6411">is the History API.</st> <st c="6432">Using the History API, we can fetch</st> <st c="6467">transactions and changes that have been made to our Swift Datastore over a particular time range.</st> <st c="6566">This capability allows us to update our app when we work with extensions such as widgets or sync changes to</st> <st c="6674">the server.</st>
<st c="6685">Reading the transaction history is not the only “pro” feature added to Swift Data.</st> <st c="6769">Let’s talk about Core Data for</st> <st c="6800">a second.</st>
<st c="6809">Custom data stores in Swift Data</st>
<st c="6842">Core Data fundamentals</st> <st c="6865">included the ability to work with</st> <st c="6899">any data store type we wanted – XML, SQLite, CSV files, or even a remote server.</st> <st c="6981">Although almost all apps that implement Core Data work with SQLite as their data store, it was built to be agnostic to whatever</st> <st c="7109">happens underneath.</st>
<st c="7128">Starting with iOS 18, Apple also brings custom data stores to</st> <st c="7191">Swift Data.</st>
<st c="7202">For example, let’s say that we want to base our data store on a CSV file.</st> <st c="7277">We start by creating a new data store configuration</st> <st c="7328">specifically for CSV</st> <st c="7350">data</st> <st c="7354">stores:</st>
final class CSVStoreConfiguration: DataStoreConfiguration {
typealias Store = CSVDataStore
var name: String
var schema: Schema? var fileURL: URL
init(name: String, schema: Schema? = nil, fileURL: URL)
{
self.name = name
self.schema = schema
self.fileURL = fileURL
}
static func == (lhs: CSVStoreConfiguration, rhs:
CSVStoreConfiguration) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
<st c="7804">The</st> `<st c="7809">CSVStoreConfiguration</st>` <st c="7830">class is a new data store configuration that accepts the name and the schema (similar to how Swift Data configuration setup works today), and we added an additional parameter, which is</st> `<st c="8016">fileURL</st>` <st c="8023">– the location of our</st> <st c="8046">CSV file.</st>
<st c="8055">In the</st> `<st c="8063">init()</st>` <st c="8069">function, we can also check whether the CSV file exists or whether we need to create a</st> <st c="8157">new one.</st>
<st c="8165">Notice that there’s a</st> `<st c="8188">typealias</st>` <st c="8197">named</st> `<st c="8204">Store</st>`<st c="8209">, which represents</st> <st c="8227">a new type</st> <st c="8238">called</st> `<st c="8246">CSVDataStore</st>`<st c="8258">. This is the actual store class where everything happens.</st> <st c="8317">Let’s create</st> <st c="8330">it now:</st>
最终类
typealias 配置 = <st c="8403">CSV 存储配置</st> typealias 快照 = 默认快照
var 配置: <st c="8481">CSV 存储配置</st> var 名称: String
var 架构: 架构
var 标识符: String
必须初始化(_ 配置: CSV 存储配置,
迁移计划: (任何 SchemaMigrationPlan 类型)?)
抛出 {
self.configuration = configuration
self.name = configuration.name
self.schema = configuration.schema! self.identifier =
配置文件 URL 的最后路径组件
}
}
<st c="8836">Our</st> `<st c="8841">CSVDataStore</st>` <st c="8853">class conforms to the</st> `<st c="8876">DataStore</st>` <st c="8885">protocol and has similar properties, such as</st> `<st c="8931">name</st>` <st c="8935">and</st> `<st c="8940">schema</st>`<st c="8946">.</st>
<st c="8947">The</st> `<st c="8952">CSVDataStore</st>` <st c="8964">class must handle a persistent store’s basic operations, such as inserting new items and deleting or updating</st> <st c="9075">existing ones.</st>
<st c="9089">Notice that the</st> `<st c="9106">init()</st>` <st c="9112">function includes a migration type, so we can even handle migrations when our</st> <st c="9191">schema changes.</st>
<st c="9206">To handle all of these</st> <st c="9229">operations, we need to implement two important</st> <st c="9276">methods that are part of the</st> `<st c="9306">DataStore</st>` <st c="9315">protocol –</st> `<st c="9327">fetch()</st>` <st c="9334">and</st> `<st c="9339">save()</st>`<st c="9345">:</st>
func 获取
抛出 -> 数据存储获取结果<T,默认快照>
where T : 持久模型 {
let 判断 = 请求描述符判断
返回 数据存储获取结果(描述符:
请求描述符,获取快照: [],
相关快照: [:])
. // 执行获取操作
}
func 保存(_ 请求:
数据存储保存更改请求<默认快照>)
抛出 -> 数据存储保存更改结果<默认快照>
{
var remappedIdentifiers = [持久标识符:
持久标识符]()
对于请求插入的快照 {
// 插入新项目
}
对于请求更新的快照 {
// 更新现有项目
}
对于请求删除的快照 {
// 删除项目
}
返回
数据存储保存更改结果<默认快照>(for:
self.identifier,
remappedIdentifiers: remappedIdentifiers)
}
<st c="10142">These two functions perform all the magic underneath.</st> <st c="10197">In this code example, I left the function implementation empty – it is up to you to fill it in according to the specific data store implementation.</st> <st c="10345">Once we modify our CSV file, we can return the results to</st> <st c="10403">the app.</st>
<st c="10411">The</st> `<st c="10416">History</st>` <st c="10423">API, the</st> `<st c="10433">DataStore</st>` <st c="10442">protocol, and the ability</st> <st c="10468">to provide uniqueness</st> <st c="10490">to entities make Swift Data much more mature and capable.</st> <st c="10549">To get started with Swift Data, read</st> *<st c="10586">Chapter 2</st>*<st c="10595">.</st>
<st c="10596">Next, let’s talk about an exciting improvement in</st> <st c="10647">SwiftUI transition.</st>
<st c="10666">Introducing zoom transition</st>
<st c="10694">This is a small improvement, but it may</st> <st c="10734">indicate an interesting direction Apple is taking with SwiftUI.</st> <st c="10799">In general, UIKit’s transitioning capabilities are very robust and provide us with the flexibility to create any transition we want.</st> <st c="10932">Even before that, from the beginning, UIKit had some nice built-in transitions we could use to make our navigation</st> <st c="11047">more appealing.</st>
<st c="11062">In iOS 18, Apple added a new transition that allows us to navigate to a new view using a</st> <st c="11152">zoom animation.</st>
<st c="11167">Let’s create an album grid that, when tapping on the album, transitions to a full album screen with a</st> <st c="11270">zoom animation:</st>
NavigationStack {
ScrollView {
LazyVGrid(columns: [
网格项(.自适应(minimum: 150)) ]) {
对于专辑专辑 {专辑 in
NavigationLink {
Image(album.imageName)
.可调整大小() <st c="11512">.导航过渡(.缩放(sourceID:专辑 ID,在:</st>
Image(album.imageName)
.可调整大小()
.scaledToFit()
.frame(minWidth: 0,
最大宽度: .infinity)
.frame(height: 150)
.cornerRadius(8.0)
} <st c="11720">.matchedTransitionSource(id:</st>
}
}
}
.padding()
}
<st c="11794">This example shows a simple grid view</st> <st c="11832">of albums, a NavigationStack, and a NavigationLink.</st> <st c="11885">The idea of performing the zoom transition is to match the source (the image we tapped on) to the destination (the image we</st> <st c="12009">zoomed into).</st>
<st c="12022">We do that by adding two</st> <st c="12048">view modifiers:</st>
* `<st c="12063">navigationTransition</st>`<st c="12084">: We add this modifier to the source view.</st> <st c="12128">The source view, in our case, is the album view in the grid.</st> <st c="12189">We select the type of animation (currently, it’s a zoom animation) and the</st> <st c="12264">source ID.</st>
* `<st c="12274">matchedTransitionSource</st>`<st c="12298">: We add this modifier to the destination view.</st> <st c="12347">In our example, the destination view is the full-screen view of the album.</st> <st c="12422">Again, we provide the ID of the album we want to present so SwiftUI can perform the zoom animation between</st> <st c="12529">these views.</st>
<st c="12541">Creating the match between the views allows SwiftUI to perform a nice zoom animation, similar to what we see in the Photos app.</st> <st c="12670">Look at</st> *<st c="12678">Figure 1</st>**<st c="12686">.1</st>*<st c="12688">:</st>

<st c="12691">Figure 1.1: Zoom transition between photos grid and a full-screen view</st>
*<st c="12761">Figure 1</st>**<st c="12770">.1</st>* <st c="12772">shows how the zoom animation looks in a couple of frames based on the p</st><st c="12844">receding</st> <st c="12854">code example.</st>
<st c="12867">Zoom transitions serve</st> <st c="12890">more than aesthetic purposes.</st> <st c="12921">They inform the user about the changes occurring on the screen, helping them</st> <st c="12998">stay oriented.</st>
<st c="13012">To read more about navigation in iOS, read</st> *<st c="13056">Chapter 4</st>*<st c="13065">.</st>
<st c="13066">Speaking of navigation, iPadOS navigation gained a unique and valuable capability – the</st> <st c="13155">floating bar.</st>
<st c="13168">Adding a floating tab bar</st>
<st c="13194">iPad is not the focus</st> <st c="13216">of this book.</st> <st c="13231">This is not because iPadOS is unimportant but because most, if not all, of the topics we discuss here are also suitable</st> <st c="13351">for iPadOS.</st>
<st c="13362">However, there are special features that are relevant to iPadOS that are worth mentioning.</st> <st c="13454">One of them is the float</st> <st c="13479">tab bar.</st>
<st c="13487">The tab bar has existed in iOS since its very beginning.</st> <st c="13545">It allows users to navigate between different sections of an app.</st> <st c="13611">In both iOS and iPadOS, the tab is located at the bottom of the screen.</st> <st c="13683">While it looks perfectly fine on small devices, a tab bar on big screens seems stretched and doesn’t use the</st> <st c="13792">large space.</st>
<st c="13804">One solution for handling navigation in a iPadOS is to implement a sidebar – a view on the side that displays the different sections of</st> <st c="13941">the app.</st>
<st c="13949">In iPadOS 18, the position</st> <st c="13976">of the sidebar changed, and it is now located at the top of the screen, floating over the app content.</st> <st c="14080">Not only that; the user can also transition between a tab bar and a sidebar.</st> <st c="14157">Let’s see how to do that</st> <st c="14182">in code:</st>
结构体 ContentView: View {
var body: some View {
TabView {
Tab("主页",系统图像:"house.fill") { }
Tab("个人资料",系统图像:
"person.crop.circle") { }
Tab("设置",系统图像:"gear") { }
}
.tint(.red) <st c="14402">.tabViewStyle(.sidebarAdaptable)</st> }
}
<st c="14438">This code example looks straightforward but includes a view modifier called</st> `<st c="14515">tabViewStyle</st>`<st c="14527">. Currently, it has only one option to choose from –</st> `<st c="14580">sidebarAdaptable</st>`<st c="14596">. When we add this view modifier, a button is added to the tab bar that allows the user to change the layout.</st> <st c="14706">Let’s see how it looks (</st>*<st c="14730">Figure 1</st>**<st c="14739">.2</st>*<st c="14741">):</st>

<st c="14831">Figure 1.2: The Tab bar adapts a sidebar layout</st>
*<st c="14878">Figure 1</st>**<st c="14887">.2</st>* <st c="14889">shows the two layouts</st> <st c="14911">for our tab bar.</st> <st c="14929">The new sidebar improves the user experience and makes navigating and focusing on content easier.</st> <st c="15027">It also resembles Apple’s apps, such as the TV app, which aligns with what users can expect from</st> <st c="15124">our app.</st>
<st c="15132">Another important aspect of SwiftUI that required improvement is scroll views.</st> <st c="15212">Let’s go over major changes in</st> <st c="15243">that area.</st>
<st c="15253">Having more control over scroll views</st>
<st c="15291">Controlling and observing scroll view behavior</st> <st c="15338">was part of the reason why UIKit developers ha</st><st c="15385">dn’t moved to</st> <st c="15400">SwiftUI yet.</st>
<st c="15412">Scroll views are crucial in mobile apps, not just because of the small screen, which often requires the user to scroll for more content, but also because they help reuse visible content to minimize memory usage or adjust our UI based on</st> <st c="15650">scroll position.</st>
<st c="15666">However, why is handling scroll views in SwiftUI</st> <st c="15716">more complex than in UIKit?</st> <st c="15744">We can think of</st> <st c="15760">two reasons:</st>
1. **<st c="15772">SwiftUI is relatively new</st>**<st c="15798">: SwiftUI is still considered to be a new framework.</st> <st c="15852">Think how much time it took for UIKit to become a mature framework.</st> <st c="15920">Obviously, we can achieve this in several years and 17 years</st> <st c="15981">of development.</st>
2. `<st c="16336">@State</st>` <st c="16342">variable or a</st> <st c="16357">view modifier.</st>
<st c="16371">These reasons lead to many workarounds that developers use to achieve the desired user experience.</st> <st c="16471">Fortunately, iOS 18 gives us two view modifiers that make SwiftUI scroll views more appealing than ever.</st> <st c="16576">We’ll start</st> <st c="16588">with</st> `<st c="16593">onScrollGeometryChange</st>`<st c="16615">.</st>
<st c="16616">Observing the scroll view position</st>
<st c="16651">Up until now, SwiftUI hasn’t provided</st> <st c="16689">any direct API to observe the scroll view position.</st> <st c="16742">Many developers had to find a workaround or use UIKit as a solution.</st> <st c="16811">Now, we have an</st> `<st c="16827">onScrollGeometryChange</st>` <st c="16849">view modifier that allows us to observe any change in the</st> <st c="16908">scroll position.</st>
<st c="16924">Let’s say we have a</st> `<st c="16945">VStack</st>` <st c="16951">view within a scroll view, and we wish to show a</st> **<st c="17001">Scroll to the top</st>** <st c="17018">button whenever the user scrolls down to allow them to return to the top of</st> <st c="17095">the list.</st>
<st c="17104">Let’s look at the</st> <st c="17123">following code:</st>
ScrollViewReader {代理 in
ScrollView {
VStack(alignment: .leading, spacing: 16) {
ForEach(albums) {专辑 in
提取视图(album:专辑)
.id(album.id)
}
}
}
.overlay(alignment: .bottom) {
如果显示滚动到顶部 {
Button("滚动到顶部") {
代理滚动到(albums[0].id,
锚点: .top)
}
.buttonStyle(.borderedProminent)
}
} <st c="17458">.onScrollGeometryChange(for: Bool.self) {</st>
<st c="17658">In this code example, we can see a</st> `<st c="17693">VStack</st>` <st c="17699">view inside a scroll view.</st> <st c="17727">The</st> `<st c="17731">VStack</st>` <st c="17737">view contains a list of albums.</st> <st c="17770">Notice that we have an</st> `<st c="17793">onScrollGeometryChange</st>` <st c="17815">view modifier for the scroll view itself.</st> <st c="17858">The view modifier has a closure that runs each time the scroll position changes with a</st> `<st c="17945">geometry</st>` <st c="17953">parameter.</st> <st c="17965">Within the closure, we inspect the scroll view content offset, and if it reaches a specific threshold, we show/hide the</st> **<st c="18085">Scroll to top</st>** <st c="18098">button using a specific</st> <st c="18123">state variable.</st>
<st c="18138">The</st> `<st c="18143">ScrollViewReader</st>` <st c="18159">view, which wraps</st> <st c="18177">the scroll view, provides a proxy to the scroll view so we can scroll to the top when the user presses</st> <st c="18281">the button.</st>
<st c="18292">We can use the</st> `<st c="18308">onScrollGeometryChange</st>` <st c="18330">method for more use cases than just toggling a button.</st> <st c="18386">For example, we can use it to perform a network request in an infinity list where we need to load more content from the server when the user reaches the bottom.</st> <st c="18547">Additional examples would be having a sticky header or a progress indicator, or even just sending analytics.</st> <st c="18656">These use cases were complex to implement before iOS 18 and are now</st> <st c="18724">extremely simple.</st>
<st c="18741">The improvement in the second scroll view seems to belong to the same family.</st> <st c="18820">Let’s review</st> <st c="18833">it now.</st>
<st c="18840">Observing items’ visibility</st>
<st c="18868">Checking whether a view is visible</st> <st c="18903">inside a scroll view is not easy.</st> <st c="18938">Up until now, we had to calculate the view frame versus the scroll view content offset, not to mention observe that during a scroll view.</st> <st c="19076">Lucky for us, we now have a new modifier</st> <st c="19117">called</st> `<st c="19124">onScrollVisibilityChange</st>`<st c="19148">.</st>
<st c="19149">Suppose we want to change a view while it enters our scroll view.</st> <st c="19216">For example, we might want to report analytics or print to</st> <st c="19275">the console.</st>
<st c="19287">Let’s look at the</st> <st c="19306">following example:</st>
对于专辑 {专辑 in
ExtractedView(album: album)
.id(album.id)
.<st c="19395">onScrollVisibilityChange(threshold: 0.9) {</st>
<st c="19500">This code example shows the same album row we created in the previous example (in the</st> *<st c="19586">Observing the scroll view position</st>* <st c="19620">section).</st> <st c="19631">This time, we added a new view modifier to the view itself –</st> `<st c="19692">onScrollVisibilityChange</st>`<st c="19716">. This view modifier has two parameters –</st> `<st c="19758">threshold</st>` <st c="19767">and</st> `<st c="19772">closure</st>` <st c="19779">with a</st> `<st c="19787">Bool</st>` <st c="19791">parameter (named</st> `<st c="19809">visible</st>` <st c="19816">in our case).</st> <st c="19831">Let’s review</st> <st c="19844">them now:</st>
* `<st c="19853">threshold</st>`<st c="19863">: The</st> `<st c="19870">threshold</st>` <st c="19879">parameter defines how much the change must occur for the closure to run.</st> <st c="19953">For example, a threshold of 0.2 means that we need 20% of the view to be visible or hidden before it runs the closure and reports</st> <st c="20083">the change.</st>
* `<st c="20094">closure</st>`<st c="20102">: The closure with the</st> `<st c="20126">Bool</st>` <st c="20130">parameter runs each time the view reaches the threshold.</st> <st c="20188">The</st> `<st c="20192">Bool</st>` <st c="20196">parameter contains the change –</st> `<st c="20229">true</st>` <st c="20233">for visible and</st> `<st c="20250">false</st>` <st c="20255">for hidden.</st>
<st c="20267">In our code example, we set the threshold to</st> `<st c="20313">0.9</st>`<st c="20316">. This means that we need to view it to reveal 90% of its size before the closure runs.</st> <st c="20404">Inside the closure, we check whether the view is visible before we report it to</st> <st c="20484">the console.</st>
<st c="20496">We can use this view modifier for many purposes.</st> <st c="20546">For example, we can perform a specific animation when the view enters, load additional information, or adjust the screen interface if needed.</st> <st c="20688">Something that was complex to do before is now simple to accomplish</st> <st c="20755">using one</st> <st c="20766">view modifier.</st>
<st c="20780">Scroll view is not the only topic we have more control of.</st> <st c="20840">Let’s talk</st> <st c="20851">about texts.</st>
<st c="20863">Changing the text rendering behavior</st>
<st c="20900">Handling texts on screen</st> <st c="20925">was also a very mature area where UIKit provided great frameworks such as TextKit.</st> <st c="21009">We could manipulate texts and create almost any effect that</st> <st c="21069">we wanted.</st>
<st c="21079">In iOS 18, Apple introduced TextRenderer, a protocol that can help us change the default behavior of our texts</st> <st c="21191">in SwiftUI.</st>
<st c="21202">Let’s say that we want a title with a different opacity for each line and even rotate the lines a bit.</st> <st c="21306">This creates a nice effect for the titles in our app.</st> <st c="21360">So, let’s see how to do that</st> <st c="21389">in SwiftUI:</st>
var body: some View {
Text("SwiftUI 中的文本新增了众多新特性")
.font(.system(size: 60)) <st c="21784">.textRenderer(CustomTextRenderer())</st> }
}
<st c="21823">This code example has a new structure called</st> `<st c="21869">CustomTextRender</st>`<st c="21885">, which conforms to the</st> `<st c="21909">TextRenderer</st>` <st c="21921">protocol.</st> <st c="21932">We have one important function to implement – the</st> `<st c="21982">draw()</st>` <st c="21988">function.</st> <st c="21999">In this function, we receive an important parameter –</st> `<st c="22053">ctx</st>` <st c="22056">– the graphic context.</st> <st c="22080">The</st> `<st c="22084">TextRenderer</st>` <st c="22096">protocol also provides us access to the different lines and slices we have in our text.</st> <st c="22185">In our example, we can iterate the different lines using the</st> `<st c="22246">layout</st>` <st c="22252">parameter, change their opacity, and even</st> <st c="22295">rotate them.</st>
<st c="22307">Once we have the</st> `<st c="22325">CustomTextRender</st>` <st c="22341">structure, we can</st> <st c="22359">add it to our</st> `<st c="22374">Text</st>` <st c="22378">component using the</st> `<st c="22399">textRenderer</st>` <st c="22411">view modifier.</st>
<st c="22426">Let’s see how it looks (</st>*<st c="22451">Figure 1</st>**<st c="22460">.3</st>*<st c="22462">):</st>

<st c="22467">Figure 1.3: The Text component with custom text rendering</st>
*<st c="22524">Figure 1</st>**<st c="22533">.3</st>* <st c="22535">shows our text with a different opacity and rotation for each line.</st> <st c="22604">Adding effects to text can give a dynamic visualization</st> <st c="22659">for titles and paragraphs and add more life to</st> <st c="22707">our apps.</st>
<st c="22716">Next, let’s see how SwiftUI has become more mature and capable than ever with positioning sub -iews from</st> <st c="22822">other views.</st>
<st c="22834">Positioning sub-views from another view</st>
<st c="22874">What does it mean to position</st> <st c="22904">sub-views from another view?</st> <st c="22934">While this description sounds weird and unclear, it is a nice addition to SwiftUI that can help us provide more dynamic and</st> <st c="23058">reusable content.</st>
<st c="23075">To understand what it means, let’s take the following code as</st> <st c="23138">an example:</st>
struct NewsView: View {
var body: some View {
Text("可再生能源领域取得重大突破:新型太阳能电池技术承诺提高 30%的效率")
Text("全球市场对突然加息作出反应:股市全面下跌")
Text("历史性的和平协议达成:领导人签署协议结束长达数十年的冲突")
Text("创新人工智能工具革新医疗保健:医生拥抱机器学习进行诊断")
Text("自然灾害袭击:沿海城市发生大规模地震,救援行动正在进行")
}
}
<st c="23682">This code example shows a view called</st> `<st c="23721">NewsView</st>` <st c="23729">with a list of</st> `<st c="23745">Text</st>` <st c="23750">components, each containing a news headline.</st> <st c="23795">If we look closely, we can see that there’s no layout – no VStack, group, or List.</st> <st c="23878">We are not used to this in SwiftUI, and that’s okay because that view is</st> <st c="23951">for display.</st>
<st c="23963">The</st> `<st c="23968">NewsView</st>` <st c="23976">job is to be a container</st> <st c="24001">for components.</st> <st c="24018">Let’s see how we can use</st> <st c="24043">this container:</st>
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("最新头条")
.font(.title)
{
firstHeadline
.font(.title2)
Spacer()
}
ForEach(collection.dropFirst()) {
newsItem in
newsItem
.font(.headline)
Spacer()
}
}
}
.padding()
}
}
}
<st c="24399">In this example, we added a SwiftUI group, but this time, from the</st> `<st c="24467">NewsView</st>` <st c="24475">view:</st>
Group(subviews: NewsView()) { collection in
<st c="24525">This line creates a group that iterates over the specific view’s sub-views and allows us to position and modify</st> <st c="24638">them ourselves.</st>
<st c="24653">In our example, we change the font of the first sub-view and present all the views with a spacer</st> <st c="24751">between them.</st>
<st c="24764">The ability to reposition views</st> <st c="24796">within other views unlocks new possibilities.</st> <st c="24843">For instance, we can reuse the same views but with different layouts, sequences, or styles.</st> <st c="24935">Treating our views as containers for smaller components makes our code</st> <st c="25006">more reusable.</st>
<st c="25020">Now, let’s move to our chapter’s last section – the</st> <st c="25073">AI revolution.</st>
<st c="25087">Entering the AI revolution</st>
<st c="25114">AI and machine learning</st> <st c="25139">are not new areas for Apple and the iOS platform.</st> <st c="25189">Apple uses AI to adjust photos taken, suggest apps to users according to their usage, optimize battery charging, and</st> <st c="25306">many more.</st>
<st c="25316">For developers, Apple provides the CoreML framework and tools such as Create ML to help users train and create their own machine</st> <st c="25446">learning models.</st>
<st c="25462">However, the rising popularity of services such as ChatGPT and Gemini proved that CoreML is insufficient, and that Apple needs to integrate AI deeper into</st> <st c="25618">the system.</st>
<st c="25629">So, what did Apple prepare for us, the developers, regarding AI in</st> <st c="25697">iOS 18?</st>
<st c="25704">Apple integrated AI into iOS 18 by letting iOS understand what’s happening in the system and helping the user perform common tasks using natural language understanding, similar</st> <st c="25882">to ChatGPT.</st>
<st c="25893">For example, let’s say we’re working on a word-processing app and created an App Intent that allows the user to add an image to</st> <st c="26022">a document.</st>
<st c="26033">Until iOS 18, we would have had to define a specific phrase for the user to use with Siri.</st> <st c="26125">However, in iOS 18, the user can say something such as “Add this image to the page I’m working on,” and Siri uses a set of machine-learning models to convert this phrase to our app intent model.</st> <st c="26320">Not only that, but Siri can also understand the current context on screen and even search our app by indexing our app content in</st> <st c="26449">the spotlight.</st>
<st c="26463">Integrating our app into Siri requires</st> <st c="26502">little effort.</st> <st c="26518">We mainly need to focus on structuring our main actions and entities.</st> <st c="26588">Apple Intelligence does all</st> <st c="26616">the rest.</st>
<st c="26625">To read more about using App Intents with Siri, go to</st> *<st c="26680">Chapter 13</st>*<st c="26690">.</st>
<st c="26691">Summary</st>
<st c="26699">There’s no other way of looking at iOS 18 than as an exciting one.</st> <st c="26767">The addition of Apple Intelligence is only part of the story – Apple took care of many system and SDK aspects such as testing, persistent data, UI,</st> <st c="26915">and more.</st>
<st c="26924">In this chapter, we explored the basics of the new Swift Testing framework, learned about Swift Data improvements, and discussed enhancements in SwiftUI such as zoom transition, floating tab bar, scroll views, and text rendering.</st> <st c="27155">We even scratched the surface of Apple Intelligence and tried to understand how it is integrated with App Intents.</st> <st c="27270">By now, you should be familiar with the most exciting and new topics in</st> <st c="27342">iOS 18.</st>
<st c="27349">A few code examples are just not enough.</st> <st c="27391">We are developers, and we need more!</st> <st c="27428">So, let’s jump straight into SwiftData and explore Apple’s new persistent data framework in the</st> <st c="27524">next chapter.</st>
第三章:2</st c="0">
使用 SwiftData 简化我们的实体</st c="2">
-
了解 SwiftData 的</st c="447"> 背景</st c="463"></st c="463"> -
定义数据模型,包括其</st c="483"> 关系</st c="533">和属性</st c="533"></st c="533"> -
了解 SwiftData 容器</st c="547"> 和配置</st c="584"></st c="584"> -
使用模型上下文</st c="602"> 检索和操作数据</st c="639"></st c="639"> -
将我们的数据迁移到新的</st c="652"> 版本模式</st c="677"></st c="677">
技术要求</st c="842">
[
理解 SwiftData 的背景</st c="1101">
定义 SwiftData 模型
<st c="4058">import SwiftData</st>
<st c="4075">@Model</st> class Book {
var author: String
var title: String
var publishedDate: Date
init(author: String, title: String, publishedDate:
Date) {
self.author = author
self.title = title
self.publishedDate = publishedDate
}
}
<st c="4327">Book</st> <st c="4374">@Model</st><st c="4403">@Model</st>
<st c="4503">@Model</st> <st c="4705">实体属性。</st>
<st c="4984">@Model</st>
展开 @Model 宏
<st c="5228">@Model</st>
@Transient
private var _$backingData: any SwiftData.BackingData<Book>
= Book.createBackingData()
public var persistentBackingData: any
SwiftData.BackingData<Book> {
get {
_$backingData
}
set {
_$backingData = newValue
}
}
static var schemaMetadata:
[SwiftData.Schema.PropertyMetadata] {
return [
SwiftData.Schema.PropertyMetadata(name: "author",
keypath: \Book.author, defaultValue: nil, metadata:
nil),
SwiftData.Schema.PropertyMetadata(name: "title",
keypath: \Book.title, defaultValue: nil, metadata:
nil),
SwiftData.Schema.PropertyMetadata(name:
"publishedDate", keypath: \Book.publishedDate,
defaultValue: nil, metadata: nil)
]
}
required init(backingData: any SwiftData.BackingData<Book>) {
_author = _SwiftDataNoType()
_title = _SwiftDataNoType()
_publishedDate = _SwiftDataNoType()
self.persistentBackingData = backingData
}
@Transient
private let _$observationRegistrar = Observation.ObservationRegistrar()
struct _SwiftDataNoType {
}
extension Book: SwiftData.PersistentModel {
}
extension Book: Observation.Observable {
}
<st c="6402">Book</st> <st c="6434">@Model</st>
-
<st c="6523">Book</st><st c="6598">PersistentModel</st><st c="6613">和</st>Observable 。 PersistentModel 协议帮助 SwiftData 与我们的风格协同工作并访问其属性。 <st c="6722">Observable</st><st c="6736">协议允许我们通知数据的</st>更改。 ` -
<st c="6914">PersistentModel</st>协议是,我们将发现它需要实现两个变量—— <st c="7014">backingData</st>和 <st c="7031">schemaMetaData</st>。我们可以在我们的宏展开代码中直接看到它们的实现。 这些变量帮助 SwiftData 专门为我们存储和检索我们的实体信息。 并且也许这正是 Swift 宏真正强大的地方——能够生成针对 我们的类定制的代码。 -
我们有属性宏 :如果我们查看类属性,我们可以看到它们现在有自己的宏。 展开它们会显示它们现在已成为计算变量,因此我们可以从我们的内存以及我们的后端 数据存储中存储和检索数据: <st c="7635">@_PersistedProperty</st> var author: String <st c="7675">@_PersistedProperty</st> var title: String <st c="7713">@_PersistedProperty</st> var publishedDate: Date
<st c="8048">@Model</st>
添加关系
@Model
class Book {
var title: String
var publishedDate: Date <st c="9127">var author: Author</st><st c="9145">var pages: [Page]</st> init(author: Author, title: String, publishedDate:
Date) {
self.title = title
self.publishedDate = publishedDate <st c="9277">self.author = author</st><st c="9297">self.pages = []</st> }
}
<st c="9379">Book</st>
-
<st c="9390">Author</st>:这是一个 *一对多 关系到 <st c="9437">Author</st>实体,因为在我们这个例子中,每本书只有一个 作者 -
<st c="9502">页数</st>:在 <st c="9526">Page</st>实体的情况下,我们 有一个 *多对多 关系,因为一本书可以包含 多个页面
<st c="9663">Page</st> <st c="9672">Author</st> <st c="9697">@Model</st>
<st c="9786">@Model</st> class Author {
var name: String
init() {
self.name = ""
}
} <st c="9854">@Model</st> class Page {
var content: String
var order: Int
init(content: String, order: Int) {
self.content = content
self.order = order
}
}
<st c="10238">@</st>``<st c="10239">关系</st>
<st c="11028">@Relationship</st>
SwiftData 关系删除规则
<st c="11169">页面</st> <st c="11179">作者</st>
<st c="11653">cascade</st>.
<st c="11661">We have four different</st> <st c="11685">deletion rules:</st>
* `<st c="11700">cascade</st>`<st c="11708">: Deletes any</st> <st c="11723">related objects</st>
* `<st c="11738">deny</st>`<st c="11743">: Prevents deletion of an object if it contains one or more references to</st> <st c="11818">other objects</st>
* `<st c="11831">nullify</st>`<st c="11839">: Nullifies the related object’s reference to the</st> <st c="11890">deleted object</st>
* `<st c="11904">noAction</st>`<st c="11913">: In this case, nothing will happen to the</st> <st c="11957">other object</st>
<st c="11969">We should remember that a deletio</st><st c="12003">n rule is not arbitrary; it should be based on our app</st> <st c="12059">business ideas.</st>
<st c="12074">For example, the reason why a book has a</st> *<st c="12116">to-one</st>* <st c="12122">connection to an author sounds logical, but there are books with co-authors as well.</st> <st c="12208">So, this is something that should be aligned with our</st> <st c="12262">product manager.</st>
<st c="12278">Most of us</st> <st c="12290">are more familiar with the term</st> *<st c="12322">one-to-many</st>* <st c="12333">than</st> *<st c="12339">to-many</st>*<st c="12346">. This is because relationships between objects go both ways – the fact that each book has one author doesn’t mean that each author has only one book.</st> <st c="12497">So, as part of th</st><st c="12514">e relationship definition, we also need to define its</st> <st c="12569">inverse relationship.</st>
<st c="12590">Defining the inverse relationship</st>
<st c="12624">Why do we need to define the inverse relationship?</st> <st c="12676">We need to realize that relationships</st> <st c="12714">always have two sides (like in real life!), and we need to maintain them to have a proper</st> <st c="12804">data schema.</st>
<st c="12816">When establishing a relationship between a book and its pages, it’s better to define the inverse relationship as well.</st> <st c="12936">This way, we can create a proper reference back to</st> <st c="12987">the book.</st>
<st c="12996">Let’s see how to create an inverse relationship between a book and its pages through the</st> <st c="13086">following code:</st>
@模型
class Book {
…
}
@模型
class Page {
var content: String <st c="13225">var book: Book?</st> init(content: String) {
self.content = content
}
}
<st c="13291">Looking at the code, we can see that we define the relationship as</st> <st c="13359">a keypath:</st>
\Page.book
<st c="13380">A keypath can</st> <st c="13395">help us avoid typos and mistakes when defining the</st> <st c="13446">inverse property.</st>
<st c="13463">Moreover, if we add a new page to the</st> `<st c="13502">pages</st>` <st c="13507">property, SwiftData will automatically set the Page’s</st> `<st c="13562">book</st>` <st c="13566">property to the</st> <st c="13583">new book:</st>
let newPage = Page(content: "Swift 数据")
newPage.book = book
// book.pages 属性
<st c="13696">SwiftData knows how to do that using our</st> <st c="13738">inverse declaration.</st>
<st c="13758">The inverse relationship may sound like an obvious feature – if we have a book with several pages, and each page is related to a book, isn’t it obvious that the</st> `<st c="13920">book</st>` <st c="13924">property in the</st> `<st c="13941">page</st>` <st c="13945">class is the inverse relationship?</st> <st c="13981">However, in reality, it’s not obvious.</st> <st c="14020">There are several real-world use cases when relationships can be much</st> <st c="14090">more complex.</st>
<st c="14103">Let’s take, for example, the data structure of a folder tree – each folder has its sub-folders.</st> <st c="14200">This means that a folder has a</st> *<st c="14231">to-one</st>* <st c="14237">relationship to its parent and a</st> *<st c="14271">to-many</st>* <st c="14278">relationship to its children.</st> <st c="14309">Let’s see that in</st> <st c="14327">the code:</st>
@模型
class Folder {
var parent: Folder? @关系(inverse: \Folder.parent) var subFolders:
[文件夹]
var name: String
var id: UUID
init(parent: Folder? = nil, subFolders: [Folder], name:
String, id: UUID) {
self.parent = parent
self.subFolders = subFolders
self.name = name
self.id = id
}
}
<st c="14632">This example</st> <st c="14646">demonstrates what a</st> `<st c="14666">Folder</st>` <st c="14672">class looks like when trying to create a multi-level hierarchical structure.</st> <st c="14750">In this case, we must define the inverse relationship to</st> <st c="14807">avoid cycles.</st>
<st c="14820">Now that we know how relationships work in SwiftData, let’s see more ways to customize our model, using the</st> `<st c="14929">@</st>``<st c="14930">Attribute</st>` <st c="14939">macro.</st>
<st c="14946">Adding the @Attribute macro</st>
<st c="14974">So far, we have learned how to declare new entities, properties, and even relationships between</st> <st c="15071">our entities.</st> <st c="15085">It looks like we can do anything with our data entities!</st> <st c="15142">Now, it’s essential to drill down to the</st> <st c="15183">property level.</st>
<st c="15198">Along with our</st> `<st c="15214">@Model</st>` <st c="15220">and</st> `<st c="15225">@Relationship</st>` <st c="15238">macros, we now have the</st> `<st c="15263">@Attribute</st>` <st c="15273">macro to define the behavior of a</st> <st c="15308">specific property.</st>
<st c="15326">If you remember from Core Data, each attribute has an inspector window where we can configure an attribute’s behavior (</st>*<st c="15446">Figure 2</st>**<st c="15455">.1</st>*<st c="15457">):</st>

<st c="15805">Figure 2.1: The Attribute inspector in Core Data</st>
*<st c="15853">Figure 2</st>**<st c="15862">.1</st>* <st c="15864">shows what it looks like when we select one of the attributes (</st>`<st c="15928">firstName</st>` <st c="15938">in this example) and how we can customize</st> <st c="15981">its behavior.</st>
<st c="15994">We can</st> <st c="16002">define some of these settings in SwiftData as part of the property declaration.</st> <st c="16082">For example, the Optional feature, as seen in</st> *<st c="16128">Figure 2</st>**<st c="16136">.1</st>*<st c="16138">, is defined by marking a property as Swift optional type, and the default value is part of</st> <st c="16230">variable initialization:</st>
var firstName: String? = "MyName"
<st c="16288">However, other settings need to be declared as part of the</st> `<st c="16348">@</st>``<st c="16349">Attribute</st>` <st c="16358">macro.</st>
<st c="16365">Let’s start with the most common one,</st> `<st c="16404">unique</st>`<st c="16410">, and making attributes unique is an important feature of many databases,</st> <st c="16484">including</st> *<st c="16494">SQLite</st>*<st c="16500">.</st>
<st c="16501">The following are a few</st> <st c="16526">reasons why:</st>
* **<st c="16538">Setting up a primary key</st>**<st c="16563">: A primary key represents a record’s unique identifier.</st> <st c="16621">We use a primary key to ensure that there are no duplicates in</st> <st c="16684">our table.</st>
* **<st c="16694">Supporting indexing</st>**<st c="16714">: Unique attributes can help us index our database for searching</st> <st c="16780">and retrieval.</st>
* **<st c="16794">Helping with data validation</st>**<st c="16823">: Utilizing unique attributes goes beyond primary keys and extends to other distinctive attributes, enhancing our ability to validate data</st> <st c="16963">during insertion.</st>
<st c="16980">Even though</st> *<st c="16993">SQLite</st>* <st c="16999">supports unique attributes, Core Data doesn’t have a built-in way to support</st> <st c="17077">unique identifiers, derived from its design philosophy to offer complete flexibility</st> <st c="17162">to developers.</st>
<st c="17176">Conversely, SwiftData supports unique attributes out of</st> <st c="17233">the box:</st>
`<st c="17389">UUID</st>` <st c="17394">是属性唯一值的经典示例,但我们可以将其应用于任何其他类型的属性,例如用户 ID</st> <st c="17520">和名称。</st>
<st c="17530">但是,将属性设置为</st> *<st c="17579">唯一</st>*<st c="17585">究竟意味着什么?当我们尝试插入一个已经存在</st> <st c="17663">唯一属性</st>的实例时,会发生什么?
<st c="17680">在唯一属性的情况下,SwiftData 执行</st> `<st c="17769">INSERT</st>` <st c="17775">或</st> `<st c="17779">UPDATE</st>` <st c="17785">操作。</st> <st c="17797">这意味着如果已存在具有唯一值的实例,SwiftData 将不会在其存储中创建新对象,而是更新</st> <st c="17940">现有实例。</st>
使用 `<st c="17958">@Attribute</st>` <st c="18000">宏将属性声明为唯一属性是直接的。</st> <st c="18037">然而,有时我们需要更复杂的功能。</st> <st c="18094">例如,假设我们有一个</st> `<st c="18127">Book</st>` <st c="18131">模型,具有</st> `<st c="18143">name</st>` <st c="18147">和</st> `<st c="18152">publicationName</st>` <st c="18167">属性。</st> <st c="18180">在我们的情况下,我们可以有两个同名或同</st> `<st c="18246">publicationName</st>`<st c="18261">的书籍,但我们不能有两个属性完全相同的书籍。</st> <st c="18327">`<st c="18346">publicationName</st>` <st c="18361">和</st> `<st c="18366">name</st>` <st c="18370">的组合构成了书籍的唯一标识。</st>
<st c="18406">一个解决方案是维护一个属性,尝试从这两个属性中构建一个唯一的 ID。</st> <st c="18505">另一个优雅的选项是使用</st> `<st c="18542">#Unique</st>` <st c="18549">宏来定义更复杂的</st> `<st c="18579">唯一性要求:</st>
@Model
class Book { <st c="18624">#Unique<Book>([\.name, \.publicationName])</st> var publicationName: String = "Packt"
var name: String
}
<st c="18723">在这个代码示例中,我们通过组合两个键路径来强制执行</st> `<st c="18779">Book</st>` <st c="18783">模型的唯一性。</st> <st c="18818">就像属性参数一样,</st> `<st c="18853">.unique</st>`<st c="18860">,如果我们尝试插入一个新书籍实例,而我们已经有了一个同名和出版名称相同的实例,SwiftData 将执行一个</st> `<st c="18994">upsert</st>` <st c="19000">操作并更新</st> `<st c="19022">该实例。</st>
<st c="19036">尽管 SwiftData 处理唯一属性很好,但确保我们根据应用程序的要求仔细选择唯一属性和键路径是很重要的。</st> <st c="19211">过多的唯一属性可能导致复杂性和</st> <st c="19263">性能问题。</st>
<st c="19282">唯一属性非常适合简化处理重复实例的任务。</st> <st c="19369">另一个可以简化我们生活的属性特性是</st> *<st c="19429">瞬态</st>*<st c="19438">。</st>
<st c="19439">使用瞬态属性进行非持久化</st>
<st c="19475">与 SwiftData 一起工作的好处是,所有属性都自动成为实体的</st> <st c="19577">属性,并且被持久保存到本地数据存储中。</st> <st c="19640">然而,有时,我们可能想要一个仅存在于内存中而不被持久保存的属性。</st> <st c="19717">内存中</st> *<st c="19728">并且不持久保存</st> *<st c="19757">的属性是一个很好的例子。</st> <st c="19842">实现这一目标的一种方法是创建一个函数或计算变量,然后根据相关属性返回一个值。</st> <st c="19973">然而,在其他情况下,计算变量或函数可能不是方便的解决方案。</st> <st c="20074">假设我们想要一个临时的计数器或维护一个仅与应用程序当前</st> <st c="20176">生命周期相关的标志。</st>
<st c="20187">对于这类情况,我们</st> <st c="20217">有一个</st> *<st c="20224">瞬态</st> *<st c="20233">属性。</st> <st c="20245">瞬态属性不是一个新概念——Core Data 从早期版本就支持瞬态属性。</st> <st c="20345">由于 SwiftData 基于 Core Data 的基本原理,它默认支持瞬态属性。</st>
<st c="20445">以下是我们在 SwiftData 中声明</st> *<st c="20471">瞬态</st> *<st c="20480">属性</st> <st c="20490">的方法:</st>
@Transient
var openCounter: Int = 0
<st c="20539">在这个代码片段中,</st> `<st c="20566">openCounter</st>` <st c="20577">变量不会被保存到本地持久</st> <st c="20631">存储中,并且每次我们从</st> <st c="20696">我们的数据库中检索实体时都会重新初始化。</st>
<st c="20709">瞬态属性可能听起来是一个小功能,但在许多情况下,它确实能带来差异,瞬态宏提供了这种灵活性。</st> <st c="20877">全名或计算年龄是很好的例子</st> <st c="20926">。</st>
<st c="20934">探索容器</st>
<st c="20958">到目前为止,我们讨论了如何使用</st> `<st c="21031">@Model</st>` <st c="21037">宏声明不同的实体,使用</st> `<st c="21082">@Relationship</st>` <st c="21095">宏定义它们的关系,以及使用</st> `<st c="21144">@</st>``<st c="21145">Attribute</st>` <st c="21154">宏自定义它们的属性。</st>
<st c="21161">然而,我们还没有讨论如何设置 SwiftData 与模式和一个</st> <st c="21241">持久存储</st>一起工作。</st>
<st c="21258">当我们深入研究</st> `<st c="21283">@Model</st>` <st c="21289">宏时,与 Core Data 的比较是直接的,并且现在仍然是如此。</st> <st c="21370">在 Core Data 中,我们使用</st> `<st c="21410">NSPersistentContainer</st>`<st c="21431">设置堆栈,它将数据模型、存储和上下文等不同组件封装到一个我们可以</st> <st c="21557">与之工作的堆栈中。</st>
<st c="21567">在 SwiftData 中,我们使用</st> `<st c="21589">模型容器</st>`<st c="21603">,它具有相同的职责。</st>
<st c="21639">让我们尝试理解它是如何工作的。</st> <st c="21668">。</st>
<st c="21677">设置模型容器</st>
`<st c="21703">模型容器</st>` <st c="21718">对于使用 SwiftData 至关重要。</st> <st c="21732">原因是 SwiftData 有三个主要组件,容器将它们封装并</st> <st c="21851">一起包装:</st>
+ `<st c="21962">@Model</st>` <st c="21968">宏到</st> <st c="21978">我们的实体</st>
+ **<st c="21990">存储</st>**<st c="22000">:我们将保存数据的后端存储
+ **<st c="22048">上下文</st>**<st c="22060">:这是我们与存储和沙盒的链接,我们可以添加、编辑和删除</st> <st c="22145">不同的记录</st>
<st c="22162">以下是以基本和最小的方式创建</st> <st c="22207">容器的方法:</st>
var container: ModelContainer = {
do {
return try <st c="22271">ModelContainer</st>(for:
Schema([<st c="22300">Book.self, Author.self, Page.self</st>]) )
} catch {
fatalError("Could not create ModelContainer:
\(error)")
}
}()
<st c="22411">在这段代码中,我们从一个</st> `<st c="22455">模型容器</st>` <st c="22469">类型创建一个对象,并为其提供我们在</st> *<st c="22535">定义 SwiftData</st>* *<st c="22556">模型</st>* <st c="22561">部分中早先创建的三个模型。</st>
<st c="22570">请注意,在我们的情况下,我们有一个参数,</st> `<st c="22618">模式</st>`<st c="22624">,它包含与我们的容器相关的所有不同模型 –</st> `<st c="22691">书籍</st>`<st c="22695">,`<st c="22697">作者</st>`<st c="22703">,`<st c="22705">和</st> `<st c="22709">页面</st>`<st c="22713">。</st>
<st c="22714">我们需要提供一个模型列表的事实可能会让人感到惊讶 – 为什么我们需要这样做呢?</st> <st c="22818">Xcode 不能定位所有模型并自动将它们添加进去吗?</st> <st c="22880">的确,</st> `<st c="22884">@Model</st>` <st c="22890">宏在编译时扩展代码,但这并不意味着 SwiftData 在应用运行开始时设置时就知道我们所有的实体。</st> <st c="23044">因此,每次我们添加一个新的模型时,我们必须将其添加到我们的</st> `<st c="23121">模式</st>` <st c="23127">参数中的模型列表中。</st>
<st c="23138">关于独立包含</st> `<st c="23163">书籍</st>` <st c="23167">实体,而不是</st> `<st c="23198">作者</st>` <st c="23204">实体 – 当我们将</st> `<st c="23230">书籍</st>` <st c="23234">模型添加到模型列表中时,它自动包含所有相关模型,包括那些与</st> <st c="23369">层次结构中更下方的模型</st> <st c="23380">相关的模型。</st> <st c="23380">这意味着,从理论上讲,我们可以在进行类似操作时只包含根对象:</st>
Schema([Book.self])
<st c="23498">这</st> <st c="23508">就足够包含</st> `<st c="23534">作者</st>` <st c="23540">和</st> `<st c="23545">页面</st>`<st c="23549">。</st>
<st c="23550">所以,我们将如何使用我们刚刚创建的容器实例呢?</st> <st c="23616">让我们在下一节中看看。</st> <st c="23633">译文:</st>
<st c="23646">使用模型容器修饰符连接容器</st>
<st c="23705">现在我们有了模型容器,我们希望以某种方式将其与我们的 UI 连接起来,以便我们可以开始使用它。</st> <st c="23715">译文:</st>
<st c="23809">为了做到这一点,我们将使用`<st c="23838">modelContainer</st>` `<st c="23852">修饰符将容器连接到我们的场景:</st> <st c="23890">译文:</st>
var body: some Scene {
WindowGroup {
ContentView()
} <st c="23954">.modelContainer(container)</st> }
<st c="23982">在我们的代码示例中,我们将`<st c="24014">modelContainer</st>` `<st c="24028">修饰符添加到我们的`<st c="24045">WindowGroup</st>` `<st c="24056">中,使其在整个应用程序中可用。</st> <st c="24090">译文:</st>
<st c="24100">我们不需要创建连接器并将其连接到`<st c="24154">WindowGroup</st>`,我们可以使用另一个`<st c="24186">modeContainer</st>` `<st c="24199">init</st>` `<st c="24204">方法,并仅传递实体列表:</st> <st c="24235">译文:</st>
.modelContainer(for: [Book.self, Author.self, Page.self])
<st c="24304">传递实体列表可以是一种简单且易于设置容器的方法。</st> <st c="24390">那么,为什么我们需要</st> `<st c="24413">ModelContainer</st>` <st c="24427">类呢?</st> <st c="24435">简单的回答是,一如既往,为了提供更多的定制。</st> <st c="24499">让我们</st> <st c="24505">看看吧!</st> <st c="24513">译文:</st>
<st c="24513">译文:</st>
<st c="24545">`<st c="24550">ModelContainer</st>` `<st c="24564">不仅提供了模式传递的功能;它还赋予我们配置特定模型的`<st c="24638">SwiftData</st>` `<st c="24647">存储并对其进行定制以满足我们`<st c="24698">特定需求`的能力。</st> <st c="24705">译文:</st>
<st c="24722">为了做到这一点,我们将使用`<st c="24751">ModelConfiguration</st>` `<st c="24769">结构体,如下所示:</st> <st c="24778">译文:</st>
var modelContainer: ModelContainer = {
do {
let schema = Schema([Book.self, Author.self,
Page.self]) <st c="24891">let modelConfiguration =</st>
<st c="24915">ModelConfiguration(schema: schema,</st>
<st c="24950">isStoredInMemoryOnly: true)</st> return try ModelContainer(for: schema, <st c="25018">configurations: [modelConfiguration]</st>)
} catch {
fatalError("Could not create ModelContainer:
\(error)")
}
}()
<st c="25128">让我们尝试理解这个代码片段中正在发生的事情。</st> <st c="25193">首先,我们创建一个包含我们模型列表的模式。</st> <st c="25246">然后,我们声明一个模型配置结构体,传递模式,并将其后端存储设置为内存。</st> <st c="25350">最后,我们根据我们的模式和刚刚创建的配置集合返回一个模型容器。</st> <st c="25449">译文:</st>
<st c="25462">整个过程感觉有点笨拙、笨拙和重复——如果我们再次传递相同的模式,为什么还需要创建配置呢?</st> <st c="25613">而且为什么它是集合形式呢?</st> <st c="25634">主要配置思想是为不同的模型集合提供不同的行为。</st> <st c="25715">译文:</st>
<st c="25725">这里有一个例子。</st> <st c="25745">想象一下,我们有一个头脑风暴草图应用。</st> <st c="25786">我们想在应用程序的持久存储中绘制并存储我们的概念,而白板画布上的所有绘图都保留在内存中。</st> <st c="25910">译文:</st>
在此情况下,我们可以<st c="25920">创建两个配置,一个用于内存,一个用于</st> <st c="25942">持久存储和</st> **<st c="26022">CloudKit</st>** <st c="26030">集成:</st>
var modelContainer: ModelContainer = {
do {
let <st c="26092">brainstormDataConfiguration</st> =
ModelConfiguration("brainstorm_configuration",
schema: schemaForBrainstorm,
isStoredInMemoryOnly: true)
let <st c="26230">projectsDataConfiguration</st> =
ModelConfiguration("projects_configuration",
schema: schemaForProjects,
cloudKitDatabase: .automatic)
return try ModelContainer(for: fullSchema,
configurations: [<st c="26420">brainstormDataConfiguration,</st>
<st c="26449">projectsDataConfiguration</st>])
} catch {
fatalError("Could not create ModelContainer:
\(error)")
}
}()
<st c="26550">在我们的例子中,我们创建了两个不同的模式——一个用于头脑风暴的模型列表和一个用于</st> <st c="26666">用户项目的模型列表。</st>
<st c="26680">基于这些模型,我们创建了两个不同的配置。</st> <st c="26744">头脑风暴配置保存在内存中,而项目配置保存在本地并同步到</st> <st c="26860">CloudKit。</st>
<st c="26872">使用两个不同的配置和两个应用程序功能的模式是模型配置使用的绝佳例子。</st> <st c="26997">我们可以使用模型配置进行额外的自定义,例如</st> <st c="27070">以下内容:</st>
+ <st c="27084">不同的</st> <st c="27095">存储文件</st>
+ <st c="27106">不同的</st> <st c="27117">组容器</st>
+ <st c="27133">不同的</st> <st c="27144">自动保存机制</st>
<st c="27166">然而,假设我们不需要模型配置来为不同的模型组配置不同的行为</st> <st c="27260">。</st> <st c="27292">在这种情况下,我们可以直接与模型容器一起工作,并使用整个模式</st> <st c="27377">来初始化它。</st>
<st c="27391">我们现在知道如何声明和分组我们的模型以用于模型容器中的模式。</st> <st c="27486">但还有一个关键的东西缺失——如何插入、更新和获取数据。</st> <st c="27565">我们将通过放置拼图中的缺失部分——</st> <st c="27626">上下文。</st> 来完成这些操作。
<st c="27638">使用模型上下文获取和操作我们的数据</st>
<st c="27693">熟悉 Core Data 的开发者也熟悉**<st c="27764">上下文</st>**<st c="27771">的概念。上下文是我们的数据</st> <st c="27793">沙盒。</st> <st c="27802">这是我们可以操作和获取数据的地方,也是我们模型和</st> <st c="27844">持久存储之间的</st> <st c="27878">链接。</st>
<st c="27922">要访问我们的上下文以从我们的 SwiftUI 视图中获取,我们可以使用一个名为</st> <st c="28011">modelContext</st> <st c="28029">的环境变量:</st>
struct ContentView: View { <st c="28059">@Environment(\.modelContext)</st> private var modelContext
}
<st c="28114">当使用</st> `<st c="28205">modelContainer</st>` <st c="28219">修饰符设置场景时,`<st c="28119">modelContext</st>` <st c="28131">环境变量始终可用。</st>
<st c="28229">在非 SwiftUI</st> <st c="28245">实例中,我们可以使用我们的模型容器</st> `<st c="28308">mainContext</st>` <st c="28319">属性来访问上下文:</st>
let modelContext = modelContainer.mainContext
<st c="28375">为了理解</st> <st c="28390">如何与模型上下文一起工作,我们将</st> <st c="28430">从最基本的操作开始,为我们的存储保存新的对象。</st>
<st c="28500">保存新对象</st>
<st c="28519">在本章的开头,在</st> *<st c="28561">定义 SwiftData 模型</st>* <st c="28587">部分,我们了解到我们的模型只是标记有</st> `<st c="28661">@</st>``<st c="28662">Model</st>` <st c="28667">宏的 Swift 类。</st>
<st c="28674">我们在 SwiftData 中定义模型的方式也意味着新实例的创建对我们来说非常直接</st> <st c="28686">:</st>
let newBook = Book(name: "Mastering iOS 18 – the future")
<st c="28845">我们的下一步是将该书籍实例添加到</st> <st c="28892">我们的上下文中:</st>
modelContext.insert(newBook)
<st c="28933">添加</st> `<st c="28941">newBook</st>` <st c="28948">到模型上下文并不一定意味着它被保存到我们的持久存储中,但它确实意味着它在我们的上下文中,并且已准备好被推送到我们的存储。</st> <st c="29129">在我们的上下文中,我们可以进行更改,添加和删除信息,而无需实际将这些操作保存到我们的数据存储中。</st> <st c="29247">上下文在处理并发操作或当我们想要管理</st> <st c="29466">撤销操作时非常有用。</st>
<st c="29482">要实际保存到持久存储,我们可以使用上下文的</st> `<st c="29548">save()</st>` <st c="29554">方法:</st>
try? modelContext.<st c="29593">save()</st> method pushes changes to the store for each model, according to its configuration.
<st c="29682">The way the</st> `<st c="29695">save()</st>` <st c="29701">method works resembles how Core Data works.</st> <st c="29746">But there’s one difference here.</st> <st c="29779">SwiftData allows us to have an</st> *<st c="29810">auto-save</st>* <st c="29819">feature for the</st> <st c="29836">model container:</st>
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Book.self, <st c="29938">isAutosaveEnabled:</st>
}
<st c="29966">In our code</st> <st c="29979">example, we set the</st> `<st c="29999">isAutosaveEnabled</st>` <st c="30016">parameter to</st> `<st c="30030">false</st>`<st c="30035">. By default, SwiftData auto-saves every change we make to the persistent store, so there’s no need to call the</st> `<st c="30147">save()</st>` <st c="30153">function unless you have a</st> <st c="30181">perfect reason.</st>
<st c="30196">Due to performance considerations, SwiftData doesn’t save every single time we perform a change to the context but, rather, in the following</st> <st c="30338">two situations:</st>
* <st c="30353">During the app life cycle – for example, when moving from the foreground to</st> <st c="30430">the background</st>
* <st c="30444">In a certain time period after we perform</st> <st c="30487">the change</st>
<st c="30497">Now that we know how to create and insert new objects, we can move on</st> <st c="30568">to fetching.</st>
<st c="30580">Fetching objects</st>
<st c="30597">Fetching objects in SwiftData is slightly different than what we know from Core Data, as there are</st> <st c="30697">two primary ways to</st> <st c="30717">retrieve data.</st>
<st c="30731">The first way</st> <st c="30746">is to fetch an object, or objects,</st> *<st c="30781">based on a predicate</st>* <st c="30801">as part of an app flow – for example, fetching objects to sync with the server or to make some kind</st> <st c="30902">of calculation.</st>
<st c="30917">The second way is to fetch objects</st> *<st c="30953">based on a query</st>* <st c="30969">and bind them to the SwiftUI view.</st> <st c="31005">An example would be when we want to bind a collection of objects to</st> <st c="31073">a list.</st>
<st c="31080">Let’s go over both ways and explore new structures and macros that SwiftData brings to</st> <st c="31168">our project.</st>
<st c="31180">Fetching objects using FetchDescriptor</st>
`<st c="31219">FetchDescriptor</st>` <st c="31235">is a struct equivalent to</st> `<st c="31262">NSFetchRequest</st>` <st c="31276">in</st> <st c="31280">Core Data.</st>
<st c="31290">Like</st> `<st c="31296">NSFetchRequest</st>`<st c="31310">,</st> `<st c="31312">FetchDescriptor</st>` <st c="31327">also works with a specific type of object; to use it, we can pass an optional predicate and</st> <st c="31420">sort descriptor.</st>
<st c="31436">Here’s</st> <st c="31444">an example of how to</st> <st c="31465">use</st> `<st c="31469">FetchDescriptor</st>`<st c="31484">:</st>
let fetchDesciprtor =
Predicate { $0.name == "My Book"})
let book = try? modelContext.fetch(fetchDesciprtor).first
<st c="31636">If you look closely, you can see that</st> `<st c="31675">FetchDescriptor</st>` <st c="31690">is not the only new type we encounter in this context, as we also have a new</st> `<st c="31768">Predicate</st>` <st c="31777">macro that creates</st> `<st c="31797">PredicateExpression</st>` <st c="31816">(a new type in</st> <st c="31832">iOS 17).</st>
<st c="31840">Unlike the familiar</st> `<st c="31861">NSPredicate</st>`<st c="31872">, the</st> `<st c="31878">Predicate</st>` <st c="31887">macro works a little bit differently.</st> <st c="31926">Instead of creating a query, we have a closure where we define the condition of the return instances, like the array</st> <st c="32043">filter method.</st>
<st c="32057">The following example returns books with more than</st> <st c="32109">10 pages:</st>
let fetchDesciprtor = FetchDescriptor
Predicate { book in
return book.pages.count > 10
})
<st c="32226">Using the</st> `<st c="32237">#Predicate</st>` <st c="32247">macro is simple and doesn’t require us to use a special syntax to perform</st> <st c="32322">complex queries.</st>
<st c="32338">In most cases, we won’t have to use</st> `<st c="32375">FetchDescriptor</st>`<st c="32390">. If we want to connect data to our SwiftUI views, SwiftData has a better solution – the</st> `<st c="32479">@</st>``<st c="32480">Query</st>` <st c="32485">macro.</st>
<st c="32492">Conn</st><st c="32497">ecting data to a view using the @Query macro</st>
<st c="32542">Data is there to be seen.</st> <st c="32569">Showing information to the user is perhaps the most common task</st> <st c="32633">for iOS developers, and SwiftData’s</st> <st c="32669">goal is just to</st> <st c="32685">simplify that.</st>
<st c="32699">As part</st> <st c="32708">of the SwiftData package, we now have an</st> `<st c="32749">@Query</st>` <st c="32755">macro that helps us present information in</st> <st c="32799">SwiftUI views:</st>
List {
ForEach(books) { book in
Text(book.name)
}
}
}
<st c="32922">This example displays a simple list of</st> `<st c="32962">Book</st>` <st c="32966">items based on the</st> `<st c="32986">books</st>` <st c="32991">variable.</st> <st c="33002">The</st> `<st c="33006">@Query</st>` <st c="33012">macro before the variable declaration makes the variable a state of the view, ensuring that data is constantly updated.</st> <st c="33133">This means that we get an instant UI update whenever we insert a new book into our</st> <st c="33216">persistent store.</st>
<st c="33233">This is pretty remarkable for just one</st> <st c="33273">additional word!</st>
<st c="33289">The</st> `<st c="33294">@Query</st>` <st c="33300">macro also has two important additional features – filter</st> <st c="33359">and sorting.</st>
<st c="33371">Filtering the query</st>
<st c="33391">The chances</st> <st c="33404">that we will fetch</st> *<st c="33423">all</st>* <st c="33426">the items of a particular entity are pretty low, and the previous example of fetching all the books and presenting them is more common in tutorials and</st> <st c="33579">demo presentations.</st>
<st c="33598">In real life, we want to filter our queries.</st> <st c="33644">To do that, we can use</st> `<st c="33667">#Predicate</st>`<st c="33677">, which we learned about in the</st> *<st c="33709">Fetching objects using</st>* *<st c="33732">FetchDescriptor</st>* <st c="33747">section:</st>
@Query(
<st c="33951">当然,我们可以通过升级在谓词内的 Swift 表达式来执行更复杂的查询:</st> <st c="34045">:</st>
@Query(filter: <st c="34075">#Predicate<Book> {</st>
<st c="34093">$0.pages.count > 300 && (!$0.isRead ||</st>
<st c="34132">$0.isFavorite)</st> }) private var bigBooks: [Book]
<st c="34179">在这个例子中,我们过滤出包含超过 300 页的书籍,但这次,我们还想</st> <st c="34278">接收那些我们尚未阅读或标记为收藏的书籍。</st> <st c="34341">我们使用 Swift 表达式来过滤结果,这使得我们的查询更加描述性和强大</st> <st c="34451">,比</st> `<st c="34456">NSPredicate</st>`<st c="34467">.</st>
<st c="34468">然而,当在列表中显示数据时,仅仅过滤是不够的;还需要对其进行排序。</st> <st c="34572">这就是我们第二个主要</st> `<st c="34606">@</st>``<st c="34607">Query</st>` <st c="34612">功能的作用。</st>
<st c="34621">对数据进行排序</st>
<st c="34638">排序是向用户展示信息的一个基本方面。</st> <st c="34710">我们应该记住</st> <st c="34729">排序不是一个轻量级任务;它需要一个复杂的算法才能高效完成。</st>
<st c="34824">这就是为什么我们需要确保我们可以按符合 iOS 15 中引入的`<st c="34912">SortComparator</st>` <st c="34926">协议的类型属性进行排序。</st>
<st c="34966">让我们看看我们如何对</st> <st c="34997">过滤后的书籍进行排序:</st>
@Query(filter: #Predicate<Book> {
$0.pages.count > 300
}, <st c="35071">sort: [SortDescriptor(\Book.name),</st>
<st c="35105">SortDescriptor(\Book.pages.count)]</st>) private var
bigBooks: [Book]
<st c="35172">在这个例子中,我们传递了一个`<st c="35210">SortDescriptor</st>` <st c="35224">数组——我们首先按书名排序,然后按页数排序。</st> <st c="35291">使用`<st c="35315">SortDescriptor</st>` <st c="35329">非常简单——我们使用一个指向所需属性的键路径来初始化它,就像在先前的例子中一样。</st>
使用 SwiftData 进行排序极其简单。<st c="35426">然而,在底层,它需要运行必须针对性能进行优化的算法,以便高效工作。</st> <st c="35482">当我们处理 100 或 200 条记录时,我们不需要这些优化。</st> <st c="35519">然而,当我们的数据存储包含数千条记录时,情况就不同了。</st> <st c="35607">在这些情况下,我们需要对数据进行索引。</st> <st c="35679">我们的数据。</st>
<st c="35798">添加#Index 宏以提高性能</st>
<st c="35838">在我们对数据进行索引之前,让我们先了解一下这究竟意味着什么。</st> <st c="35912">当执行排序或查询等读取操作时,我们希望我们的应用程序能够与数千条记录无缝工作。</st> <st c="36037">显然,对整个表进行全表扫描以查找名为</st> *<st c="36098">Mastering iOS 18</st>* <st c="36114">的书籍是不高效的。</st> <st c="36131">那么我们该怎么办呢?</st> <st c="36150">就像书籍索引一样,数据库索引包含键,帮助它定位特定记录。</st> <st c="36241">例如,如果我们想对书籍的</st> `<st c="36285">name</st>` <st c="36289">属性进行索引,我们可以创建一个数据结构,如 B 树,它可以帮助我们根据</st> <st c="36407">其名称定位精确实例。</st>
<st c="36416">在 SwiftData 中,我们不需要创建任何结构来索引我们的数据。</st> <st c="36488">我们只需要将</st> `<st c="36517">#Index</st>` <st c="36523">宏添加到</st> <st c="36533">我们的模型中:</st>
@Model
class Book { <st c="36564">#Index<Book>([\.name], [\.name, \.publicationName])</st> var publicationName: String = "Packt"
var name: String
}
<st c="36672">如果前面的代码看起来很熟悉,那是因为我们在</st> *<st c="36803">添加 @Attribute</st>* *<st c="36825">宏</st>* <st c="36830">部分添加了</st> `<st c="36769">#Unique</st>` <st c="36776">宏到我们的模型中时,我们做了类似的事情。</st>
<st c="36839">在这种情况下,我们决定为我们</st> <st c="36887">模型添加两个索引:</st>
+ <st c="36897">第一个是索引名称属性,允许应用程序按名称排序记录或查询特定</st> `<st c="37008">书籍名称</st>` <st c="37095">的数据。</st>
+ <st c="37018">第二个索引是基于</st> `<st c="37071">name</st>` <st c="37075">和</st> `<st c="37080">publicationName</st>` <st c="37095">属性的</st>
<st c="37106">如果您还记得从</st> *<st c="37132">添加 @Attribute 宏</st>* <st c="37159">部分,我们决定这个组合定义了我们书籍的独特性。</st> <st c="37233">为这个组合创建索引可以帮助我们在需要时快速找到特定的书籍</st> <st c="37313">。</st>
<st c="37325">索引看起来像魔法——我们向索引列表中添加另一个键路径,然后一切运行得更快。</st> <st c="37431">那么,为什么不将此应用于所有属性呢?</st> <st c="37471">有什么</st> <st c="37478">问题吗?</st>
<st c="37488">这是因为</st> <st c="37502">索引是有代价的。</st> <st c="37531">首先,我们需要复制一些我们的数据。</st> <st c="37577">如果我们需要索引名称属性,我们需要创建一个包含所有名称的结构。</st> <st c="37675">这导致我们的应用程序需要额外的存储空间。</st> <st c="37723">但添加索引并不止于存储——它还会影响性能。</st> <st c="37800">索引不是一次性操作,因为它需要维护。</st> <st c="37866">每次</st> `<st c="37871">插入</st>`<st c="37877">、</st> `<st c="37879">更新</st>`<st c="37885">或</st> `<st c="37890">删除</st>` <st c="37896">操作都需要 SwiftData 维护索引结构,从而影响</st> <st c="37973">操作性能。</st>
<st c="37995">总的来说,索引是 SwiftData 的一个优秀功能。</st> <st c="38047">然而,请谨慎使用,并权衡其好处与</st> <st c="38103">成本。</st>
<st c="38113">我们到目前为止已经学到了很多东西!</st> <st c="38151">我们学习了如何定义模型、创建实例、获取它们并将它们连接到</st> <st c="38237">UI。</st>
<st c="38244">但我们知道,维护持久存储远不止这些。</st> <st c="38263">我们的第一个应用程序版本与我们的第 50 个版本大不相同,这也意味着我们的数据模式将在应用程序版本的生命周期中发生变化。</st> <st c="38472">但我们已经有一个数据满载的存储库,我们应该怎么办呢?</st> <st c="38537">这就是我们下一个主题——如何执行</st> <st c="38576">数据迁移。</st>
<st c="38591">将我们的数据迁移到新架构</st>
<st c="38626">对于那些使用过 Core Data 的人来说,数据迁移不是一个奇怪的表达。</st> <st c="38710">很明显,随着我们的应用发展,我们需要更改我们的</st> <st c="38724">架构。</st>
<st c="38777">有两种类型的迁移——</st> *<st c="38814">轻量级</st>* <st c="38825">和</st> *<st c="38830">自定义</st>* <st c="38836">迁移。</st> <st c="38848">在轻量级迁移中,我们执行不需要自定义逻辑的更改。</st> <st c="38928">例如,添加实体、属性和关系都是轻量级迁移的好例子。</st> <st c="39038">相反,更改属性类型、使属性唯一以及基于其他属性创建新属性都是自定义迁移的例子。</st> <st c="39191">现在我们知道了有哪些迁移类型,了解何时进行迁移是重要的。</st>
<st c="39310">在我们处于开发阶段时,在我们拥有 App Store 上的官方版本之前,迁移是不必要的。</st> <st c="39437">我们只需要在最终用户持有</st> <st c="39494">较旧架构的版本时进行迁移。</st> <st c="39526">这也意味着,如果我们对几个版本进行架构更改,我们必须确保 SwiftData 知道如何在整个</st> <st c="39663">这些版本中进行迁移。</st>
<st c="39678">现在,让我们讨论 SwiftData 迁移的工作原理以及基本迁移</st> <st c="39761">组件。</st>
<st c="39776">学习基本迁移过程</st>
<st c="39813">SwiftData 迁移有三个</st> <st c="39826">主要组件:</st>
+ `<st c="39862">版本架构</st>`<st c="39877">: 描述特定的</st> <st c="39901">架构版本</st>
+ `<st c="39915">迁移阶段</st>`<st c="39930">: 描述同一架构版本之间的迁移过程</st>
+ `<st c="40004">架构迁移计划</st>`<st c="40024">: 描述架构迁移阶段是基于</st> <st c="40086">迁移阶段</st>
<st c="40102">让我们尝试描述如何使用</st> *<st c="40160">图 2</st>**<st c="40168">.2</st>*<st c="40170">来展示所有事物之间的联系:</st>

<st c="40295">图 2.2:三个不同版本之间的迁移过程</st>
*<st c="40359">图 2</st>**<st c="40368">.2</st>* <st c="40370">展示了三个不同版本的三个不同版本架构。</st> <st c="40439">每次我们将应用程序从一个版本迁移到另一个版本时,我们都会创建一个迁移</st> <st c="40461">阶段。</st> <st c="40525">一旦我们有了各种阶段,我们就可以将它们封装成一个大的</st> <st c="40588">迁移计划。</st>
<st c="40603">回到我们的书籍应用,让我们尝试将我们的架构迁移以支持</st> `<st c="40678">副标题</st>` <st c="40686">为我们</st> `<st c="40695">Book</st>` <st c="40699">实体。</st>
<st c="40707">首先,我们需要创建我们的</st> <st c="40737">版本架构。</st>
<st c="40753">创建版本架构</st>
<st c="40779">为了将</st> <st c="40791">我们的书籍迁移到新的架构,我们需要创建两个版本架构——第一个是我们当前的架构,第二个是</st> <st c="40912">目标架构:</st>
enum BookSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version
{ return .init(1, 0, 0) }
static var models: [any PersistentModel.Type] {
[Book.self]
}
@Model class Book {
var name: String
init(name: String) {
self.name = name
}
}
}
enum BookSchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version
{return .init(1, 1, 0) }
static var models: [any PersistentModel.Type] {
[Book.self]
}
@Model class Book {
<st c="41373">var subtitle: String = ""</st> var name: String
init(subtitle: String, name: String) { <st c="41455">self.subtitle = subtitle</st> self.name = name
}
}
}
<st c="41502">在这段代码中,我们创建了两个符合</st> `<st c="41558">VersionedSchema</st>` <st c="41573">协议的枚举。</st> <st c="41584">作为协议定义的一部分,我们需要定义版本标识符以及哪些模型</st> <st c="41677">将发生变化。</st>
<st c="41689">在这种情况下,我们向第二个版本添加了一个新的</st> `<st c="41705">副标题</st>` <st c="41713">属性。</st> <st c="41759">我们需要更新整个应用中使用的架构,包括新的</st> <st c="41824">属性。</st>
<st c="41842">我们的下一步是定义不同的阶段和</st> <st c="41899">迁移计划。</st>
<st c="41914">创建迁移阶段和计划</st>
<st c="41953">我们应该</st> <st c="41964">将版本架构视为我们迁移过程的构建块。</st> *<st c="42044">图 2</st>**<st c="42052">.2</st>* <st c="42054">显示我们根据</st> <st c="42110">版本架构创建迁移阶段。</st>
<st c="42128">这是一个迁移阶段的示例:</st>
static let migrateV1toV2 = <st c="42196">MigrationStage.lightweight</st>(fromVersion:
BookSchemaV1.self, toVersion: BookSchemaV2.self)
<st c="42285">`<st c="42290">migrateV1toV2</st>` <st c="42303">阶段处理从</st> `<st c="42337">BookSchemaV1</st>` <st c="42349">到</st> `<st c="42353">BookSchemaV2</st>`<st c="42365">的迁移。</st> 注意,这是一个轻量级迁移——我们只添加了一个属性,所以这就是我们需要创建</st> <st c="42475">阶段的所有内容。</st>
<st c="42485">关于自定义迁移呢?</st> <st c="42517">使用自定义迁移,我们需要提供一个闭包来处理迁移阶段前后数据,在那里我们执行所有</st> <st c="42652">所需的变化。</st>
<st c="42669">这是一个从版本 V2 到 V3 的自定义过渡示例,其中我们移除了副标题属性并将其作为</st> <st c="42806">书名的一部分:</st>
static let migrateV2toV3 = <st c="42844">MigrationStage.custom</st>(fromVersion: BookSchemaV2.self,
toVersion: BookSchemaV3.self, <st c="42929">willMigrate</st>: { context in
if let books = try? context.fetch(FetchDescriptor<Book>()) {
for book in books {
let newName = book.name + " " +
book.subtitle
book.name = newName
}
}
try? context.save()
}, didMigrate: nil)
<st c="43147">正如我们可以在代码示例中看到的那样,我们的</st> `<st c="43187">willMigrate</st>` <st c="43198">闭包接收一个上下文来工作,SwiftData 在需要时执行该闭包</st> <st c="43276">。</st>
<st c="43288">我们获取所有书籍并从书名及其副标题属性中组装一个新的名称。</st> <st c="43382">在关闭代码的末尾,我们</st> <st c="43417">调用</st> `<st c="43422">context.save()</st>`<st c="43436">。</st>
<st c="43437">现在我们有了迁移步骤,我们可以创建我们的</st> <st c="43495">迁移计划:</st>
<st c="43510">enum MyMigrationPlan: SchemaMigrationPlan</st> {
static var schemas: [VersionedSchema.Type] {
[BookSchemaV1.self, BookSchemaV2.self,
BookSchemaV3.self]
} <st c="43660">static var stages: [MigrationStage] {</st>
<st c="43697">[migrateV1toV2, migrateV2toV3]</st>
<st c="43728">}</st> static let migrateV1toV2 =
MigrationStage.lightweight(fromVersion:
BookSchemaV1.self, toVersion: BookSchemaV2.self)
static let migrateV2toV3 =
MigrationStage.custom(fromVersion: BookSchemaV2.self,
toVersion: BookSchemaV3.self, willMigrate:{context in
if let books = try? context.fetch(FetchDescriptor<Book>()) {
for book in books {
let newName = book.name + " " +
book.subtitle
book.name = newName
}
}
try? context.save()
}, didMigrate: nil)
}
<st c="44174">迁移</st> <st c="44189">计划只是符合</st> `<st c="44229">SchemaMigrationPlan</st>`<st c="44248">的另一个枚举,其中静态变量描述了模式列表和阶段(不是我们之前没有</st> <st c="44340">见过的东西)。</st>
<st c="44353">现在,我们有了迁移计划,但 SwiftData 不知道如何处理它。</st> <st c="44432">我们的下一步将是将迁移计划连接到我们的</st> <st c="44491">SwiftData 容器。</st>
<st c="44511">将迁移计划连接到我们的容器</st>
<st c="44558">将</st> <st c="44570">迁移计划连接到我们的容器可能是这个过程中最直接的一步。</st>
<st c="44662">`<st c="44667">ModelContainer</st>` <st c="44681">结构体有一个</st> `<st c="44695">migrationPlan</st>` <st c="44708">属性专门用于此,我们需要传递迁移计划</st> <st c="44780">枚举类型:</st>
return try ModelContainer(for: schema, <st c="44830">migrationPlan:</st>
<st c="44844">MyMigrationPlan.self,</st> configurations:
[modelConfiguration])
<st c="44904">注意 SwiftData 在语言范式方面迁移的工作方式。</st> <st c="44985">我们不需要初始化任何东西,因为我们只传递模式、阶段和计划类型。</st> <st c="45079">原因是 SwiftUI 的工作方式——由于我们在不可变环境中工作,使用静态变量和类型而不是实例要方便得多</st> <st c="45235">。</st>
<st c="45248">在 SwiftData 中迁移不是一个简单的任务。</st> <st c="45294">它涉及到遵守多个协议、维护模式版本,以及理解如何构建存储以在轻量级和</st> <st c="45442">自定义迁移之间切换。</st>
<st c="45459">但这是因为迁移,总的来说,是一个复杂且敏感的过程。</st> <st c="45539">在事先仔细规划我们的模式看起来如何时,可以减少模式版本和阶段的数量,简化我们在考虑</st> <st c="45684">我们将在某个时候迁移我们的存储时</st> <st c="45727">的过程。</st>
<st c="45738">摘要</st>
<st c="45746">SwiftData 对希望支持 iOS 17 及以上版本的 iOS 开发者具有重要意义,它代表了从苹果之前框架 Core Data 的自然演进。</st> <st c="45911">在声明式 Swift 环境中,SwiftData 比以前更无缝地</st> <st c="45999">对齐。</st>
<st c="46011">在本章中,我们了解了 SwiftData 的背景,定义了不同的 SwiftData 模型,创建了关系,并自定义了模型属性。</st> <st c="46173">然后我们转向容器——一个将所有内容包装在一起、执行获取和保存操作的组件。</st> <st c="46277">最后,我们使用轻量级和自定义迁移将数据从不同的模式版本迁移过来。</st> <st c="46378">在整个章节中,我们看到了 Swift 宏和协议的广泛使用,这些在 Swift 的现代世界中比 Objective-C 更合适。</st> <st c="46517">。</st>
<st c="46532">这章内容很多!</st> <st c="46563">请记住,数据层是复杂的,管理和维护它需要学习很多。</st> <st c="46596">数据层是项目的一侧;当然,另一侧是 UI。</st> <st c="46741">为了完整理解数据层,探索 UI 如何监控变化是至关重要的。</st> <st c="46848">这就是为什么我们即将到来的章节将专注于</st> <st c="46902">观察框架。</st>
第四章:3
理解 SwiftUI 观察系统
-
回顾 SwiftUI 观察系统并讨论 其问题 -
添加 <st c="719">@Observable</st>宏并学习它是如何 工作的 -
讨论观察属性,包括 计算变量 -
使用环境变量并将它们适应到 新框架 -
讨论新的 <st c="906">@Bindable</st>属性包装器 -
学习如何将我们的应用程序迁移到与 观察框架 一起工作
技术要求
回顾 SwiftUI 观察系统
-
<st c="1564">@</st>``<st c="1565">Binding</st>, <st c="1574">@Environment</st> -
<st c="1600">@State</st>, <st c="1608">@Binding</st>, <st c="1618">@</st>``<st c="1619">StateObject</st>, <st c="1632">@Environment</st> -
<st c="1668">@</st>``<st c="1669">ObservableObject</st>, <st c="1687">@Published</st> -
<st c="1715">@AppStorage</st>、<st c="1726">@</st><st c="1729">SceneStorage</st>、<st c="1741">@EnvironmentObject</st>
<st c="1762">不同的级别让我们了解不同包装器的不同角色。</st> <st c="1850">让我们来探讨一些这些包装器,以了解系统是如何工作的。</st>
<st c="1923">一个本地的</st> <st c="1932">@State</st> 属性包装器管理视图内部原始属性的状态。例如,一个特定视图是否隐藏、可用按钮的数量、当前排序方法等都是由这个包装器管理的。
<st c="2161">我们使用 <st c="2186">@State</st> 属性包装器的原因是 SwiftUI 视图是不可变的。
<st c="2396">问题开始于我们基于数据模型信息构建视图的时候。</st> <st c="2465">例如,一个书店应用从本地数据文件中显示书籍列表的情况。</st> <st c="2563">在这种情况下,我们的视图必须使用
<st c="2663">现在我们来回顾一下</st>。
<st c="2685">遵守 ObservableObject 协议</st>
我们可以使用 <st c="2729">ObservableObject</st> 协议与 <st c="2795">@ObservedObject</st> 属性包装器一起用于需要被观察的类。
<st c="2865">这是一个 <st c="2889">UserData</st> 类的例子,它成为一个 <st c="2921">@ObservedObject</st> 属性包装器:`
class UserData: <st c="2971">ObservableObject</st> { <st c="2990">@Published</st> var username = "Avi Tsadok"
}
struct ContentView: View {
<st c="3058">@ObservedObject var userData = UserData()</st> var body: some View {
Text("Welcome, \(userData.username)!")
.padding()
}
}
<st c="3175">实现数据类观察有三个部分:</st>
-
<st c="3254">ObservableObject</st>:如果我们想让一个类在 SwiftUI 中被观察,它必须遵守<st c="3342">ObservableObject</st>协议。这表示 SwiftUI,任何从这个类派生出的实例都可以在视图中被观察。 -
<st c="3475">@Published</st><st c="3536">@Published</st>属性包装器,SwiftUI 创建了一个发布者,并在 SwiftUI 视图中使用它。 -
<st c="3658">@ObservedObject</st><st c="3697">@ObservedObject</st>属性包装器在视图和对象之间建立连接,允许视图在变化时被通知。
<st c="3870">@ObservedObject</st>
<st c="4112">@</st>``<st c="4113">StateObj</st><st c="4121">ect</st>
<st c="4129">@StateObject</st> <st c="4173">@State</st>
<st c="4376">@Binding</st> <st c="4423">@State</st>
解释当前观察情况的问题
<st c="4687">ObservableObject</st> <st c="4777">@Published</st> <st c="4777">@Published</st>
添加@Observable宏
@Observable宏<st c="5286">Book</st>
class Book: <st c="5323">ObservableObject</st> { <st c="5342">@Published</st> var title:String = "" <st c="5375">@Published</st> var author: String = "" <st c="5410">@Published</st> var publishedYear: Date = Date() <st c="5454">@Published</st> var numberOfPages: Int = 0
}
<st c="5498">Book</st> 类是一个标准的 <st c="5539">类,包含四个属性,每个属性都使用
<st c="5622">Observation</st> <st c="5697">ObservableObject</st>
<st c="5779">@Observable</st> class Book {
var title:String = ""
var author: String = ""
var publishedYear: Date = Date()
var numberOfPages: Int = 0
}
<st c="5917">@Observable</st> <st c="6007">Book</st>
<st c="6091">Book</st>
struct ContentView: View { <st c="6140">var book:Book = Book()</st> var body: some View {
VStack {
Text(book.title)
Button("Change") { <st c="6230">book.title = "Mastering iOS 17"</st> }
}
.padding()
}
}
<st c="6339">Text</st>
<st c="6542">@ObserverdObject</st> <st c="6562">@StateObject</st>
了解 @Observable 宏的工作原理
<st c="6775">@Observable</st>
@Observable
class Book { <st c="6902">@ObservationTracked</st> var title:String = ""
<st c="6944">@ObservationIgnored private var _title: String = ""</st>
<st c="6995">{</st>
<st c="6997">@storageRestrictions(initializes: _title)</st>
<st c="7039">init(initialValue) {</st>
<st c="7060">_title = initialValue</st>
<st c="7082">}</st>
<st c="7084">get {</st>
<st c="7090">access(keyPath: \.title)</st>
<st c="7115">return _title</st>
<st c="7129">}</st>
<st c="7131">set {</st>
<st c="7137">withMutation(keyPath: \.title) {</st>
<st c="7170">_title = newValue</st>
<st c="7188">}</st>
<st c="7190">}</st>
<st c="7192">}</st>
<st c="7194">@ObservationTracked</st> var author: String = "" <st c="7239">@ObservationTracked</st> var publishedYear: Date = Date() <st c="7292">@ObservationTracked</st> var numberOfPages: Int = 0 <st c="7339">@ObservationIgnored private let _$observationRegistrar</st>
<st c="7393">= Observation.ObservationRegistrar()</st>
<st c="7430">internal nonisolated func access<Member>(</st>
<st c="7472">keyPath: KeyPath<Book , Member></st>
<st c="7504">) {</st>
<st c="7508">_$observationRegistrar.access(self, keyPath:</st>
<st c="7553">keyPath)</st>
<st c="7562">}</st>
<st c="7564">internal nonisolated func withMutation<Member,</st>
<st c="7611">MutationResult>(</st>
<st c="7628">keyPath: KeyPath<Book , Member>,</st>
<st c="7661">_ mutation: () throws -> MutationResult</st>
<st c="7701">) rethrows -> MutationResult {</st>
<st c="7732">try _$observationRegistrar.withMutation(of: self,</st>
<st c="7782">keyPath: keyPath, mutation)</st>
<st c="7810">}</st>
<st c="7812">}</st>
<st c="7813">extension Book: Observation.Observable {</st>
<st c="7853">}</st>
<st c="7951">@ObservationTracked</st>
-
<st c="8157">Observable</st>,不是一个协议。 该协议本身是空的,但 SwiftUI 使用它来标记类为被观察的。 使用扩展,你 可以在宏代码的末尾看到协议的符合情况。 -
<st c="8378">observationRegistrar</st>: <st c="8405">observationRegistrar</st>变量是一个单例结构体,负责管理被观察类属性的注册。 SwiftUI 依赖于这个结构体来检测当被观察属性被访问 或修改时。 -
<st c="8709">Observation</st>框架需要这些获取器和设置器来跟踪每个访问或 修改尝试。 -
<st c="8947">@Observable</st>宏为每个原始变量添加了一个私有变量,仅为此目的。 获取器和设置器使用私有变量来返回和修改存储的值。 -
<st c="9172">access()</st>和 <st c="9185">withMutation()</st>方法。 计算变量调用这些方法来通知 <st c="9265">observationRegistrar</st>实例关于任何数据修改访问。 之后, <st c="9346">observationRegistrar</st>实例会告诉 SwiftUI 这些变化。
<st c="9660">@ObservedObject</st>
<st c="9938">@</st>``<st c="9939">ObservationIgnored</st>
使用 @ObservationIgnored 排除属性观察
<st c="10093">@Published</st> <st c="10147">@Observable</st>
@Observable
class Book {
var title:String = ""
var author: String = ""
var publishedYear: Date = Date()
var numberOfPages: Int = 0 <st c="10944">@ObservationIgnored</st>
<st c="10963">var lastPageRead: Int = 0</st> }
<st c="11039">lastPageRead</st><st c="11220">@</st>``<st c="11221">ObservationIgnored</st>
<st c="11246">与 <st c="11295">@Observable</st> <st c="11306">宏用来创建观察属性获取器和设置器的, <st c="11385">@ObservationIgnored</st> <st c="11404">不会修改属性。</st> <st c="11434">SwiftUI 只使用该宏来确定它不使用
<st c="11549">默认观察所有属性为我们提供了一个即插即用的令人兴奋且强大的功能——观察 <st c="11666">计算变量</st> <st c="11684">。</st>
<st c="11685">观察计算变量
<st c="11714">首先,提醒一下——计算变量是一个具有获取器和可选设置器的属性。</st> <st c="11811">这意味着计算变量没有自己的存储,其值是从其他变量(也可以是计算变量)派生出来的。</st>
<st c="11958">看看下面的代码:</st>
class Book: ObservableObject {
@Published var pages: Int = 0
@Published var averageWordsPerPage: Int = 0 <st c="12092">@Published var totalWordsInBook: Int {</st>
<st c="12130">return pages * averageWordsPerPage</st>
<st c="12165">}</st> }
<st c="12169"> <st c="12173">Book</st> <st c="12177">类遵循古老的
<st c="12235">注意, <st c="12252">totalWordsInBook</st> <st c="12268">属性是一个计算变量——它将 <st c="12321">pages</st> <st c="12326">和 <st c="12331">averageWordsPerPage</st> <st c="12350">变量相乘,以返回书中的总字数。</st>
我们希望观察计算变量,以便在我们的 SwiftUI 视图中展示其结果,因此我们使用 <st c="12409"> <st c="12533">@Published</st> <st c="12543">属性包装器。</st>
<st c="12561">遗憾的是,这是不可能的。</st> <st c="12597">尝试使用以下错误编译结果:</st> <st c="12629">
<st c="12645">属性包装器不能应用于计算属性</st> <st c="12686">
<st c="12703">遵循
<st c="12807">使用 Observable 宏可以很好地解决这个问题:</st> <st c="12849">
@Observable
class MyBook {
var pages: Int = 0
var averageWordsPerPage: Int = 0
var totalWordsInBook: Int {
return pages * averageWordsPerPage
}
}
在前面的代码中,我们只是添加了计算变量,并且我们可以没有问题地在我们的视图中观察它 <st c="13018"> <st c="13050"> <st c="13118">。
<st c="13130">它是如何工作的?</st> <st c="13149">如果一个计算变量没有其值的后端存储,我们如何观察它?</st>
*

*
@Observable
class Book {
var title:String = ""
var pages: Int = 0
var averageWordsPerPage: Int = 0 <st c="14340">var totalWordsInBook: Int {</st>
<st c="14367">return pages * averageWordsPerPage</st>
<st c="14402">}</st> }
struct ContentView: View {
var book:Book = Book()
var body: some View {
VStack {
Text(book.title)
Button("Change") { <st c="14524">book.averageWordsPerPage = 300</st>
<st c="14554">book.pages = 200</st>
<st c="14571">}</st>
<st c="14573">Text("number of pages in the book:</st>
<st c="14608">\(book.totalWordsInBook)")</st> .padding()
}
}
<st c="14687">averageWordsPerPage</st> <st c="14706">和</st>
<st c="14943">@ObservationIgnored</st> <st c="14962">属性添加到这两个属性(</st><st c="15026">pages</st> <st c="15031">)中不会触发</st> <st c="15099">@Observation</st> <st c="15111">框架无法知道有什么东西发生了变化。</st> <st c="15161">好事是我们通过扩展我们的</st>
使用环境变量
<st c="15607">ViewModel</st>
-
应用设置 :用户资料是应用设置的一部分,可以存储在一个 环境变量 中 -
主题和样式 :主颜色色调、字体样式、间距,以及更多 -
用户认证状态 :登录状态是环境变量的一个好例子 。 环境变量
按类型添加环境变量
<st c="16662">Themes</st>
<st c="16675">@Observable</st> class Themes {
var primaryColor: Color = .red
}
<st c="16740">Themes</st> <st c="16828">@Observable</st>
<st c="16932">BookApp</st>
@main
struct BookApp: App { <st c="16976">var themes: Themes = Themes()</st> var body: some Scene {
WindowGroup {
ContentView() <st c="17057">.environment(themes)</st> }
}
}
<st c="17091">BookApp</st>
-
<st c="17264">@State</st>或 <st c="17274">@ObservedObject</st>。 -
<st c="17392">主题</st>对象易于访问。
struct ContentView: View { <st c="17496">@Environment(Themes.self) var themes</st> var book: Book = {
let book = Book()
book.title = "Mastering iOS 17"
return book
}()
var body: some View {
VStack {
Text(book.title)<st c="17665">.foregroundStyle(themes.primaryColor)</st> }
}
}
将主题实例添加到我们的 <st c="17743">ContentView</st> 结构体中很简单。我们使用 <st c="17798">@Environment</st> 属性包装器来注入我们之前创建的主题对象。
我们在主体部分使用主题的主要颜色来为我们的 <st c="17939">书名</st> 着色。
现在,我们必须注意,我们可以在层次结构中的每个视图中使用环境变量,即使我们没有使用环境修改器初始化它。
这里是一个例子:
struct ContentView: View {
var body: some View {
VStack {
MyTitle(text: "Mastering iOS 17")
}
}
}
struct MyTitle: View { <st c="18255">@Environment(Themes.self) var themes</st> let text: String
var body: some View {
Text(text).foregroundStyle(<st c="18358">themes.primaryColor</st>)
}
}
在前面的代码中,我们创建了一个名为 <st c="18452">MyTitle</st> 的另一个 SwiftUI 组件,它具有环境变量 themes。
<st c="18508">MyTitle</st> 视图是 <st c="18536">ContentView</st> 层次结构的一部分。因此,它可以直接访问 <st c="18598">themes</st> 变量。
通过类型传递环境变量很简单!然而,当在大规模工作的时候,它有一些缺点。我相信主要缺点是我们将代码耦合到了一个特定的类型。在 <st c="18822">themes</st> 的例子中,我们处理的是一个显式的类型(<st c="18872">Themes</st>)。
SwiftUI 提供了一种更好的方式来管理环境变量,那就是使用环境键。
通过键添加环境变量
当我们的项目变得更重要时,管理环境变量会更好。
使用环境键提高了我们的视图和实际变量之间的分离。
为了更好地管理环境值,SwiftUI 有两个主要组件:
-
<st c="19281">EnvironmentValues</st>结构体 :这是一个以键值形式结构化的不同环境值的容器。它可以从应用中的任何视图访问。我们可以扩展这个结构体并添加新的变量。 -
<st c="19485">EnvironmentKey</st>协议 :它允许我们为新的变量添加一个键,并使用该键添加新的环境值。
让我们看看它在实践中是如何工作的:
struct ThemesKey: <st c="19660">EnvironmentKey</st> {
static let defaultValue = Themes()
}
extension <st c="19724">EnvironmentValues</st> {
var themes: Themes {
get { self[ThemesKey.self]}
set { self[ThemesKey.self] = newValue}
}
}
<st c="19878">EnvironmentKey</st> <st c="19904">ThemesKey</st><st c="19927">EnvironmentKey</st> <st c="20016">Themes</st>
<st c="20091">EnvironmentValues</st> <st c="20199">themes</st>
<st c="20259">get</st> <st c="20321">ThemesKey</st><st c="20343">set</st>
struct ContentView: View { <st c="20514">@Environment(\.themes) var themes</st> // rest of the view
}
ContentView() <st c="20696">EnvironmentValues</st> struct, we extended the global variables container of our app. That’s the reason why we have access from any view.
<st c="20828">Other than accessing the values from any view, working with environment variable keys has several</st> <st c="20927">additional advantages:</st>
* **<st c="20949">Quickly replacing the variable type in the future</st>**<st c="20999">: Unlike adding an environment value by type, we are not tied to a specific type when adding the variable by key.</st> <st c="21114">We can easily replace the type itself in one place and not have to replace it in all views as long as we keep the</st> <st c="21228">same interface.</st>
* **<st c="21243">Great for testing</st>**<st c="21261">: Another advantage of not being coupled to a specific type is the ability to create mocks and add</st> <st c="21361">unit tests.</st>
* `<st c="21505">get</st>` <st c="21508">and</st> `<st c="21513">set</st>` <st c="21516">functions in the</st> `<st c="21534">EnvironmentValues</st>` <st c="21551">struct?</st> <st c="21560">Now, we can customize them the way we</st> <st c="21598">want to.</st>
<st c="21606">We can understand why environment keys are essential for big projects by looking at the list</st> <st c="21700">of advantages.</st>
<st c="21714">No matter how we work with environment variables, they are crucial for a clean and simple SwiftUI code, especially when we combine them with</st> `<st c="21856">@</st>``<st c="21857">Observable</st>` <st c="21867">objects.</st>
<st c="21876">By now, we already know how to create an observed object and inject it into child views using</st> <st c="21971">environment variables.</st>
<st c="21993">Our next topic revolves</st> <st c="22017">around the compatibility problem that the</st> *<st c="22060">Observation</st>* <st c="22071">framework created for us, specifically</st> <st c="22111">reg</st><st c="22114">arding binding.</st>
<st c="22130">Binding objects using @Bindable</st>
<st c="22162">Let’s start with a short recap of what</st> <st c="22202">binding is.</st>
<st c="22213">In some cases, a view and its</st> <st c="22244">child must share a state and create a two-way connection for reading and modifying a value.</st> <st c="22336">To do that, we use</st> <st c="22354">something</st> <st c="22365">called</st> **<st c="22372">binding</st>**<st c="22379">.</st>
<st c="22380">One classic</st> <st c="22392">example is</st> `<st c="22404">TextField</st>` <st c="22413">– a</st> `<st c="22418">TextField</st>` <st c="22427">view is a SwiftUI component with a</st> `<st c="22463">text</st>` <st c="22467">variable.</st> <st c="22478">Both</st> `<st c="22483">TextField</st>` <st c="22492">and its parent view share the same value of text.</st> <st c="22543">Therefore, it’s a</st> <st c="22561">binding</st> <st c="22569">variable:</st>
struct ContentView: View {
VStack {
TextField("电子邮件", text: <st c="22692">$email</st>)
}
}
}
<st c="22706">We see that the</st> `<st c="22723">email</st>` <st c="22728">variable is marked as a state, but the</st> `<st c="22768">TextField</st>` <st c="22777">view is the one that updates it.</st> <st c="22811">The binding occurs using the</st> `<st c="22840">$</st>` <st c="22841">character.</st>
<st c="22851">We can create a binding variable ourselves using the</st> `<st c="22905">@Binding</st>` <st c="22913">proper</st><st c="22920">ty wrapper:</st>
struct MyCounter: View {
VStack {
Button("增加") {
value += 1
}
}
}
}
struct ContentView: View {
VStack {
MyCounter(value: <st c="23154">$count</st>)
Text("值 = \(count)")
}
}
}
<st c="23193">The</st> `<st c="23198">count</st>` <st c="23203">variable in the parent</st> <st c="23227">view (</st>`<st c="23233">ContentView</st>`<st c="23245">) and the</st> `<st c="23256">value</st>` <st c="23261">variable in the child view (</st>`<st c="23290">ContentView</st>`<st c="23302">) share</st> <st c="23311">the same state, and now we have a two-way connection</st> <st c="23364">between them.</st>
<st c="23377">We can connect a binding variable to a</st> `<st c="23417">@State</st>` <st c="23423">property wrapper (such as in the example we just saw) or a</st> `<st c="23483">@</st>``<st c="23484">ObservedObject</st>` <st c="23498">variable.</st>
<st c="23508">Can you guess what the problem is</st> <st c="23543">with trying to create a binding connection using the</st> `<st c="23596">Observation</st>` <st c="23607">framework?</st>
<st c="23618">So, apparently, classes</st> <st c="23642">that are marked with the</st> `<st c="23668">@Observed</st>` <st c="23677">macro are not eligible for</st> `<st c="23705">@State</st>` <st c="23711">or</st> `<st c="23715">@ObservedObject</st>`<st c="23730">, so we can’t use</st> `<st c="23748">@Binding</st>` <st c="23756">with them.</st>
<st c="23767">Fortunately, with the</st> *<st c="23790">Observation</st>* <st c="23801">framework, we have a new property wrapper</st> <st c="23844">called</st> **<st c="23851">@Bindable</st>**<st c="23860">.</st>
<st c="23861">Let’s see a short</st> <st c="23880">example of how to use</st> `<st c="23902">@Bindable</st>` <st c="23911">with a</st> <st c="23919">counter object:</st>
struct ContentView: View {
VStack {
CounterView(counter: <st c="24038">counter</st>)
Text("值 = \(counter.value)")
}
}
}
struct CounterView: View {
VStack {
Button("增加") { <st c="24197">counter.increment()</st> }
}
}
}
<st c="24224">The code example has two views as before – a</st> `<st c="24270">ContentView</st>` <st c="24281">view and a child view named</st> `<st c="24310">CounterView</st>`<st c="24321">. The</st> `<st c="24327">ContentView</st>` <st c="24338">view has a variable called</st> `<st c="24366">counter</st>` <st c="24373">of the</st> `<st c="24381">Counter</st>` <st c="24388">type.</st> <st c="24395">The</st> `<st c="24399">Counter</st>` <st c="24406">class is marked</st> <st c="24422">with</st> `<st c="24428">@Observed</st>`<st c="24437">, so we don’t need to mark the property as</st> `<st c="24480">@State</st>` <st c="24486">or</st> `<st c="24490">@ObservedObject</st>`<st c="24505">.</st>
<st c="24506">In the</st> `<st c="24514">CounterView</st>` <st c="24525">structure, we</st> <st c="24540">also have a counter from the same type, but it is marked with</st> `<st c="24602">@Bindable</st>`<st c="24611">. This means we need to bind it to an object with a</st> <st c="24663">similar type.</st>
<st c="24676">The</st> `<st c="24681">CounterView.counter</st>` <st c="24700">and</st> `<st c="24705">ContentView.counter</st>` <st c="24724">variables are linked – whenever we change the value in the child view, it automatically reflects in the parent view.</st> <st c="24842">Notice that with</st> `<st c="24859">@Bindable,</st>` <st c="24869">we don’t need to add any</st> `<st c="24895">$</st>` <st c="24896">signs to the variable expression.</st> <st c="24931">Everything</st> <st c="24942">just works.</st>
<st c="24953">Binding is a critical usage of SwiftUI – it stands at the heart of many input views such as text fields, toggles, sheets,</st> <st c="25076">and more.</st>
<st c="25085">Working with the</st> `<st c="25103">@Bindable</st>` <st c="25112">macro can be confusing – we now have both</st> `<st c="25155">@Binding</st>` <st c="25163">and</st> `<st c="25168">@Bindable</st>` <st c="25177">at the same time!</st> `<st c="25196">@Binding</st>` <st c="25204">is used for states and observable objects and</st> `<st c="25251">@Bindable</st>` <st c="25260">is used for...</st> <st c="25276">observed objects?</st>
<st c="25293">So yes, it feels like we are in a transition era.</st> <st c="25344">The good news is that we can solve the issue easily by migrating our project</st> <st c="25421">to</st> *<st c="25424">Observable</st>*<st c="25434">.</st>
<st c="25435">Migrating to Observable</st>
<st c="25459">Before migrating to</st> *<st c="25480">Observable</st>*<st c="25490">, we must ensure that our app deployment target is at least 17\.</st> <st c="25554">Remember that this</st> <st c="25573">feature (and most of the new features described in this book) are from iOS 17, and some are irrelevant if our app deployment target is</st> <st c="25708">not 17.</st>
<st c="25715">Let’s try to recap the different</st> <st c="25749">Observable attributes:</st>
* `<st c="25771">@State</st>`<st c="25778">: This is used to manage the state within a specific view.</st> <st c="25838">A change to a</st> `<st c="25852">@State</st>` <st c="25858">property triggers a view update.</st> <st c="25892">For example, data related to a list or view visibility can be marked</st> <st c="25961">as</st> `<st c="25964">@State</st>`<st c="25970">.</st>
* `<st c="25971">@Observable</st>`<st c="25983">: This can</st> <st c="25995">be applied to a class to make the class observable.</st> <st c="26047">Each class property is automatically marked with</st> `<st c="26096">@Published</st>` <st c="26106">unless we mark them as</st> `<st c="26130">@ObservataionIgnored</st>`<st c="26150">.</st> `<st c="26152">@Observable</st>` <st c="26163">can be added to view models or business</st> <st c="26204">logic classes.</st>
* `<st c="26218">@Bindable</st>`<st c="26228">: This creates a two-way connection between a property and another value.</st> <st c="26303">Text field input, toggles, or a counter are examples of views for implementing a</st> `<st c="26384">@</st>``<st c="26385">Bindable</st>` <st c="26393">connection.</st>
* `<st c="26405">@Environment</st>`<st c="26418">: Mark an object to be shared down the view hierarchy with this attribute.</st> <st c="26494">For example, configuration or a theme can be shared with all views in the hierarchy using the</st> `<st c="26588">@</st>``<st c="26589">Environemnt</st>` <st c="26600">attribute.</st>
<st c="26611">This list aims to summarize the different attributes in the Observable framework and their</st> <st c="26703">use cases.</st>
<st c="26713">Once we decide to move to the</st> *<st c="26744">Observable</st>* <st c="26754">framework, there are a few things we need</st> <st c="26797">to do:</st>
* <st c="26803">Remove the pro</st><st c="26818">tocol conformation to</st> `<st c="26841">ObservableObject</st>` <st c="26857">and add the</st> `<st c="26870">@Observable</st>` <st c="26881">macro for all the</st> <st c="26900">relevant classes</st>
* <st c="26916">Remove the</st> `<st c="26928">@Published</st>` <st c="26938">property wrapper and add</st> `<st c="26964">@ObservationIgnored</st>` <st c="26983">for the properties we don’t want</st> <st c="27017">to observe</st>
* <st c="27027">Remove the</st> `<st c="27039">@ObservedObject</st>` <st c="27054">property wrapper</st>
* <st c="27071">Rename</st> `<st c="27079">@Binding</st>` <st c="27087">to</st> `<st c="27091">@Bindable</st>` <st c="27100">for the properties that are based</st> <st c="27135">on classes</st>
<st c="27145">Once we finish migrating to the</st> `<st c="27178">Observable</st>` <st c="27188">framework, things will be clearer and more straightforward, with fewer property wrappers and less protocol conformation.</st> <st c="27310">The binding can also be simple – now it’s</st> `<st c="27352">@Binding</st>` <st c="27360">for primitive values and</st> `<st c="27386">@Bindable</st>` <st c="27395">for classes.</st> <st c="27409">That’s not perfect, but not too bad either.</st> <st c="27453">It’s time to</st> <st c="27466">enjoy</st> *<st c="27472">Observable</st>*<st c="27482">!</st>
<st c="27483">Summary</st>
<st c="27490">This was another chapter that made use of Swift macros and other advanced Swift techniques.</st> <st c="27583">A small note: to understand topics such as</st> *<st c="27626">Observable</st>*<st c="27636">, I recommend having good knowledge of Swift.</st> <st c="27682">Otherwise, it becomes just another boring tutorial.</st> <st c="27734">Knowing how things work on the inside is fascinating and can only make</st> <st c="27805">us better.</st>
<st c="27815">In this chapter, we did a recap of the SwiftUI observation system, and we discussed its problem.</st> <st c="27913">We added the</st> `<st c="27926">@Observable</st>` <st c="27937">macro and explored how it works.</st> <st c="27971">We talked about computed variables, environment variables, and bindable.</st> <st c="28044">Ultimately, we discussed migrating from the “old” observation system to the new</st> *<st c="28124">Observable</st>* <st c="28134">framework.</st>
<st c="28145">Remember – observation is a core feature of SwiftUI and is crucial to delivering a superior experience to</st> <st c="28252">our users.</st>
<st c="28262">In the next chapter, we will learn about another critical feature, especially in mobile – navigation</st> <st c="28364">and search.</st>
第五章:4
使用 SwiftUI 进行高级导航
-
理解为什么 SwiftUI 导航是 一个挑战 -
探索 SwiftUI 的 <st c="873">NavigationStack</st> -
使用不同的数据模型来 触发导航 -
使用协调者模式来更好地管理我们的 关注点 -
实现 SwiftUI 的 <st c="1036">NavigationSplitView</st>以创建一个 基于列的导航
技术要求
理解为什么 SwiftUI 导航是一个挑战
为了回答那个问题,我们需要理解导航是如何直观工作的。用户点击按钮、链接或其他可能发生的事件。然后,应用响应该事件并将视图过渡到另一个屏幕。
从某种意义上说,我们理解这听起来像是一个事件驱动范式。当我们讨论 SwiftUI 和 UIKit 之间的区别时,我们实际上是在讨论声明式编程和<st c="2255">imperative programming</st>之间的区别。
命令式 UI,如 UIKit,也是事件驱动的,而声明式 UI,如 SwiftUI,则表示当前状态。因此,我们可以理解为什么在 UIKit 中导航看起来更简单,并且可能感觉更自然。
许多开发者都在 SwiftUI 导航上挣扎。他们在一个<st c="2593">UIHostingController</st>中包裹一个 SwiftUI 视图,并使用 UIKit 导航系统。这是一个合理的解决方案,用于实现一些在 SwiftUI 中难以完成的复杂导航技术。然而,我们需要记住,SwiftUI 已经发展多年,提供了优秀的导航工具。
让我们从基本的导航工具<st c="2904">NavigationStack</st>开始。
探索<st c="2904">NavigationStack</st>
当 SwiftUI 被引入时,基本的导航机制是基于一个名为<st c="3034">NavigationView</st>的视图。然而,<st c="3059">NavigationView</st>对于大多数应用来说过于简单,因此<st c="3108">NavigationStack</st>取代了它。实际上,苹果从 iOS 18 开始弃用了<st c="3163">NavigationView</st>。
与<st c="3213">NavigationView</st>相比,<st c="3229">NavigationStack</st>给这个堆栈增加了一点点复杂性,这为我们提供了新的功能。
让我们看看<st c="3364">NavigationStack</st>的一个简单用法示例:
struct ContentView: View {
var body: some View { <st c="3436">NavigationStack {</st> NavigationLink("Tap here to go to the next
screen") {
Text("Next Screen!")
}
}
}
}
这个代码示例看起来非常简单!
然而,<st c="3585">NavigationStack</st>比它看起来要强大得多。
怎么样?<st c="3658">NavigationStack</st>的概念是由四个组件构成的:
-
<st c="3786">NavigationView</st>。在 <st c="3805">NavigationStack</st>中, <st c="3822">NavigationLink</st>描述了发生了什么,而 <st c="3870">navigationDestination</st>视图修饰符描述了我们去哪里。 -
数据与目的地之间的链接 :在某种程度上,这是前面点的进一步发展。 目的地链接到一个数据类型。 这意味着我们可以有多个导航链接指向同一个目的地,仅仅因为它们共享相同 的数据类型。 -
允许我们读取和更新路径 :这里,我们对我们想法的另一个发展。 因为数据和屏幕现在是链接的,我们可以将路径表示为数据实例的数组。 修改路径数组也会改变我们的 视图栈。 -
<st c="4509">NavigationLink</st>也具有这种能力,但 <st c="4574">NavigationStack</st>的引入使其变得过时。
使用 navigationDestination 视图修饰符分离导航目标
<st c="5232">NavigationLink</st>
<st c="5392">NavigationStack</st><st c="5444">navigationDestination</st>
<st c="5571">navigationDestination</st>
struct ContentView: View {
@State var isNextScreenDisplayed: Bool = false
var body: some View {
NavigationStack {
Button("Go to next screen") {
isNextScreenDisplayed = true
}
.<st c="5799">navigationDestination(isPresented:</st>
<st c="5834">$isNextScreenDisplayed) {</st>
<st c="5860">Text("Next Screen!")</st> }
}
}
}
<st c="5924">NavigationStack</st> <st c="5989">NavigationLink</st> <st c="6096">@State</st> <st c="6118">isNextScreenDisplayed</st> <st c="6160">NavigationLink</st>
<st c="6224">navigationDestination</st><st c="6251">navigationDestination</st> <st c="6340">isNextScreenDisplayed</st> <st c="6378">它还有一个包含我们的下一个屏幕(类似于
<st c="6495">isNextScreenDisplayed</st>
<st c="6595">NavigationLink</st>
<st c="7300">NavigationView</st>
<st c="7581">NavigationStack</st>
使用数据模型触发导航
对于习惯于使用 UIKit 导航的开发者来说,使用数据模型的想法可能很奇怪。
我们理解到许多屏幕都与特定的数据模型相关。
如果我们再深入思考,我们可以使用数据模型在我们的应用中表示许多屏幕。
在我们开始思考探索潜在的可能性和实现方式之前,让我们看看基于基本数据导航是什么样子:
struct ContentView: View { <st c="8603">private let countries = ["England", "France", "Spain",</st>
<st c="8657">"Italy"]</st> var body: some View {
NavigationStack {
List(countries, id: \.self) { country in <st c="8748">NavigationLink(country, value: country)</st> } <st c="8790">.navigationDestination(for: String.self)</st> { item
in
Text(item)
}
}
}
}
和往常一样,我已经在前面代码中突出了有趣的部分。
每一行都有一个 <st c="9039">NavigationLink</st>
我们只有在查看导航目的地时才能理解将国家作为值发送的含义。
换句话说,点击一个国家会通过 <st c="9746">NavigationLink</st>
我们可以通过定义多个导航目的地来使用数据模型导航到不同的地方,每个目的地对应不同的数据模型类型。
这里是使用导航目的地向个人资料屏幕添加导航的另一个例子:
struct Profile<st c="10109">: Hashable</st> {
let firstName: String
let lastName: String
let email: String
}
struct ContentView: View {
let profile = Profile(firstName: "Avi", lastName:
"Tsadok", email: "myemail@domain.com")
let countries = ["England", "France", "Spain", "Italy"]
var body: some View {
NavigationStack {
List(countries, id: \.self) { country in <st c="10439">NavigationLink(country, value: country)</st> }.toolbar(content: { <st c="10500">NavigationLink("Go to profile", value:</st>
<st c="10538">profile)</st> }) <st c="10551">.navigationDestination(for: String.self)</st> { item
in
Text(item)
} <st c="10615">.navigationDestination(for: Profile.self)</st> {
profile in
VStack {
Text(profile.firstName)
Text(profile.lastName)
Text(profile.email)
}
}
}
}
}
在前面的代码中,我们看到一个来自<st c="10851">Profile</st> <st c="10858">类型的数据模型的另一个导航目的地。要导航到配置文件屏幕,我们在屏幕工具栏中添加了另一个<st c="10909">NavigationLink</st> <st c="10923">视图并发送了配置文件<st c="10972">数据模型</st>。
我们的导航系统是动态的,因为我们可以使用不同的数据模型。但这并没有停止。<st c="11093">NavigationStack</st> <st c="11108">可以</st> <st c="11112">揭示并甚至修改当前视图的堆栈。</st>我们使用<st c="11167">path</st> <st c="11193">绑定变量</st>来做到这一点。
《st c="11210">响应路径变量</st>
将<st c="11242">目的地</st> <st c="11258">与其导航链接</st>分离是很好的,但<st c="11309">NavigationStack</st> <st c="11324">’s观察和更新其视图堆栈的能力非常强大。
如前所述,一个<st c="11394">NavigationStack</st> <st c="11411">视图有一个名为<st c="11462">path</st>的绑定变量,并且<st c="11476">path</st>变量可以包含通过其<st c="11529">数据模型</st>的视图列表。
使用链表很容易证明这一点:
struct ContentView: View {
let list: LinkedList<Int> = {
let list = LinkedList<Int>()
list.head = ListNode(1)
list.head?.next = ListNode(2)
list.head?.next?.next = ListNode(3)
return list
}() <st c="11786">@State var path: [ListNode<Int>]</st> = []
var body: some View { <st c="11846">NavigationStack(path: $path)</st> {
VStack {
NavigationLink("Start", value: list.head)
}
.navigationDestination(for: ListNode<Int>.self)
{ node in
NavigationLink("\(node.value)", value:
node.next)
}
}
}
}
我选择使用链表来演示<st c="12082">path</st> <st c="12086">使用,因为它是一个很好的数据结构,类似于导航堆栈(来自相同类型的链接项)。
如果我们在导航期间观察<st c="12232">path</st> <st c="12236">变量,我们可以看到它包含当前作为视图活动的列表节点集合。
<st c="12345">path</st> <st c="12383">变量</st>绑定到<st c="12413">NavigationStack</st> <st c="12428">的事实非常棒,因为我们能够操作和
path.append(ListNode(4))
向<st c="12524">path</st> <st c="12528">添加新的列表节点会触发导航并将用户引导到一个新的
我们也可以使用<st c="12634">path</st> <st c="12638">变量</st>来创建整个堆栈:
path = [ListNode(1), ListNode(2)]
设置新的节点数组会创建相应视图的新堆栈。这是一个实现深度链接或将用户引导到应用内的特定位置的好方法。
你现在可能正在挠头,在想我们如何在应用内部实现它?在哪些用例中,我们需要使用相同的数据模型类型在层次结构中导航几个级别?
<st c="13105">Task</st><st c="13111">Album</st><st c="13121">Contact</st>
enum Screen: Hashable {
case signin
case onboarding
case mainScreen
case settings
}
@State var path: [Screen] = []
<st c="13475">Screen</st>
path = [.mainScreen, .settings]
<st c="13963">NavigationPath</st>
使用 NavigationPath 处理不同类型的数据
<st c="14036">NavigationPath</st> <st c="14078">NavigationStack</st><st c="14167">NavigationPath</st>
<st c="14429">歌曲</st> <st c="14481">专辑</st> <st c="14601">NavigationPath</st><st c="14661">path</st>
struct ContentView: View { <st c="14785">@State private var navigationPath = NavigationPath()</st> @State private var albums: [Album] = [Album(title:
"Album 1"), Album(title: "Album 2")]
@State private var songs: [Song] = [Song(title: "Song
1"), Song(title: "Song 2")]
var body: some View { <st c="15030">NavigationStack(path: $navigationPath) {</st> VStack {
List {
Section(header: Text("Songs")) {
ForEach(songs) { song in
Button(action: { <st c="15162">navigationPath.append(song)</st> }) {
Text(song.title)
}
}
}
Section(header: Text("Albums")) {
ForEach(albums) { album in
Button(action: { <st c="15296">navigationPath.append(album)</st> }) {
Text(album.title)
}
}
}
} <st c="15356">.navigationDestination(for: Song.self) {</st>
<st c="15396">song in</st>
<st c="15404">SongDetailView(song: song,</st>
<st c="15431">navigationPath: $navigationPath)</st>
<st c="15464">}</st>
<st c="15466">.navigationDestination(for: Album.self) {</st>
<st c="15507">album in</st>
<st c="15516">AlbumDetailView(album: album)</st>
<st c="15546">}</st> }
我们从声明一个状态变量开始,该变量保存我们的
@State private var navigationPath = NavigationPath()
如前所述,与之前我们使用的<st c="15918">path</st> <st c="15922">变量不同,在<st c="15948">NavigationPath</st> <st c="15962">的情况下,我们不需要定义其类型。<st c="16058">Hashable</st> <st c="16066">,它就可以保存我们想要的任何类型。
接下来,我们将使用我们的新<st c="16091">NavigationStack</st>
NavigationStack(path: $navigationPath) {
注意,我们使用了一个类似的签名,但是类型不同——使用<st c="16326">Binding<Data></st>
现在我们有了
navigationPath.append(song)
或者,你可以这样做:
navigationPath.append(album)
添加操作会触发
.navigationDestination(for: Song.self) { song in
SongDetailView(song: song, navigationPath:
$navigationPath)
}
.navigationDestination(for: Album.self) { album in
AlbumDetailView(album: album)
}
在这个例子中,我们为每种类型我们传递的
我们可以添加任何我们想要的实体的事实使得
我们也可以使用
Button("Back") {
navigationPath.removeLast()
}
在这个例子中,我们添加了一个返回按钮,当点击时移除导航路径的最后几个组件
因为我们仍然处于一个声明性世界中,我们对导航栈所做的任何更改,无论是通过添加或移除组件,都会反映在我们的 UI 中
与协调器模式一起工作
<st c="17545">NavigationPath</st> <st c="17564">NavigationStack</st>
理解协调器的原理
-
协调器是一个组件,它保存当前的导航路径和一般上下文。 它知道显示的是哪个屏幕以及一般的当前流程。 协调器还会将新的视图添加到堆栈中,弹出,并显示模态或 表单视图。 -
视图不知道用户应该导航到的下一个视图。 它所知道的是用户执行的操作。 从某种意义上说,视图与导航逻辑是隔离的,并且不了解 一般上下文。 -
协调器代表一个流程。 在我们的应用中,我们可以有多个流程,每个流程有多个协调器。

<st c="19669">NavigationPath</st>
构建协调器对象
class Coordinator: ObservableObject {
@Published var path = NavigationPath()
}
<st c="20819">导航路径</st> <st c="20846">导航路径</st> <st c="21043">可观察对象</st> <st c="21157">导航堆栈</st>
enum PageAction: Hashable {
case gotoAlbumView(album: Album)
case gotoSettingsView
}
enum UserAction {
case albumTappedInAlbumsList(album: Album)
case settingButtonTapped
}
-
<st c="21453">页面操作</st>:这个枚举描述了协调器需要执行的一些导航操作,例如导航到一个 <st c="21566">专辑</st>视图或一个 设置视图。 -
<st c="21596">用户操作</st>:这个枚举描述了用户执行的操作,例如在专辑列表中点击专辑或在 设置按钮上点击。
<st c="21812">专辑</st>
func performedAction(action: UserAction) {
switch action {
case .albumTappedInAlbumsList(let album):
path.append(PageAction.gotoAlbumView(album:
album))
case .settingButtonTapped:
path.append(PageAction.gotoSettingsView)
}
}
@ViewBuilder
func buildView(forPageAction pageAction: PageAction) ->
some View {
switch pageAction {
case .gotoAlbumView(let album):
AlbumDetailView(album: album)
case .gotoSettingsView:
SettingsView()
}
}
<st c="22342">performAction()</st> <st c="22391">UserAction</st> <st c="22462">NavigationPath</st>
<st c="22686">专辑</st> <st c="22710">专辑</st>
<st c="23098">CoordinatorView</st>
添加 CoordinatorView
<st c="23330">CoordinatorView</st>
<st c="23412">CoordinatorView</st>
struct CoordinatorView: View {
@ObservedObject private var coordinator = Coordinator()
var body: some View { <st c="23549">NavigationStack</st>(path: $coordinator.path) {
AlbumListView()
.navigationDestination(for:
PageAction.self, destination: { pageAction in
coordinator.buildView(forPageAction:
pageAction)
})
}
.environmentObject(coordinator)
}
}
<st c="23772">CoordinatorView</st>
-
<st c="23840">coordinator</st>: 在 <st c="23862">CoordinatorView</st>中,我们添加了我们刚刚构建的 <st c="23907">Coordinator</st>类的一个实例。 我们将该协调器制作为一个可观察对象,这样我们就可以使用其路径来添加和从 <st c="24047">堆栈</st>中移除视图。 -
<st c="24057">NavigationStack</st>: 这与我们在本章中遇到的 <st c="24093">NavigationStack</st>相同。 如前所述,我们使用协调器路径作为 <st c="24178">NavigationStack</st>,但更重要的是,还有两个附加事项——我们使用根视图( <st c="24285">AlbumListView</st>)初始化堆栈,并使用协调器 <st c="24330">buildView</st>函数将页面操作映射到视图,以将相应的视图添加到 堆栈。 -
<st c="24426">环境对象</st>:我们在协调器中添加了一个 <st c="24457">environmentObject</st>视图修饰符来声明一个环境对象。 我们这样做是为了让所有在 <st c="24584">NavigationStack</st>下的视图都能访问协调器,以便它们可以调用不同的 用户操作。
<st c="24799">AlbumListView</st>
直接从视图调用协调器
<st c="25081">AlbumListView</st>
struct AlbumListView: View { <st c="25139">@EnvironmentObject private var coordinator: Coordinator</st> var body: some View {
List(albums) { album in
VStack(alignment: .leading) {
Text(album.title)
.font(.headline)
Text(album.artist)
.font(.subheadline)
}
.onTapGesture { <st c="25363">coordinator.performedAction(action:</st>
<st c="25398">.albumTappedInAlbumsList(album: album))</st> }
}
.navigationTitle("Albums")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing)
{
Button(action: { <st c="25546">coordinator.performedAction(action:</st>
<st c="25581">.settingButtonTapped)</st> }) {
Image(systemName: "gear")
}
}
}
}
}
<st c="25648">AlbumListView</st>
<st c="25802">performedAction()</st>
<st c="25950">performedAction()</st>
<st c="26293">NavigationStack</st> <st c="26532">NavigationSplitView</st>
使用 NavigationSplitView 进行列导航
<st c="27190">NavigationSplitView</st>
创建 NavigationSplitView
<st c="27395">NavigationSplitView</st>
-
<st c="27551">侧边栏</st>:从左侧数的第一列。 这是我们的导航开始的主要列。 -
<st c="27647">内容</st>:当有三列时, <st c="27692">内容</st>列显示与 <st c="27754">侧边栏</st>列中选定的项目相关的数据。 -
<st c="27769">详情</st>: <st c="27783">详情</st>列展示 <st c="27831">内容</st>列或 <st c="27853">侧边栏</st>列中选定的项目。 一般来说,它是分割视图层次结构中最后的项。
<st c="28093">NavigationSplitView</st>
var body: some View { <st c="28229">NavigationSplitView</st> {
List(albums, selection: $selectedAlbum) { album
in
NavigationLink(album.title, value: album)
}
} <st c="28348">detail: {</st> if let selectedAlbum = selectedAlbum {
List(selectedAlbum.songs, selection:
$selectedSong) { song in
Text(song.title)
}
.navigationTitle(selectedAlbum.title)
} else {
Text("Select an album")
}
}
}
<st c="28570">NavigationSplitView</st> <st c="28721">selectedAlbum</st> <st c="28761">selected album</st>



<st c="29674">NavigationSplitView</st> <st c="29754">NavigationSplitView</st> <st c="29825">NavigationStack</st> <st c="29872">UIKit 的</st>
移动到三列
-
第一级:第一级项目的列表 -
第二级:基于第一级选择,第二级项目的列表 -
第三级:所选第二级项目的详细信息
<st c="30649">Detail</st> <st c="30784">Detail</st> <st c="30806">Sidebar</st> <st c="30835">Content</st>
<st c="30867">Content</st>
var body: some View { <st c="30922">NavigationSplitView {</st> List(albums, selection: $selectedAlbum) { album
in
NavigationLink(album.title, value: album)
} <st c="31039">} content: {</st> if let selectedAlbum = selectedAlbum {
List(selectedAlbum.songs, selection:
$selectedSong) { song in
NavigationLink(song.title, value: song)
}
.navigationTitle(selectedAlbum.title)
} else {
Text("Select an album")
} <st c="31268">} detail: {</st> if let selectedSong = selectedSong {
VStack {
Text("Song Title:
\(selectedSong.title)")
Text("Artist: \(selectedSong.artist)")
}
.padding()
.navigationTitle(selectedSong.title)
} else {
Text("Select a song")
}
}
}
<st c="31561">Content</st> <st c="31603">Detail</st> <st c="31653">selectedSong</st>

<st c="31938">NavigationSplitView</st>
总结
<st c="32414">NavigationStack</st><st c="32515">NavigationSplitView</st>
第六章:5
使用 WidgetKit 增强 iOS 应用程序
-
小部件的概念 是 -
了解小部件是如何 工作的 -
添加我们的第一个小部件并构建条目 时间线 -
添加一个 用户可配置的小部件 -
确保我们的小部件是最新的 的 -
自定义小部件动画 -
添加用户交互,例如按钮 和开关 -
将控制小部件添加到控制中心和 锁屏
技术要求
小部件的概念
-
一目了然的信息 **——小部件为我们应用的用户提供最新和重要的信息。 这可能包括配送状态、股票价值、事件日历或任何其他在日常生活中有用的信息。 -
通往我们应用的捷径 **——点击小部件可以打开我们的应用,在许多情况下,还可以打开我们应用的特定屏幕。 在 watchOS 中,使用小部件打开我们的应用尤为重要,因为与 iOS 不同,watchOS 的启动界面不是用户的默认视图。 对于许多应用开发者来说,这是一种很好的推广他们应用的方式,并在主屏幕上* 争夺 用户的注意力。 -
执行基本操作 **——从 iOS 17 开始,苹果公司增加了交互式小部件,允许用户在不打开应用的情况下执行基本操作,例如完成任务、打开车库门或接受支付请求。 在 iOS 18 中,这一功能更进一步,我们甚至可以将小部件添加到控制中心,或者使用 iPhone 15 设备上的动作按钮打开它们。
理解小部件的工作原理
添加小部件

<st c="6268">MyWidget</st>
-
<st c="6279">MyWidgetBundle</st>– 小部件包是我们扩展所包含的不同小部件的容器。 目前,我们只有一个小部件,但可以添加更多。 -
<st c="6444">MyWidget</st>– 包含小部件代码本身,包括其 UI 和配置。 -
<st c="6524">Assets</st>– 一个专门为小部件扩展设计的资产目录。 -
<st c="6589">Info.plist</st>– 就像任何目标一样,小部件扩展包含一个 <st c="6657">plist</st>文件,其中包含有关小部件扩展的通用信息。

配置我们的小部件
<st c="7706">StaticConfiguration</st><st c="7727">StaticConfiguration</st>
<st c="7840">StaticConfiguration</st>
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration { <st c="7999">StaticConfiguration(kind: kind, provider:</st>
<st c="8040">Provider()) { entry in</st>
<st c="8063">MyWidgetEntryView(entry: entry)</st>
<st c="8095">.containerBackground(.fill.tertiary, for:</st>
<st c="8136">.widget)</st>
<st c="8144">}</st>
<st c="8146">.configurationDisplayName("My Widget")</st> .description("This is an example widget.")
}
}
<st c="8247">StaticConfiguration</st>
-
<st c="8357">kind</st>– 这是小部件配置的唯一标识符。 它帮助我们使用 WidgetCenter 向特定小部件配置发送请求。 -
<st c="8501">configurationDisplayName</st>– 这是小部件显示名称,当用户想要选择要添加的正确小部件时,它将显示给用户。 to add. -
<st c="8634">description</st>– 这是小部件的描述,它显示在用户旁边,紧邻其 显示名称。
除了这三个参数之外,我们还有其他一些重要的参数。
.supportedFamilies([.systemMedium])
<st c="9009">backgroundTask</st>
<st c="9126">WidgetConfiguration</st> <st c="9278">StaticConfiguration</st>
-
<st c="9385">StaticConfiguration</st>– 如前所述,此配置允许我们创建一个不可配置的小部件 -
<st c="9499">AppIntentConfiguration</st>– 这使用户能够自定义他们的小部件,例如,为天气小部件选择一个城市,或为 提醒应用 -
<ActivityConfiguration>– 这项配置显示了实时活动小部件的实时数据
一个小部件只能包含一个配置。如果我们需要多个配置,那么这是一个很好的迹象,表明我们需要创建具有不同配置的多个小部件,并在它们之间共享一些代码。
所有这些小部件配置听起来都很吸引人!让我们从探索StaticConfiguration开始。
工作与静态配置
一个静态小部件是一个没有用户可配置选项的小部件。例如,一个显示特定城市当前时间的小部件不能是静态的,因为用户需要指定一个城市或位置来配置小部件。
然而,一个静态小部件的好例子是一个显示整个月视图并标记当前日期的日历小部件,或者是一个显示最近播放的歌曲的音乐应用小部件。
尽管日历和音乐应用小部件显示的信息不是由用户更新的,但它们需要不时地更新自己。
如果我们回顾静态配置示例(在《配置我们的小部件》部分),我们可以看到一个名为provider的参数,它包含一个名为entry的视图构建器闭包参数。
使用provider和entry,我们可以以高效的方式在时间上为我们的小部件提供数据。
小部件的一个关键方面是提供时间上的数据,我们使用时间线提供者来实现这一点。现在,让我们了解时间线提供者是什么意思。
理解小部件的时间线提供者
有一个原因,为什么苹果公司花了近 14 年时间才在 iOS 主屏幕上支持小部件。主要原因是性能,包括电力和内存性能。虽然今天的设备功能强大,但在 Springboard 上拥有大量活动小部件会消耗大量的电力。因此,我们需要找到更有效的方法来高效地加载我们的小部件。
struct EventEntry: TimelineEntry {
let date: Date
let nextEvent: String
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> EventEntry {
EventEntry(date: Date(), nextEvent: "Loading")
}
func getSnapshot(in context: Context, completion:
@escaping (EventEntry) -> Void) {
let entry = EventEntry(date: Date(), nextEvent: "Go
to the book store")
completion(entry)
}
func getTimeline(in context: Context, completion:
@escaping (Timeline<EventEntry>) -> Void) {
let entries: [EventEntry] = getListOfEnties()
let timeline = Timeline(entries: entries, policy:
.atEnd)
completion(timeline)
}
func getListOfEnties()->[EventEntry] {
…
}
}
<st c="13321">EventEntry</st> <st c="13336">Provider</st>
<st c="13345">EventEntry</st> <st c="13386">TimeLineEntry</st> <st c="13414">TimeLineEntry</st>
var date: Date { get }
<st c="13568">date</st> <st c="13664">date</st>之外,我们还添加了一个表示条目下一个事件标题的变量,名为<st c="13756">。
<st c="13769">Provider</st>。 <st c="13801">结构体符合<st c="13837">。<st c="13855">Provider</st> <st c="13904">WidgetCenter</st> <st c="13970">Provider</st>
生成时间线
<st c="14347">Provider</st> <st c="14466">Provider</st>
<st c="14532">getTimeLine</st><st c="14585">getTimeline</st>
func getTimeline(in context: Context, completion: @escaping
(Timeline<EventEntry>) -> Void) {
let entries: [EventEntry] = getListOfEntries()
let timeline = Timeline(entries: entries, policy:
.atEnd)
completion(timeline)
}
<st c="14838">getTimeline()</st> <st c="14906">Timeline</st> <st c="14948">completion</st> <st c="15023">Context</st>
-
<st c="15073">上下文</st>– <st c="15087">Context</st>参数包含有关小部件环境的信息,例如小部件系列(它是一个小部件吗? 也许是一个中等大小的?),或者实际的小部件大小。 如果小部件 UI 在大尺寸时显示更多信息,我们可能希望将更多数据加载到我们的时间线条目中。 但这里最重要的信息可能是 <st c="15424">isPreview</st>属性,它指示小部件是否出现在小部件库中。 一般来说,在组件库中展示我们的组件的真实用户数据是最佳实践,但这并不仅限于安全或网络问题。 因此,我们可以通过检查 <st c="15752">isPreview</st>属性来为组件库提供模拟数据。 -
<st c="15771">策略</st><st c="15778">– 我们提供给小部件的时间线有一个最终条目数。</st><st c="15850">那么,当它们完成,时间线达到其尽头时会发生什么?</st><st c="15921">这正是</st><st c="15940">策略</st><st c="15952">参数在描述时间线重新加载行为时的作用。</st><st c="16017">有几个选项 –</st><st c="16045">atEnd</st><st c="16050">(</st><st c="16052">WidgetKit</st><st c="16062">请求一个新的时间线),</st><st c="16088">never</st><st c="16093">(</st><st c="16095">WidgetKit</st><st c="16105">不请求一个新的时间线),以及</st><st c="16142">after(date:Date)</st><st c="16158">(</st><st c="16160">WidgetKit</st><st c="16170">在特定日期生成一个新的时间线)。</st><st c="16216">策略有助于</st><st c="16237">WidgetCenter</st>`更好地优化时间线重新加载机制。
<st c="16302">在我们继续之前,关于时间线重新加载优化的几点说明。</st> <st c="16374">我们希望尽可能长地构建我们的时间线并不意味着我们的小部件需要不断重新加载。</st> <st c="16495">*<st c="16499">WidgetCenter</st> <st c="16511">为每个主屏幕上的小部件都有一个“预算”,指定一天中它执行刷新的时间。</st> <st c="16623">优化我们的时间线结构和“节省”系统预算符合我们的利益。</st> <st c="16724">精心规划时间线条目和重新加载策略可以帮助我们实现相关的事件驱动</st> <st c="16825">刷新间隔。</st>
<st c="16843">回到</st> <st c="16862">TimelineProvider</st> <st c="16878">协议,我们可以看到另外两个函数 –</st> <st c="16927">placeholder</st> <st c="16938">和</st> <st c="16943">getSnapshot</st> <st c="16954">。让我们</st> <st c="16962">实现它们。</st>
<st c="16977">第一个函数是</st> <st c="17000">getTimeline</st> <st c="17011">,它返回一个</st> <st c="17029">Timeline</st> <st c="17037">结构,其中包含特定时间段的实际数据条目列表。</st> <st c="17116">但这是否足以让我们的小部件完全功能正常?</st>
<st c="17171">答案是:不——在提供实际数据可能不足以满足的两种情况下。</st>
<st c="17265">*<st c="17270">placeholder</st> <st c="17281">函数解决了第一个用例。</st> <st c="17319">当用户将小部件添加到他们的主屏幕时,</st> <st c="17369">WidgetKit</st> <st c="17379">需要在小部件从我们的应用程序获取</st> <st c="17445">或更新真实数据之前立即显示某些内容。</st> <st c="17480">*<st c="17484">placeholder</st> <st c="17495">函数返回临时数据,仅用于向</st> <st c="17554">用户</st> <st c="17555">显示:</st>
func placeholder(in context: Context) -> EventEntry {
EventEntry(date: Date(), nextEvent: "English
class")
}
<st c="17702">占位符</st> <st c="17740">English</st> <st c="17748">类</st>
<st c="18155">getSnapshot</st><st c="18172">getSnapShot</st> <st c="18221">placeholder</st>
getSnapshot</st> <st c="18428">TimelineEntry</st>
<st c="18523">getSnapshot</st>
func getSnapshot(in context: Context, completion:
@escaping (EventEntry) -> Void) {
let entry = EventEntry(date: Date(), nextEvent: "Go
to the book store")
completion(entry)
}
<st c="18739">getSnapshot</st> <st c="18866">小部件</st>
<st c="18899">占位符</st> <st c="18915">getSnapshot</st>中,我们都有与</st><st c="18952">参数</st> <st c="18963">相同的参数,就像我们在</st><st c="18999">函数中拥有的那样。</st> <st c="19010">我们需要</st><st c="19029">的原因与之前相同——为了了解围绕</st>
<st c="19190">TimelineEntry</st>
构建我们的 <st c="19245">TimelineEntry</st> 结构
我们现在可以看到,TimelineProvider<st c="19410">TimelineEntry</st>
<st c="19593">的结构</st>
<st c="19861">时间线条目</st>
-
<st c="19906">日期</st>– 我们希望小部件重新加载特定条目信息的日期。 注意,在大多数情况下, <st c="20020">日期</st>属性不是屏幕上展示的信息的一部分。 例如,在日历小部件中,我们可能将日期属性作为 <st c="20173">时间线条目</st>协议的一部分,并为实际 事件时间使用类似 <st c="20225">eventDate</st>的属性。 -
<st c="20401">标题</st>, <st c="20408">正文内容</st>, 和 <st c="20422">时间字符串</st>, 可以简化我们的代码甚至 提高性能。 -
<st c="20819">时间线条目</st>是用户与之交互时我们所拥有的全部。 -
<st c="20902">相关性</st>属性是我们作为 <st c="20973">时间线条目</st>协议的一部分拥有的可选属性。 在 <st c="21004">相关性</st>属性中,我们可以确定条目对用户的关联优先级。 例如,一个向用户展示下一个任务的待办事项应用可能希望将高分数设置给包含关键任务的条目。 或者,一个在小部件中展示最新新闻的运动应用可能希望将高分数设置给包含用户喜欢的球队新闻的条目。 条目的相关性值帮助 WidgetKit 决定如何在系统中何时展示小部件。 例如, WidgetKit 可能决定旋转堆叠小部件并展示一个高相关性信息的小部件。 让我们看看如何为 <st c="21608">相关性</st>设置 <st c="21622">一个</st>时间线条目 `的例子: struct EventEntry: TimelineEntry { let date: Date let nextEvent: String var relevance: TimelineEntryRelevance? } let entry = EventEntry(date: date, nextEvent: "Go to the book store", <st c="21823">relevance:</st> <st c="21897">relevance</st> property to our <st c="21923">EventEntry</st> struct and set a score of <st c="21960">1.0</st>. It is worth noting that any efforts to manipulate the system and set high scores for all entries won’t succeed – Apple has built an algorithm that filters out widgets that have unrealistic values. As with many iOS frameworks, this is a situation where we need to follow the platform’s intended usage guidelines.
构建我们的小部件 UI
StaticConfiguration(kind: kind, provider: Provider()) {
entry in <st c="22809">MyWidgetEntryView(entry: entry)</st>
<st c="22840">.containerBackground(.fill.tertiary, for:</st>
<st c="22881">.widget)</st> }
<st c="22910">StaticConfiguration</st>
<st c="23241">containerBackground</st>
<st c="23566">containerBackground</st>
<st c="23774">MyWidgetEntryView</st>
处理时间线条目
struct MyWidgetEntryView: View { <st c="24370">let entry:</st> EventEntry
var body: some View {
VStack(alignment: .leading) {
Text("Next Event:")
.font(.headline)
Text(<st c="24486">entry.nextEventTitle</st>)
.font(.title)
.foregroundColor(.blue)
Text("Time: \(<st c="24562">entry.nextEventTime</st>)")
.font(.subheadline)
Spacer()
}
.padding()
}
}
-
条目应包含所有组件的数据 —— 我们在讨论时间线提供者时提到了这一点,但现在我们可以看到原因了。 组件需要尽可能静态和简单。 我们不希望在视图显示时执行任何数据获取操作。 -
没有状态 —— 与常规 SwiftUI 视图不同,我们的组件视图没有状态。 有些情况下,我们可能希望根据不同情况显示不同的视图。 例如,在我们的下一个组件视图示例中,如果我们希望用户尚未批准其日历权限,我们可能想显示一条消息,提示用户 连接到您的日历 **。如果用户尚未批准其日历权限,我们可能想显示一条消息,提示用户 连接到您的日历 **。为了做到这一点,我们需要生成不同的时间线条目,并在静态配置闭包中显示不同的视图。 无论如何,我们应该提前进行这些检查。
<st c="25723">WidgetKit</st>
添加动画
<st c="25994">1.0</st> <st c="26018">0.5</st>
<st c="26374">WidgetCenter</st> <st c="26464">转换</st>。
<st c="26588">contentTransition</st>
@State private var isRed = false
var body: some View {
VStack {
Color(isRed ? .red : .blue)
.frame(width: 100, height: 100)
.cornerRadius(10)
Button("Change Color") { <st c="26898">withAnimation {</st>
<st c="26913">self.isRed.toggle()</st>
<st c="26933">}</st> }
}
}
<st c="27066">withAnimation</st>
<st c="27268">contentTransition</st>
Color(isRed ? .red : .blue)
.frame(width: 100, height: 100)
.cornerRadius(10)
<st c="27367">.contentTransition(.opacity)</st> Button("Change Color") {
withAnimation() {
self.isRed.toggle()
}
}
<st c="27462">contentTransition</st> <st c="27613">withAnimation</st>
Text(text)<st c="27818">withAnimation()</st> function, it will change its content with a nice numeric transition (you can try it yourself). If you are not familiar with the <st c="27962">withAnimation</st> function, *<st c="27986">Chapter 6</st>* provides a brief discussion on it.
<st c="28030">In widgets, all we need to do is to add these to views with content that is based on our timeline entry, and SwiftUI will take care of the</st> <st c="28170">animation itself.</st>
<st c="28187">Look at our widget again, now</st> <st c="28218">with</st> `<st c="28223">contentTransition</st>`<st c="28240">:</st>
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("时间:")
Text(entry.nextEventTime, style: .time)
Text("下一个事件")
Text(entry.nextEvent) <st c="28429">.contentTransition(.numericText())</st> }
}
}
<st c="28469">Even though</st> <st c="28482">there is no state or</st> `<st c="28503">withAnimation</st>` <st c="28516">function, the</st> `<st c="28531">nextEvent</st>` <st c="28540">title will animate its transition.</st> <st c="28576">The</st> `<st c="28580">contentTransiton</st>` <st c="28596">view modifier has additional options, such as opacity and symbol effects.</st> <st c="28671">Despite the fact that it is not designed explicitly for widgets, it’s the best way to make our widgets</st> <st c="28774">more alive.</st>
<st c="28785">Customize our widget</st>
<st c="28806">Up until now, we have discussed widgets based on a</st> `<st c="28858">staticConfiguration</st>`<st c="28877">. The</st> `<st c="28883">staticConfiguration</st>` <st c="28902">set is great for most widgets.</st> <st c="28934">However, there are cases where we</st> <st c="28968">want to provide our users the ability to customize</st> <st c="29019">and configure t</st><st c="29034">heir widgets with</st> <st c="29053">additional entities.</st>
<st c="29073">Going back to our calendar widget, we want to allow the user to filter the next event information based on a</st> <st c="29183">specific calendar.</st>
<st c="29201">To do that, we’ll start by creating a new file and add a struct called</st> `<st c="29273">CalendarWidgetIntent</st>` <st c="29293">that conforms</st> <st c="29308">to</st> `<st c="29311">WidgetConfigurationIntent</st>`<st c="29336">.</st>
<st c="29337">Adding intent</st>
<st c="29351">A</st> `<st c="29354">WidgetConfigurationIntent</st>` <st c="29379">is an App Intent we can use to configure widgets, and our</st> `<st c="29438">CalendarWidgetIntent</st>` <st c="29458">contains all the configuration information</st> <st c="29502">we need.</st>
<st c="29510">Here is</st> <st c="29519">a basic</st> `<st c="29527">CalendarWidgetIntent</st>` <st c="29547">implementation:</st>
struct CalendarWidgetIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select
Calendar"
@Parameter(title: "Calendar") var calendar:
CalendarEntity
}
<st c="29743">In the preceding code, we can see</st> <st c="29778">two properties:</st>
* `<st c="29793">title</st>` <st c="29799">– The title of the intent.</st> <st c="29827">It is important to note that we don’t see the title in the widget configuration string but rather in Siri Shortcuts.</st> <st c="29944">But we must add it since it is part of the</st> `<st c="29987">AppIntent</st>` <st c="29996">protocol (the</st> `<st c="30011">WidgetConfigurationIntent</st>` <st c="30036">inheritance from</st> `<st c="30054">AppIntent</st>` <st c="30063">protocol).</st>
* `<st c="30074">calendar</st>`<st c="30083">– This is the widget parameter that allows the user to configure the calendar the event belongs to.</st> <st c="30184">We can see that the</st> `<st c="30204">calendar</st>` <st c="30212">variable is prefixed by the</st> `<st c="30241">@Parameter</st>` <st c="30251">macro, which manages this property for the</st> <st c="30295">user’s configuration.</st>
<st c="30316">Now, let’s add the</st> <st c="30336">App Intent.</st>
<st c="30347">Adding AppEntity</st>
<st c="30364">As you</st> <st c="30372">have noticed, the calendar variable is based on a type</st> <st c="30427">called</st> `<st c="30434">CalendarEntity</st>`<st c="30448">.</st>
<st c="30449">If we want to support our own entity type, it needs to conform to</st> `<st c="30516">AppEntity</st>`<st c="30525">. Let’s see the</st> `<st c="30541">CalendarEntity</st>` <st c="30555">type implementation:</st>
struct CalendarEntity: AppEntity {
let id: String
let name: String
static var typeDisplayRepresentation:
TypeDisplayRepresentation = "Calendar"
static var defaultQuery = CalendarQuery()
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: name)
}
}
<st c="30852">The</st> `<st c="30857">CalendarEntity</st>` <st c="30871">struct represents the data model for the</st> `<st c="30913">intent</st>` <st c="30919">parameter.</st> <st c="30931">First, we need to add the parameters we need in order to support the item when displaying</st> <st c="31021">the widget, such as</st> `<st c="31041">id</st>` <st c="31043">and</st> `<st c="31048">name</st>`<st c="31052">. Next, we’ll add some representation variables, such as</st> `<st c="31109">typeDisplayRepresentation</st>` <st c="31134">and</st> `<st c="31139">displayRepresentation</st>`<st c="31160">.</st>
<st c="31161">Finally, we’ll add a static variable that handles the actual data fetching, and that’s the</st> `<st c="31253">defaultQuery</st>` <st c="31265">property.</st> <st c="31276">Remember that the user needs to select the desired calendar based on a list of calendars.</st> <st c="31366">To do that, we need to provide</st> *<st c="31397">WidgetKit</st>* <st c="31407">with a way to query our data to support the selection</st> <st c="31461">UI flow.</st>
<st c="31469">So, what does the query look like?</st> <st c="31505">Let’s</st> <st c="31511">find out.</st>
<st c="31520">Building the EntityQuery</st>
<st c="31545">Sometimes, having a</st> <st c="31566">list of options for the user relies on a data store, and sometimes on</st> <st c="31636">static information.</st>
<st c="31655">Regardless of the model type, if we want to provide options to the user, we need to have a simple and effective interface to work with, and that’s what the</st> `<st c="31812">EntityQuery</st>` <st c="31823">protocol</st> <st c="31833">is for.</st>
<st c="31840">In our current</st> `<st c="31856">AppIntent</st>` <st c="31865">example, we let the user choose one of its calendars, so we need to build a struct named</st> `<st c="31955">CalendarQuery</st>` <st c="31968">that conforms</st> <st c="31983">to</st> `<st c="31986">EntityQuery</st>`<st c="31997">.</st>
<st c="31998">Let’s look</st> <st c="32010">at a simple</st> `<st c="32022">CalendarQuery</st>` <st c="32035">example:</st>
struct CalendarQuery: EntityQuery {
func entities(for identifiers: [CalendarEntity.ID])
async throws -> [CalendarEntity] {
allCalendars.filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws ->
[CalendarEntity] {
allCalendars
}
func defaultResult() async -> CalendarEntity? {
nil
}
}
<st c="32352">Assume that</st> `<st c="32365">allCalendars</st>` <st c="32377">is an array containing all the</st> <st c="32409">user calendars.</st>
<st c="32424">In this case,</st> `<st c="32439">CalendarQuery</st>` <st c="32452">implements three methods.</st> <st c="32479">Let’s quickly go</st> <st c="32496">over them:</st>
* `<st c="32506">entities(for identifiers:)</st>` <st c="32533">– This function returns calendar entities based on a list of IDs.</st> *<st c="32600">WidgetKit</st>* <st c="32610">uses it to show the</st> <st c="32630">selected calendar</st>
* `<st c="32647">suggestedEntities()</st>` <st c="32667">– This returns the list of entities in the</st> <st c="32711">pop-up menu</st>
* `<st c="32722">defaultResult()</st>` <st c="32738">– This returns the value when nothing</st> <st c="32777">is selected</st>
<st c="32788">Now, let’s</st> <st c="32800">see how it looks (</st>*<st c="32818">Figure 5</st>**<st c="32827">.3</st>*<st c="32829">):</st>

<st c="32885">Figure 5.3: The widget configuration menu</st>
<st c="32926">In</st> *<st c="32930">Figure 5</st>**<st c="32938">.3</st>*<st c="32940">, we can see the</st> `<st c="33153">Bool</st>` <st c="33157">or</st> `<st c="33161">String</st>`<st c="33167">, and</st> *<st c="33173">WidgetKit</st>* <st c="33183">will create their corresponding</st> <st c="33215">input control.</st>
<st c="33229">Let’s flip to the other side now and go to the widget UI to use the</st> `<st c="33298">AppEntity</st>` <st c="33307">the</st> <st c="33312">user selected.</st>
<st c="33326">Using the AppEntity in our Widget</st>
<st c="33360">Going back</st> <st c="33372">to our widget code, let’s examine the widget configuration</st> <st c="33431">code again:</st>
AppIntentConfiguration(kind: kind,
ConfigurableProvider(), content: { entry in
ConfigurableWidgetView(entry: entry)
})
<st c="33606">The</st> `<st c="33611">AppIntentConfiguration</st>` <st c="33633">struct has an important property, which is the intent type it uses, and in this case, it is</st> `<st c="33726">CalendarWidgetIntent</st>`<st c="33746">. If we go back to the</st> *<st c="33769">Customize our widget</st>* <st c="33789">section, we can see that</st> `<st c="33815">CalendarWidgetIntent</st>` <st c="33835">contains all the information we need to present our widget according to the</st> <st c="33912">user configuration.</st>
<st c="33931">Indeed, the timeline provider is now conforming to a different protocol,</st> `<st c="34005">AppIntentTimelineProvider</st>`<st c="34030">, which supports the intent configuration now.</st> <st c="34077">Let’s see how it creates</st> <st c="34102">a timeline:</st>
struct ConfigurableProvider: AppIntentTimelineProvider {
func timeline(for configuration: CalendarWidgetIntent,
in context: Context) async ->
Timeline<ConfiguredNextEventEntry>
<st c="34290">We can see that the timeline function inside</st> `<st c="34336">ConfigurableProvider</st>` <st c="34356">now receives the configuration parameter.</st> <st c="34399">From this point, all we need to do is use the information we have inside the configuration and create the relevant</st> <st c="34514">timeline entries.</st>
<st c="34531">By now, we know how to set up a new widget, animate it, create its timeline, and even let the user configure it.</st> <st c="34645">Next, we’ll learn how to ensure our widgets stay up</st> <st c="34697">to date.</st>
<st c="34705">Keeping our widgets up to date</st>
<st c="34736">We have learned that we need to look ahead and create a timeline with different entries and dates to</st> <st c="34838">keep our widget up to date.</st> <st c="34866">But how does our widget work under</st> <st c="34901">the hood?</st>
<st c="34910">Widgets don’t get any running time – once we generate the timeline entries,</st> *<st c="34987">WidgetCenter</st>* <st c="34999">generates their different views, keeps them persistently, and just switches them according to the</st> <st c="35098">provided timeline.</st>
<st c="35116">So, there’s no way to update our widget without reloading the timeline, and when we created our timeline, we had to define its</st> <st c="35244">reload policy:</st>
let timeline = Timeline(entries: entries,
<st c="35451">让我们看看它是如何发生的。</st>
<st c="35477">使用 WidgetCenter 重新加载小部件</st>
<st c="35515">在整个章节中,我经常提到</st> *<st c="35557">WidgetCenter</st>* <st c="35569">,但我还没有解释它的含义。</st>
*<st c="35619">WidgetCenter</st>* 是一个包含当前使用配置的不同小部件信息的对象,它还提供了一个选项来 <st c="35758">重新加载它们。</st>
<st c="35770">要使用 <st c="35778">WidgetCenter</st><st c="35790">,我们需要调用 <st c="35812">shared</st> <st c="35818">属性来访问其 <st c="35842">单例引用:</st>
WidgetCenter.shared
<st c="35882">WidgetCenter</st> 与我们迄今为止处理的其他代码之间的区别在于,我们是从应用中调用 <st c="35998">WidgetCenter</st> 而不是调用 <st c="36036">widget 扩展。</st>
<st c="36053">让我们看看如何调用 <st c="36084">WidgetCenter</st> 来获取活动小部件的列表:</st>
func getConfigurations() { <st c="36157">WidgetCenter.shared.getCurrentConfigurations</st> { result
in
if let widgets = try? result.get() {
// handle our widgets
}
}
}
<st c="36278">The</st> `<st c="36283">getCurrentConfigurations</st>` <st c="36307">函数使用闭包来返回一个活动小部件的数组。</st> <st c="36370">它们中的每一个都是 <st c="36394">WidgetInfo</st> <st c="36404">类型 – 一个包含有关特定 <st c="36450">配置小部件</st> <st c="36467">信息的结构。</st>
<st c="36485">The</st> `<st c="36490">WidgetInfo</st>` <st c="36500">结构有三个属性 – kind, family,和 configuration:</st>
+ `<st c="36566">kind</st>` <st c="36571">– 这是我们创建小部件配置时设置的字符串(再次查看 *<st c="36660">配置我们的</st> * *<st c="36676">小部件</st> * *<st c="36682">部分)。</st>
+ `<st c="36692">family</st>` <st c="36699">– 小部件的家庭大小 – 小型、中型或大型。</st>
+ `<st c="36758">configuration</st>` <st c="36772">– 包含用户配置信息的意图。</st> <st c="36832">The</st> `<st c="36836">configuration</st>` <st c="36849">属性是可选的。</st>
<st c="36871">如果需要,我们可以使用这些信息来重新加载特定类型的小部件的时间线。</st> <st c="36964">例如,如果我们想重新加载具有 <st c="37023">MyWidget</st> <st c="37031">类型的小部件,我们需要调用</st> <st c="37049">以下内容:</st>
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
<st c="37119">注意,该函数说的是 <st c="37150">Timelines</st> <st c="37159">而不是 <st c="37168">Timeline</st>,因为可能存在多个相同类型的小部件。</st>
<st c="37237">如果我们想重新加载我们应用中的所有小部件,我们可以调用 <st c="37296">reloadAllTimelines()</st> <st c="37316">函数:</st>
`<st c="37326">WidgetCenter.shared.reloadAllTimelines()</st>`
<st c="37367">在我们的小部件时间线中重新加载有几个很好的用例,例如当我们收到推送通知,或者当用户数据或设置发生变化时。</st> <st c="37526">如果你还记得,当我们讨论小部件时间线时,在</st> *<st c="37588">生成时间线</st>* <st c="37609">部分,我们提到了小部件每天可以重新加载的次数有一定的预算。</st> <st c="37729">但好消息是,如果我们的应用在前台或使用某些其他技术,例如在</st> <st c="37931">后台播放音频</st>,调用 `<st c="37767">reloadTimelines</st>` <st c="37782">或</st> `<st c="37786">reloadAllTimelines</st>` <st c="37804">函数不计入这个预算。</st>
<st c="37946">在大多数</st> <st c="37955">情况下,</st> `<st c="37962">reloadTimelines</st>` <st c="37977">当更新后的数据已经在设备上或在我们的应用中时,工作得很好。</st> <st c="38051">但当我们本地的持久存储没有更新时,我们应该怎么办呢?</st>
<st c="38120">我们执行一个网络请求,</st> <st c="38151">当然!</st>
<st c="38161">前往网络获取更新</st>
<st c="38191">在移动应用中,执行网络请求以更新本地数据是一种典型操作。</st> <st c="38281">但在小部件中它是如何工作的呢?</st> <st c="38289">does it work</st> <st c="38302">in widgets?</st>
<st c="38313">让我们再次看看</st> `<st c="38332">getTimeline</st>` <st c="38343">函数:</st>
func getTimeline(in context: Context, <st c="38465">getTimeline</st> function is an asynchronous function. It means that when we build our timeline, we can perform async operations such as open URL sessions and fetching data.
<st c="38633">Let’s see an example of requesting the next</st> <st c="38678">calendar events:</st>
func getTimeline(in context: Context, completion: @escaping
(Timeline<SimpleEntry>) -> Void) {
var entries: [SimpleEntry] = [] <st c="38822">calendarService.fetchNextEvents { result in</st> switch result {
case .success(let events):
for event in events {
let entry = SimpleEntry(date:
event.alertTime, nextEvent:
event.title, nextEventTime:
event.date)
entries.append(entry)
}
case .failure(let error):
print("Error fetching next events:
\(error.localizedDescription)")
}
let timeline = Timeline(entries: entries, policy:
.atEnd)
completion(timeline)
}
}
<st c="39230">The</st> `<st c="39235">getTimeline</st>` <st c="39246">function implementation is similar to the previous</st> `<st c="39298">getTimeline</st>` <st c="39309">implementation we saw in the</st> *<st c="39339">Generating a timeline</st>* <st c="39360">section, and this time, we are fetching the</st> <st c="39405">events using the</st> `<st c="39422">calendarService</st>` <st c="39437">instance.</st> <st c="39448">The</st> `<st c="39452">calendarService</st>` <st c="39467">goes to our server and returns an array of events.</st> <st c="39519">Afterward, we loop the events, generate timeline entries, and return a timeline using the</st> `<st c="39609">completion</st>` <st c="39619">block.</st>
<st c="39626">Up until now, we have seen how to create a widget, animate it, and ensure it is updated as much as we can.</st> <st c="39734">But if we want to make our widget shine, we need to add some</st> <st c="39795">user-interactive capabilities.</st>
<st c="39825">Interacting with our widget</st>
<st c="39853">Besides</st> <st c="39862">providing us with a glance at our app information, widgets are a great way to open our app in a specific location or manipulate data without even opening</st> <st c="40016">the app.</st>
<st c="40024">As mobile developers, we sometimes wonder why implementing user interaction with a widget is such a big deal.</st> <st c="40135">After all, our users interact with our app daily, so why is it such a problem?</st> <st c="40214">But when we remember that widgets don’t really run, we can understand</st> <st c="40284">the challenge.</st>
<st c="40298">The most basic way we have to allow interaction with our widgets is by using</st> <st c="40376">deep links.</st>
<st c="40387">Opening a specific screen using links</st>
<st c="40425">If you are not familiar with the concept of deep links, now is the time to straighten things out.</st> <st c="40524">A deep</st> <st c="40531">link is a link that opens our app on a specific screen.</st> <st c="40587">Today’s deep links format is similar to website URLs.</st> <st c="40641">For example, a deep link that opens our app in a specific calendar event screen can look something</st> <st c="40740">like this:</st>
http://www.myGreatCalendarAp.com/event/
<st c="40800">To do that, the app needs to do</st> <st c="40833">three things:</st>
* *<st c="40846">Register to that specific domain</st>* <st c="40879">by placing a special JSON file on the</st> <st c="40918">relevant server</st>
* <st c="40933">Add the domain</st> *<st c="40949">entitlement</st>* <st c="40960">to</st> <st c="40964">our app</st>
* <st c="40971">Respond to</st> *<st c="40983">launching the app from a deep link</st>*<st c="41017">, parse the URL, and direct the user to the corresponding location within</st> <st c="41091">the app</st>
<st c="41098">To learn more about deep links, I recommend reading about it in the Apple</st> <st c="41173">Developer website:</st>
[<st c="41191">https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content</st>](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content)
<st c="41290">Going back to our topic, let’s see an example of adding a deep link to our</st> `<st c="41366">Next</st>` `<st c="41371">Event</st>` <st c="41376">widget:</st>
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("时间:")
Text(entry.nextEventTime, style: .time)
Text("Next Event")
Text(entry.nextEvent) <st c="41571">.widgetURL(URL(string:</st>
}
}
<st c="41663">In the</st> <st c="41671">preceding code example, we can see that we added a view modifier called</st> `<st c="41743">widgetURL</st>` <st c="41752">to the</st> `<st c="41760">Next Event</st>` `<st c="41770">Text</st>` <st c="41775">component.</st>
<st c="41786">The</st> `<st c="41791">Next Event</st>` `<st c="41801">Text</st>` <st c="41806">component is indeed the view that accepts the user’s touch and opens the app in the specific deep link.</st> <st c="41911">But when the widget is small (</st>`<st c="41941">.systemSmall</st>`<st c="41953">), we can add only one deep link that is acceptable in the</st> <st c="42013">whole widget.</st>
<st c="42026">In widgets with medium and large sizes, we can add multiple links to</st> <st c="42096">multiple components.</st>
<st c="42116">It is worth noting that in terms of security, deep links work even when the device is locked, but require</st> *<st c="42223">FaceID</st>* <st c="42229">or passcode when tapping</st> <st c="42255">on them.</st>
<st c="42263">In iOS 17, deep links are not the only option we have to allow users to interact with our widget, as it is possible to add buttons and toggles</st> <st c="42407">as well.</st>
<st c="42415">Adding interactive capabilities</st>
<st c="42447">Deep links</st> <st c="42459">in widgets are great, but they have one big problem – tapping on the widget always opens the app.</st> <st c="42557">But, sometimes, we want to update data or confirm something without entering the app.</st> <st c="42643">For example, maybe we want to accept a calendar invitation, approve a payment, or mark a task</st> <st c="42737">as completed.</st>
<st c="42750">Because</st> <st c="42759">widgets don’t actually run, it’s a challenge to respond to a user interaction.</st> <st c="42838">Fortunately, there is a solution that we have already encountered in configurable widgets, and that’s</st> <st c="42940">App Intents.</st>
<st c="42952">Using App Intents to add interactive widgets</st>
<st c="42997">App Intents</st> <st c="43010">made especially for this kind of use case – to allow runtime for</st> <st c="43075">specific actions.</st>
<st c="43092">So, how do App Intents help us?</st> <st c="43125">Let’s look at</st> *<st c="43139">Figure 5</st>**<st c="43147">.4</st>*<st c="43149">:</st>

<st c="43315">Figure 5.4: App Intent event flow</st>
<st c="43348">In</st> *<st c="43352">Figure 5</st>**<st c="43360">.4</st>*<st c="43362">, we can see that the first stage is tapping on a button inside the widget.</st> <st c="43438">Starting from iOS 17, the</st> *<st c="43464">WidgetKit</st>* <st c="43474">framework has its own type of button, which can be linked to a specific</st> <st c="43546">App Intent:</st>
Button("Turn (entry.isAlarm ? "Off" : "On") Alarm" , role:
nil,
<st c="43979">让我们谈谈应用意图,但这次是在</st> <st c="44046">用户交互</st>的上下文中。</st>
<st c="44063">使用意图执行数据更改</st>
我们已经说过,小部件不管理任何状态。因此,真正的 widget 状态是本地存储和 timeline provider 之间的一种组合。
`<MyWidgetIntent>`接收一个`eventID`并负责联系`EventKit`并更新实际事件的警报信息。
让我们看看`App Intent`:
struct MyWidgetIntent: AppIntent {
init() {
}
var eventID: String = ""
init(eventID: String) {
self.eventID = eventID
}
static var title: LocalizedStringResource = "Changing '
event alarm settings." func perform() async throws -> some IntentResult {
// working with EventKit and updating the event alarm data. return .result()
}
}
除了我们在*自定义我们的小部件*部分讨论的`LocalizedStringResource`静态属性之外,我们还有一个主要函数叫做`perform()`。`perform()`函数在用户点击与该 App Intent 关联的按钮时执行。请注意,`perform()`函数也是一个异步函数,它允许我们执行更重的任务,例如写入数据库或执行 URL 请求。
一旦`perform()`函数完成执行,App Intent 就会触发*WidgetCenter*。
更新小部件 UI
现在本地存储已更新,是时候让*WidgetCenter*重新加载 Timeline Provider 了。我们应该已经熟悉这个过程了——Timeline Provider 获取相关的本地数据,并根据我们刚刚执行的变化构建时间线。最后,小部件 UI 正在被更新。
如果我们想在不同的应用组件之间共享代码执行,使用 App Intent 也是很好的。例如,我们可以在我们的 widget 和 Siri Shortcut 之间共享逻辑代码。
我们应该记住,即使小部件可能有它自己的运行时,将我们的代码分离以获得更好的灵活性和模块化仍然是一个好的实践。
App Intent 的另一个很好的用途是控制小部件,这是 iOS 18 的另一个伟大补充。现在让我们来了解一下。
添加控制小部件
*<st c="46114">WidgetKit</st>* <st c="46125">提供了在启动器中展示我们的应用程序的方法。</st> <st c="46139">然而,它并不止于此。</st> <st c="46211">从 iOS 18 开始,我们可以在控制中心和锁屏上展示小部件,甚至可以将应用程序意图附加到 iPhone 15 Pro 的操作按钮上。</st>
<st c="46375">将小部件添加到控制中心或锁屏上</st> <st c="46433">很容易。</st>
<st c="46441">类似于我们通过遵循小部件协议创建小部件的方式,我们需要遵循</st> `<st c="46540">ControlWidget</st>` <st c="46553">协议来创建控制小部件。</st> <st c="46591">例如,想象我们有一个帮助我们控制智能家居配件的应用程序,我们想要创建一个可以打开和关闭我们家的主门的小部件。</st> <st c="46704">让我们从创建一个简单的控制小部件开始,命名为</st> `<st c="46803">MaindoorControl</st>`<st c="46818">:</st>
struct MaindoorControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.avitsadok.MaindoorControl"
) {
// rest of the widget goes here
}
}
}
<st c="47012">在这个代码示例中,</st> `<st c="47039">MaindoorControl</st>` <st c="47054">小部件包含了从</st> `<st c="47075">body</st>` <st c="47079">变量到</st> `<st c="47106">ControlWidgetConfiguration</st>`<st c="47132">的时间点。</st> 这与我们如何在</st> *<st c="47204">配置我们的</st>* *<st c="47220">小部件</st>* <st c="47226">部分创建一个主屏幕小部件非常相似。</st>
<st c="47235">在这种情况下,我们返回一个</st> `<st c="47279">StaticControlConfiguration</st>` <st c="47305">类型的实例,这意味着我们不向用户提供配置它的能力。</st> <st c="47376">然而,类似于主屏幕小部件,我们也可以通过返回</st> `<st c="47484">AppIntentControlConfiguration</st>` <st c="47513">(查看</st> *<st c="47527">自定义我们的</st>* *<st c="47541">小部件</st>* <st c="47547">部分)来添加一个用户可配置的控制小部件。</st>
<st c="47557">我们可以添加两个控制小部件控件 – 一个切换和一个按钮。</st> <st c="47622">在我们的家庭主门状态控制的情况下,我们需要添加一个切换。</st> <st c="47702">让我们修改我们的代码并添加一个</st> `<st c="47734">ControlWidgetToggle</st>` <st c="47753">实例:</st>
struct MaindoorControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.avitsadok.MaindoorControl"
) { <st c="47918">ControlWidgetToggle(</st>
<st c="47938">"Main door control",</st>
<st c="47959">isOn: HouseManager.shared.isOpen,</st>
<st c="47993">action: MaindoorIntent()</st>
<st c="48018">) { isOn in</st>
<st c="48030">Label(isOn ?</st> <st c="48044">"Opened" : "Closed",</st>
<st c="48064">systemImage: isOn ?</st>
<st c="48084">"door.left.hand.open" :</st>
<st c="48108">"door.left.hand.closed")</st>
<st c="48133">}</st> }
}
}
<st c="48141">在这个</st> <st c="48150">代码示例中,我们添加了</st> `<st c="48175">ControlWidgetToggle</st>`<st c="48194">,包含以下参数:</st>
+ **<st c="48232">一个标题</st>** <st c="48240">– 在小部件画廊中显示的小部件标题。</st>
+ `<st c="48310">isOn</st>` <st c="48315">– 在这里,我们将小部件连接到我们应用程序中的实际状态。</st> <st c="48369">。</st>
+ `<st c="48377">action</st>` <st c="48384">– 当用户点击我们的控制小部件时运行的 App Intent。</st> <st c="48451">我们将在本节的后面部分介绍这一点。</st>
+ `<st c="48545">标签</st>` <st c="48550">显示控制状态的标题和</st> <st c="48590">一个图像。</st>
<st c="48599">小部件实例很简单。</st> <st c="48640">让我们看看它在我们的控制中心(</st>*<st c="48686">图 5</st>**<st c="48695">.5</st>*<st c="48697">)中的样子:</st>

<st c="48702">图 5.5:控制中心中的我们的控制小部件</st>
*<st c="48754">图 5</st>**<st c="48763">.5</st>* <st c="48765">显示了控制中心的主门控制小部件。</st> <st c="48819">然而,我们的控制小部件还有另一个方面</st> <st c="48849">——将控制小部件连接到打开和关闭主门的操作。</st> <st c="48955">让我们看看我们在</st> `<st c="48973">MaindoorIntent</st>` <st c="48987">参数中看到的</st> `<st c="49009">action</st>` <st c="49015">结构:</st>
struct MaindoorIntent: SetValueIntent {
static let title: LocalizedStringResource = "Maindoor
opening"
@Parameter(title: "is open")
var value: Bool
func perform() throws -> some IntentResult {
HouseManager.shared.isOpen = value
return .result()
}
}
<st c="49275">在这个代码示例中,我们看到了</st> `<st c="49309">MaindoorIntent</st>` <st c="49323">实现。</st> <st c="49340">`<st c="49344">MaindoorIntent</st>` <st c="49358">结构符合`<st c="49385">SetValueIntent</st>` <st c="49399">协议,该协议包含一个我们可以设置的值。</st> <st c="49445">在这个例子中,值来自`<st c="49484">Bool</st>` <st c="49488">类型,我们可以用它来执行</st> <st c="49527">所需的操作。</st>
<st c="49545">将控制小部件添加到我们的应用程序中涉及我们在添加主屏幕小部件和允许我们在小部件和其他</st> <st c="49725">应用程序组件之间共享代码的应用程序意图时看到的类似做法。</st>
<st c="49740">摘要</st>
<st c="49748">小部件是我们在 iOS 开发中可以与之交互的有趣且有趣的 UI 元素。</st> <st c="49828">它们提供流畅的 UI、出色的动画和可一览无余的用户体验。</st> <st c="49902">我们已经看到,每个 iOS 版本都添加了有趣的新小部件功能,使小部件比以往任何时候都更强大</st> <st c="50013">。</st>
<st c="50023">在本章中,我们了解了小部件的概念、如何添加小部件、创建时间线以及添加用户可配置选项。</st> <st c="50158">此外,我们还学习了如何创建自定义动画,甚至添加用户交互。</st> *<st c="50238">WidgetKit</st>* <st c="50248">已经成为一个令人着迷的框架。</st> <st c="50297">在下一章中,我们将继续探讨如何改进用户体验,这次我们将使用</st> <st c="50396">SwiftUI 动画。</st>
第七章:6
SwiftUI 动画和 SF 符号
-
讨论动画的重要性 动画 -
理解 SwiftUI 动画概念 -
使用视图修改器和 <st c="851">withAnimation</st>函数 执行基本动画 -
执行高级动画,如过渡和 关键帧动画 -
动画 SF 符号
技术要求
动画的重要性
-
首先,动画为用户的操作提供视觉反馈——当用户点击按钮时,按钮会变大,这有助于他们知道他们触摸了正确的位置。
-
动画还可以提供指导和导航——页面之间的转换表明我们是“向前”还是“向后”移动我们的流程。
-
动画还有助于错误处理——我们可以动画化错误消息和一般问题,并减少用户的挫败感。
-
最重要的是,在许多情况下,动画是应用品牌和独特性的一部分,并提供那种特殊的触感,加强用户与应用之间的联系。
现在我们已经了解了动画的重要性,让我们看看 SwiftUI 的声明式方法如何与该概念相一致。
理解 SwiftUI 动画的概念
对于一个来自 UIKit 并在 SwiftUI 中迈出第一步的开发者来说,在声明式框架中编写动画的概念可能会感觉有点尴尬。
UIView.animate(withDuration: 2.0, animations: {
sampleView.alpha = 0.0
}) { (finished) in
}
在这个示例中,我们修改了 <st c="3436">sampleView</st> 在 <st c="3456">UIView</st> 动画闭包中的 alpha 级别。
虽然这看起来很简单,但它带来一个显著的缺点——需要将动画动作同步到屏幕状态。
然而,在 SwiftUI 中,屏幕状态始终与 UI 保持同步,这对动画也是如此。
在 SwiftUI 中实现动画有几种方法;有些确实很简单,而另一些则允许我们提供高级和复杂的动画。
执行基本 动画
-
<st c="4617">animation</st>修饰符 – 将动画添加到一个 特定的视图 上 -
<st c="4687">withAnimation</st>全局函数 – 通过改变 几个状态 来执行动画 -
<st c="4773">animation()</st>方法 – 将动画附加到一个 绑定值 上
使用动画视图修饰符
struct UsingAnimationModifier: View {
@State var width: CGFloat = 50
@State var height: CGFloat = 50
var body: some View {
ZStack {
Circle()
.frame(width:width, height:height)
.foregroundColor(.blue) <st c="5797">.animation(.easeIn, value: width)</st> .onTapGesture {
width += 50
height += 50
}
}
}
}
<st c="5932">50</st> <st c="6164">width</st>
<st c="6621">withAnimation:</st>
使用 withAnimation 函数
<st c="6731">withAnimation:</st>
struct UsingWithAnimationFunction: View {
@State var greenCircleYPosition: CGFloat = 400
@State var redCircleYPosition: CGFloat = 800
var body: some View {
VStack {
ZStack {
Circle()
.size(width: 100.0, height: 100.0)
.foregroundColor(.green)
.position(x: 400, y:
greenCircleYPosition)
Circle()
.size(width: 100.0, height: 100.0)
.foregroundColor(.red)
.position(x: 200, y:
redCircleYPosition)
}
Button("Animate") { <st c="7335">withAnimation {</st>
<st c="7350">greenCircleYPosition =</st>
<st c="7373">greenCircleYPosition == 400 ?</st> <st c="7404">800 :</st>
<st c="7409">400</st>
<st c="7413">redCircleYPosition = redCircleYPosition</st>
<st c="7453">== 800 ?</st> <st c="7463">400 : 800</st>
<st c="7472">}</st> }
}
}
}
<st c="7645">withAnimation:</st>
<st c="7754">withAnimation:</st>
struct WithAnimationCompletionBlock: View {
@State var yPos: CGFloat = 300
@State var isReset: Bool = false
var body: some View {
VStack {
Circle()
.foregroundColor(.blue)
.frame(width: 50, height:50)
.position(x: 200, y:yPos)
Button(isReset ? "Reset" : "Start") { <st c="8155">withAnimation</st> {
if isReset {
yPos = 300
} else {
yPos = 500
} <st c="8217">} completion: {</st>
<st c="8232">isReset.toggle()</st>
<st c="8249">}</st> }
}
}
}
用弹簧动画给我们的动画增添一些活力
withAnimation<st c="9179">(.bouncy(extraBounce: 0.3))</st> {
if isReset {
yPos = 300
} else {
yPos = 500
}
} completion: {
isReset.toggle()
}
}
<st c="9319">.bouncy(extraBounce: 0.3)</st> <st c="9352">withAnimation</st>
<st c="9750">.</st>``<st c="9751">smooth</st>
withAnimation(.smooth(extraBounce: 0.3))
withAnimation(.snappy)
执行高级动画
执行过渡
实现内置过渡
<st c="11113">transition</st> <st c="11200">withAnimation</st>
struct BuiltInTransitionsView: View {
@State var showSlideText: Bool = false
var body: some View {
VStack {
Button("Slide in text") { <st c="11473">withAnimation {</st> showSlideText.toggle()
}
}
if showSlideText {
Text("Hello, slided
text")<st c="11561">.transition(.slide)</st> }
}
}
<st c="11616">VStack</st>
<st c="11768">withAnimation</st>
<st c="12033">slide</st>
-
<st c="12335">move</st>: 将视图移动到/从特定边缘: Text("Hello, moved text") .transition(.move(edge: .bottom)) -
<st c="12442">scale</st>: 以特定数量和从特定锚点缩放视图: Text("Hello, scaled text") .transition(.scale(scale: 0.5, anchor: .center)) -
<st c="12591">opacity</st>: 对视图执行“淡入/淡出”效果: 。 Text("Hello, opacity text") .transition(.opacity)
Text("Text scaled in. Now it will slide out")
<st c="13183">.transition(.asymmetric(insertion: .scale, removal:</st>
<st c="13272">scale</st> animation for the insertion of text and a <st c="13320">slide</st> animation for the removal of text.
<st c="13360">Sometimes, we may want to combine several animations.</st> <st c="13415">For example, we may want to scale</st> <st c="13449">and slide at the same time.</st> <st c="13477">We can do that using the</st> `<st c="13502">combined</st>` <st c="13510">function:</st>
Text("缩放和滑动")
<st c="13586">We can even combine a</st> <st c="13609">combined transition!</st>
.transition(.scale.combined(with: .slide.combined(with:
.opacity)))
<st c="13696">However, if things become too complicated, it could be a sign that we should build a</st> <st c="13782">custom transition.</st>
<st c="13800">Creating a custom transition</st>
<st c="13829">Building</st> **<st c="13839">custom transitions</st>** <st c="13857">gives us complete control and flexibility of how transitions</st> <st c="13919">work and is useful when other compound transition methods</st> <st c="13977">don’t provide the</st> <st c="13995">expected results.</st>
<st c="14012">The idea of building a custom transition is built around providing two</st> <st c="14084">view modifiers:</st>
* <st c="14099">One that represents the</st> *<st c="14124">identity</st>* <st c="14132">state of the view (before we started</st> <st c="14170">the transition)</st>
* <st c="14185">One that represents the</st> *<st c="14210">active</st>* <st c="14216">state of the view (after</st> <st c="14242">the transition)</st>
<st c="14257">Both view modifiers must be of the same type so that SwiftUI has the same properties</st> <st c="14343">to transition.</st>
<st c="14357">Let’s create a custom transition that takes a view and inserts it with rotation, opacity,</st> <st c="14448">and scale.</st>
<st c="14458">We will start by creating a view modifier that handles all the</st> <st c="14522">three properties:</st>
struct ViewRotationModifier: ViewModifier {
let angle: Angle
let opacity: CGFloat
let scale: CGFloat
func body(content: Content) -> some View {
content
.rotationEffect(angle)
.scaleEffect(scale)
.opacity(opacity)
}
}
<st c="14756">The</st> `<st c="14761">ViewRotationModifier</st>` <st c="14781">view modifier receives three properties,</st> `<st c="14823">angle</st>`<st c="14828">,</st> `<st c="14830">opacity</st>`<st c="14837">, and</st> `<st c="14843">scale</st>`<st c="14848">, and applies them to the content.</st> <st c="14883">This view modifier is like any view modifier we’re</st> <st c="14934">accustomed to.</st>
<st c="14948">Now, we can</st> <st c="14961">build our custom transition.</st> <st c="14990">If we look at the built-in transitions we covered in the previous</st> *<st c="15056">Implementing built-in transitions</st>* <st c="15089">section and their code’s documentation, we can see that they are from the type</st> `<st c="15169">AnyTransition</st>`<st c="15182">.</st> `<st c="15184">AnyTransition</st>` <st c="15197">is a struct that describes a SwiftUI transition between</st> <st c="15254">two states.</st>
<st c="15265">Let’s build our</st> `<st c="15282">rotate</st>` `<st c="15288">AnyTransition</st>`<st c="15302">:</st>
let rotate = AnyTransition.modifier(
active: <st c="15350">ViewRotationModifier</st>(angle: .degrees(360),
opacity: 0.0, scale: 0.0),
identity: <st c="15431">ViewRotationModifier</st>(angle: .degrees(0),
opacity: 1.0, scale: 1.0)
)
<st c="15500">The</st> `<st c="15505">AnyTransition</st>` <st c="15518">struct we created receives the</st> `<st c="15550">active</st>` <st c="15556">and</st> `<st c="15561">identity</st>` <st c="15569">view modifiers, each with</st> <st c="15596">different parameters.</st>
<st c="15617">We can</st> <st c="15625">use the new transition in the same way as the</st> <st c="15671">built-in transitions:</st>
struct CustomizedTransitionView: View {
@State private var showRectangle: Bool = false
var body: some View {
VStack {
Spacer()
if showRectangle {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue) <st c="15907">.transition(rotate)</st> }
Spacer()
Button("插入矩形") {
withAnimation {
showRectangle.toggle()
}
}
}
}
}
<st c="16015">The preceding code creates a rectangle and a button.</st> <st c="16069">Tapping on the button toggles the</st> `<st c="16103">showRectangle</st>` <st c="16116">state variable, which reveals the rectangle using our</st> <st c="16171">new transition.</st>
<st c="16186">So far, we have discussed great animations that were pretty simple and short.</st> <st c="16265">However, if we</st> <st c="16280">want to provide more sophisticated animations that may require multiple stages and different timing,</st> `<st c="16381">AnyTransition</st>` <st c="16394">structure is insufficient.</st> <st c="16422">For much more advanced animations, we should try to implement</st> <st c="16484">keyframe animations.</st>
<st c="16504">Executing keyframe animations</st>
<st c="16534">The idea</st> <st c="16544">of</st> **<st c="16547">keyframe animations</st>** <st c="16566">in SwiftUI is similar to how they are implemented</st> <st c="16617">in UIkit.</st>
<st c="16626">With</st> <st c="16632">keyframe animations, we declare different changes in different properties over time.</st> <st c="16717">There are four primary components</st> <st c="16751">in</st> <st c="16754">keyframe animations:</st>
* `<st c="16900">AnimationsProperties</st>` <st c="16920">struct can define the opacity, scale, or color in different</st> <st c="16981">animation phases.</st>
* `<st c="16998">KeyFrameAnimator</st>`<st c="17015">: The keyframe animator defines the different animation tracks we have and what happens with the view in</st> <st c="17121">each track.</st>
* `<st c="17132">KeyframeTrack</st>`<st c="17146">: Each track handles a different animation property and defines the various phases (key frames) for that property.</st> <st c="17262">Tracks work in parallel with each other.</st> <st c="17303">A keyframe animator can have</st> <st c="17332">multiple tracks.</st>
* `<st c="17348">KeyFrame</st>`<st c="17357">: Defines a single change for a specific property within the</st> <st c="17419">keyframe track.</st>
<st c="17434">With these</st> <st c="17446">four primary components, we can build amazing and complex animations.</st> <st c="17516">Let’s build our first keyframe animation with SwiftUI, but we’ll start by explaining the concept behind</st> <st c="17620">keyframe animations.</st>
<st c="17640">Understanding a keyframe animation</st>
<st c="17675">Describing a</st> <st c="17689">keyframe animation can be slightly confusing at first, mainly because it is a way to create complex animations.</st> <st c="17801">Let’s try to explain it in a diagram (</st>*<st c="17839">Figure 6</st>**<st c="17848">.1</st>*<st c="17850">):</st>

<st c="17894">Figure 6.1: A key frame animation as a diagram</st>
*<st c="17940">Figure 6</st>**<st c="17949">.1</st>* <st c="17951">shows two tracks – scale and opacity – positioned on a timeline.</st> <st c="18017">In each track, we see two keyframes.</st> <st c="18054">The number inside each keyframe describes the value, and the keyframe length describes its duration.</st> <st c="18155">For example, in the scale track, we have two keyframes – the first sets the scale to 0.7, and the second brings it back to 1.0\.</st> <st c="18283">We can also see that the durations of both the scale and opacity tracks</st> <st c="18355">are equal.</st>
<st c="18365">If you think that that resembles a video editing application such as</st> *<st c="18435">iMovie</st>* <st c="18441">or</st> *<st c="18445">Premiere</st>*<st c="18453">, that’s because it is based on the</st> <st c="18489">same concept.</st>
<st c="18502">Let’s try to create a breathing animation using the concept of keyframe animation.</st> <st c="18586">A breathing animation mimics the way something breathes, such as a balloon slowly inflating</st> <st c="18678">and deflating.</st>
<st c="18692">Let’s see how to do that</st> <st c="18718">in code:</st>
struct AnimationProperties {
var scale = 1.0
var opacity = 1.0
}
struct KeyFrameAnimations: View {
var body: some View {
Circle()
.foregroundColor(.red)
.frame(width:100, height:100)
.<st c="18911">关键帧动画器</st>(initialValue:
AnimationProperties(), repeating: true) {
content, value in
content
.opacity(value.opacity)
.scaleEffect(value.scale)
} <st c="19064">关键帧</st>: { _ in <st c="19083">KeyframeTrack</st>(\.scale) { <st c="19109">CubicKeyframe</st>(0.7, duration: 0.8) <st c="19144">CubicKeyframe</st>(1.0,
duration: 0.8)
} <st c="19181">关键帧轨道</st>(\.opacity) { <st c="19209">CubicKeyframe</st>(0.3, duration: 0.8) <st c="19244">CubicKeyframe</st>(1.0, duration: 0.8)
}
}
}
}
<st c="19286">The code example seems long!</st> <st c="19316">However, upon closer examination, we can see that it is not that complex and contains the different components we</st> <st c="19430">discussed earlier.</st>
<st c="19448">Let’s explain</st> <st c="19463">what we’ve</st> <st c="19474">done here:</st>
1. <st c="19484">We created a circle and added a view modifier called</st> `<st c="19538">keyframeAnimator</st>`<st c="19554">, which handles the general animations.</st> <st c="19594">We initialized it with the</st> `<st c="19621">AnimationProperties</st>` <st c="19640">struct that holds the properties we want to modify during the animation phases, and we defined that animator to repeat by passing</st> `<st c="19771">true</st>` <st c="19775">in the</st> <st c="19783">corresponding parameter.</st>
2. <st c="19807">The animator has another closure parameter with the content view and the value.</st> <st c="19888">That’s where we can</st> *<st c="19908">modify our view</st>* <st c="19923">according to the animation properties.</st> <st c="19963">In this example, we changed the view opacity</st> <st c="20008">and scale.</st>
3. <st c="20018">Right after the closure, we define our tracks.</st> <st c="20066">We have two properties we want to change over time, so we’ve created two tracks – one for scale and one for opacity.</st> <st c="20183">Because we wanted a</st> *<st c="20203">breathing</st>* <st c="20212">animation, we’ve created two keyframes – one for exhaling (scale down and reduce opacity) and one for inhaling (scale up and</st> <st c="20338">increase opacity).</st>
4. <st c="20356">We can see that each one of the frames is declared as</st> `<st c="20411">CubicKeyframe</st>`<st c="20424">. Before we explain what</st> `<st c="20449">CubicKeyframe</st>` <st c="20462">means, let’s talk about keyframes, which are fundamental concepts</st> <st c="20529">in animations.</st>
<st c="20543">A keyframe specifies an object’s state at a particular point in time.</st> <st c="20614">The animator’s responsibility is to perform the animations between these keyframes.</st> <st c="20698">In a way, it’s like animating a state change, but in this case, we define the different</st> <st c="20786">modifications upfront.</st>
<st c="20808">In the case of SwiftUI’s</st> `<st c="20834">keyframeAnimator</st>`<st c="20850">, the keyframes align with the concept of states – each keyframe defines a change in a specific property</st> <st c="20955">over time.</st>
<st c="20965">In SwiftUI, we have different types of keyframes, each representing a</st> <st c="21036">different experience:</st>
* `<st c="21057">CubicKeyframe</st>`<st c="21071">: This</st> <st c="21079">is the keyframe we used</st> <st c="21103">in our code example.</st> `<st c="21124">CubicKeyframe</st>` <st c="21137">provides</st> <st c="21147">a smooth transition to the next keyframe while computing something called</st> **<st c="21221">Catmull-Rom splines</st>**<st c="21240">. Catmull-Rom splines are curves used in computer animations to provide</st> <st c="21312">smooth movement.</st>
* `<st c="21328">SpringKeyframe</st>`<st c="21343">: This</st> <st c="21351">represents a transition</st> <st c="21375">that emulates a spring experience, including a</st> <st c="21422">bouncy effect.</st>
* `<st c="21436">MoveKeyframe</st>`<st c="21449">: This</st> <st c="21457">type of keyframe modifies</st> <st c="21483">the given</st> <st c="21493">value immediately.</st>
* `<st c="21511">LinearKeyframe</st>`<st c="21526">: This</st> <st c="21534">keyframe animates</st> <st c="21552">the change without a defined curve and, instead, does that in a simple</st> <st c="21623">linear interpolation.</st>
<st c="21644">SwiftUI is</st> <st c="21656">intelligent enough to smoothly handle the combination of different keyframes on the same track.</st> <st c="21752">For example, let’s see what happens when we define velocity on one of</st> <st c="21822">our keyframes:</st>
CubicKeyframe(0.5, duration: 0.2,
CubicKeyframe(0.7, duration: 0.5)
<st c="21944">We can see that the end velocity of the first keyframe is</st> `<st c="22003">0.8</st>`<st c="22006">. However, we haven’t defined any initial velocity for the second keyframe.</st> <st c="22082">In this case, the second keyframe’s</st> `<st c="22118">startVelocity</st>` <st c="22131">value will be the end value of the previous keyframe, which</st> <st c="22192">means</st> `<st c="22198">0.8</st>`<st c="22201">.</st>
<st c="22202">Now, let’s discuss another crucial aspect of keyframe animations –</st> <st c="22270">animation duration.</st>
<st c="22289">Handling keyframe animation duration</st>
<st c="22326">The keyframe animator is a hierarchal structure with three levels – the animator, the tracks, and the keyframe.</st> <st c="22439">This means that different keyframes can have different durations, and these duration values don’t always add up nicely.</st> <st c="22559">That makes duration management complex, especially for long and</st> <st c="22623">intricate animations.</st>
<st c="22644">How do we</st> <st c="22655">ensure that all the keyframe durations are always aligned with each other and maintain the same scale?</st> <st c="22758">The answer is to use relative duration, not</st> <st c="22802">absolute duration.</st>
<st c="22820">An absolute duration specifies the exact time an animation should take, regardless of the initial state, or without comparing it to the</st> <st c="22957">other keyframes.</st>
<st c="22973">Conversely, relative duration reflects the duration time, considering the total animation duration.</st> <st c="23074">For example, if the relative duration is 0.5 and the total animation duration is 3 seconds, the actual keyframe duration would be 1.5 (0.5 *</st> <st c="23215">3.0 seconds).</st>
<st c="23228">By using relative duration, we can establish an animation’s overall duration and allocate specific durations for each keyframe, relative to the</st> <st c="23373">total duration.</st>
<st c="23388">Let’s take our “breathing” example and try to implement</st> <st c="23445">relative duration:</st>
Circle()
.foregroundColor(.red)
.frame(width:100, height:100)
.keyframeAnimator(initialValue:
AnimationProperties(), repeating: true) {
content, value in
content
.opacity(value.opacity)
.scaleEffect(value.scale)
} keyframes: { _ in
KeyframeTrack(\.scale) {
CubicKeyframe(0.7, <st c="23795">持续时间: 0.5 *</st>
CubicKeyframe(1.0,
duration: <st c="23851">0.5 * duration</st>)
}
KeyframeTrack(\.opacity) {
CubicKeyframe(0.3, <st c="23916">持续时间: 0.5 *</st>
CubicKeyframe(1.0, <st c="23962">持续时间: 0.5 *</st>
}
}
<st c="23992">In this code example, we have a keyframe animation with two keyframes, similar to our previous example.</st> <st c="24097">The first keyframe handles the scale animation, and the second handles</st> <st c="24168">the opacity.</st>
<st c="24180">We can see</st> <st c="24192">that we have a total duration variable, currently set to</st> `<st c="24249">1.8</st>`<st c="24252">. With each keyframe, we set the duration relative to that value.</st> <st c="24318">In this case, it is</st> `<st c="24338">0.5</st>` <st c="24341">of the total duration, but this can vary from one example</st> <st c="24400">to another.</st>
<st c="24411">Relative duration can help us set a dynamic overall duration time and change it according to our needs, even</st> <st c="24521">at runtime.</st>
<st c="24532">SwiftUI animations are extremely powerful and easy to use, and keyframe animations make them even more powerful by allowing us to build complex animations with multiple steps</st> <st c="24708">and durations.</st>
<st c="24722">However, in many cases, animating views is one of the many challenges that app developers face.</st> <st c="24819">After all, animating simple shapes such as a rectangle or a circle isn’t always what we desire.</st> <st c="24915">So, what about the assets?</st> <st c="24942">Fortunately, the iOS SDK contains a fantastic resource called SF Symbols.</st> <st c="25016">Let’s explore</st> <st c="25030">it now.</st>
<st c="25037">Animating SF Symbols</st>
**<st c="25058">SF Symbols</st>** <st c="25069">is a library that</st> <st c="25088">contains over 5,000 symbols that developers can integrate within their text, using the</st> *<st c="25175">San</st>* *<st c="25179">Francisco</st>* <st c="25188">font.</st>
<st c="25194">Don’t be</st> <st c="25204">confused – SF Symbols are not emojis.</st> <st c="25242">Emojis are meant to express feelings and emotions within text.</st> <st c="25305">Conversely, SF Symbols are excellent replacements for icons that represent states, actions,</st> <st c="25397">and tools.</st>
<st c="25407">Here’s a basic example of displaying a clock alarm symbol with text next</st> <st c="25481">to it:</st>
var body: some View {
HStack { <st c="25519">Image(systemName:</st>
}.font(.system(size: 30))
}
<st c="25613">We can see no surprises here – we use a basic</st> `<st c="25660">Image</st>` <st c="25665">view with the</st> `<st c="25680">systemName</st>` <st c="25690">parameter to provide the</st> <st c="25716">image name.</st>
<st c="25727">As mentioned earlier in this section, there are thousands of symbols available.</st> <st c="25808">To get the full symbols catalog, we need</st> <st c="25849">to download a Mac application called</st> *<st c="25886">SF Symbols</st>* <st c="25896">(what a coincidence, uh?)</st> <st c="25923">from</st> [<st c="25928">https://developer.apple.com/sf-symbols/</st>](https://developer.apple.com/sf-symbols/)<st c="25967">.</st>
<st c="25968">The app is simple to use, as we can see in</st> *<st c="26012">Figure 6</st>**<st c="26020">.2</st>*<st c="26022">:</st>

<st c="27085">Figure 6.2: The SF Symbol Mac app</st>
<st c="27118">By exploring</st> <st c="27132">the SF Symbol app, we can see how the symbols differ from emojis.</st> <st c="27198">They are not only vector illustrations (meaning they can scale to any size) but also built</st> <st c="27289">as layers.</st>
<st c="27299">To understand why the SF symbols contain layers, try to perform a bounce animation using the app.</st> <st c="27398">Doing so lets us see how the layers create a</st> *<st c="27443">sense of depth</st>*<st c="27457">, making them bounce at</st> <st c="27481">different intervals.</st>
<st c="27501">Other than the bounce effect, SF Symbols supports other effects such as pulse, scale, and replace.</st> <st c="27601">We can perform the same animations in our SwiftUI code using the</st> `<st c="27666">symbolEffect</st>` <st c="27678">view modifier:</st>
struct SFSymbolsAnimationView: View {
@State private var animate = false
var body: some View {
HStack {
Image(systemName:
"alarm.waves.left.and.right.fill") <st c="27851">.symbolEffect(.bounce, options: .repeating,</st>
}.font(.system(size: 40))
.onTapGesture {
animate = true
}
}
}
<st c="27987">The</st> `<st c="27992">symbolEffect</st>` <st c="28004">view modifier has several parameters.</st> <st c="28043">The first is the</st> `<st c="28060">effect</st>` <st c="28066">type, the same as those found in the SF Symbol app.</st> <st c="28119">The second parameter is</st> `<st c="28143">options</st>` <st c="28150">– we can make the effect repeat itself or even set</st> <st c="28202">its speed.</st>
<st c="28212">The third</st> <st c="28223">parameter is the</st> `<st c="28240">value</st>` <st c="28245">parameter – the state variable that triggers the animation.</st> <st c="28306">In this case, we trigger the animation by tapping on the</st> `<st c="28363">HStack</st>` <st c="28369">view that contains both the symbol and the</st> <st c="28413">attached text.</st>
<st c="28427">To read more about SF Symbols, it is recommended to visit Apple’s</st> <st c="28494">website:</st> [<st c="28502">https://developer.apple.com/sf-symbols/</st>](https://developer.apple.com/sf-symbols/)<st c="28542">.</st>
<st c="28543">Even though this chapter mainly concerns SwiftUI animations, there is much more to SF Symbols than just animations, such as supporting multiple colors.</st> <st c="28696">Let’s see how we can modify different</st> <st c="28734">symbol colors.</st>
<st c="28748">Modifying symbol colors</st>
<st c="28772">The fact</st> <st c="28782">that SF Symbols are built with different layers helps not only with animation but also with</st> <st c="28874">coloring them.</st>
<st c="28888">Let’s take, for instance, the</st> *<st c="28919">two persons</st>* *<st c="28931">waving</st>* <st c="28937">symbol:</st>
Image(systemName: "person.2.wave.2")
*<st c="28982">Figure 6</st>**<st c="28991">.3</st>* <st c="28993">shows what the symbol</st> <st c="29016">looks like:</st>

<st c="29029">Figure 6.3: The person.2.wave.2 symbol</st>
<st c="29067">We can see two different types of image components – on the one hand, two people, and on the</st> <st c="29161">other hand, their waves.</st> <st c="29186">So, unlike a regular image, we can set one color for the people and another for</st> <st c="29266">the waves.</st>
<st c="29276">Every SF Symbol has a</st> **<st c="29299">primary</st>** <st c="29306">and</st> **<st c="29311">secondary</st>** <st c="29320">color, and SwiftUI knows how to color</st> <st c="29359">it accordingly.</st>
<st c="29374">For example, let’s set a primary color of brown and a secondary color of blue.</st> <st c="29454">We will use the</st> `<st c="29470">foregroundStyle</st>` <st c="29485">view modifier</st> <st c="29500">for that:</st>
Image(systemName: "person.2.wave.2")
.foregroundStyle(.棕色, .蓝色)
<st c="29578">There are symbols that even have a third color, such as in the case of the three-person symbol (</st>*<st c="29675">Figure 6</st>**<st c="29684">.4</st>*<st c="29686">):</st>

<st c="29691">Figure 6.4: The person.3.sequence.fill symbol</st>
<st c="29736">To use the third color, we just need to add one more color as</st> <st c="29799">a parameter:</st>
Image(systemName: "person.3.sequence.fill")
.foregroundStyle(.red, .blue, .<st c="29887">棕色</st>)
<st c="29895">Anyone who has had to manage multi-color icons knows the complexity of supporting different themes and colors, especially when we need to</st> <st c="30033">animate them.</st>
<st c="30046">So, we know</st> <st c="30059">how to add an SF Symbol, animate it nicely, and color it.</st> <st c="30117">However, we can also use vector multi-layer symbols, which is known</st> <st c="30185">as localization.</st>
<st c="30201">Localizing our symbols</st>
<st c="30224">Localizing our apps is a crucial topic today, more than ever.</st> <st c="30287">However, how many of us pay attention</st> <st c="30325">to icon localization and try to adjust them according to the app</st> <st c="30390">layout direction?</st>
<st c="30407">The excellent news about SF Symbols is that they can adjust to the current app locale.</st> <st c="30495">The even better news is that we can force them to do that if</st> <st c="30556">we want.</st>
<st c="30564">But why do SF Symbols even need to</st> <st c="30600">support localization?</st>
<st c="30621">Let’s take the</st> `<st c="30637">arrowshape.turn.up.forward</st>` <st c="30663">SF Symbol (</st>*<st c="30675">Figure 6</st>**<st c="30684">.5</st>*<st c="30686">):</st>

<st c="30691">Figure 6.5: The arrowshap.turn.up.forward SF Symbol</st>
<st c="30742">The forward</st> <st c="30755">icon arrow points to the right, which fits</st> <st c="30798">nicely in</st> **<st c="30808">LTR</st>** <st c="30811">(</st>**<st c="30813">Left-to-Right</st>**<st c="30826">) layout views.</st> <st c="30843">But what about</st> **<st c="30858">RTL</st>** <st c="30861">(</st>**<st c="30863">Right-to-Left</st>**<st c="30876">) layouts, such as in Hebrew or Arabic</st> <st c="30916">localized applications?</st>
<st c="30939">Well, in this case, we will have to flip the icon direction.</st> <st c="31001">With SF Symbol, this adjustment is done automatically</st> <st c="31055">for us.</st>
<st c="31062">Moreover, we can set the icon localization regardless of the view settings, using the</st> `<st c="31149">environment</st>` <st c="31160">view modifier:</st>
Image(systemName: "arrowshape.turn.up.forward")
<st c="31269">In the preceding code, we force the SF Symbol to have an RTL layout direction, which flips the forward arrow to the</st> <st c="31386">left direction.</st>
<st c="31401">Having localization</st> <st c="31422">support doesn’t stop with layout direction.</st> <st c="31466">Some symbols even change their look according to the</st> <st c="31519">current locale.</st>
<st c="31534">For example, let’s take the</st> `<st c="31563">character.book.closed</st>` <st c="31584">SF Symbol (</st>*<st c="31596">Figure 6</st>**<st c="31605">.6</st>*<st c="31607">):</st>

<st c="31612">Figure 6.6: The character.book.closed SF Symbol</st>
<st c="31659">In the case of the symbol in</st> *<st c="31689">Figure 6</st>**<st c="31697">.6</st>*<st c="31699">, we can see that in addition to its layout direction (LTR), it also has a letter</st> <st c="31781">on it.</st>
<st c="31787">In the case of the Hebrew locale, not only does the symbol’s direction change but also the letter (</st>*<st c="31887">Figure 6</st>**<st c="31896">.7</st>*<st c="31898">):</st>

<st c="31903">Figure 6.7: The character.book.closed SF Symbol in a Hebrew locale</st>
<st c="31969">We can force the symbol to retrieve a specific locale using the</st> `<st c="32034">environment</st>` <st c="32045">view modifier, similar to the</st> <st c="32076">layout direction:</st>
Image(systemName: "character.book.closed")
<st c="32184">To sum up, SF Symbols</st> <st c="32207">contain so much power and valuable features.</st> <st c="32252">Trying to support standard icons in different environments, such as locales and themes, can be a hassle, and animating them without creating a dedicated image sequence is almost impossible.</st> <st c="32442">So, getting all these features for free is l</st><st c="32486">ike a massive present from</st> <st c="32514">Apple engineers.</st>
<st c="32530">Summary</st>
<st c="32538">iOS animations are like salt – they can enhance the user experience, but too much</st> <st c="32621">is overwhelming.</st>
<st c="32637">The great thing about SwiftUI animations is that they are aligned to the screen state because of the declarative implementation.</st> <st c="32767">However, it’s a significant change to how they work</st> <st c="32819">in UIkit.</st>
<st c="32828">Because of that, in this chapter, we went from understanding the basic concepts and performing fundamental animations to custom transitions and keyframe animations, and we even discussed a great present that Apple gave us,</st> <st c="33052">SF Symbols.</st>
<st c="33063">Now, we should be able to easily animate changes on our screen in a meaningful and</st> <st c="33147">expressive way!</st>
<st c="33162">In our next chapter, we’ll explore enhancing user engagement using a built-in solution –</st> <st c="33252">TipKit.</st>
第八章:7
使用 TipKit 改进功能探索
在上一章中,我们学习了 SwiftUI 动画。
-
在移动应用中学习提示的重要性 -
添加新的提示——内联 和弹出视图 -
自定义我们的提示的感觉 和外观 -
支持 提示操作 -
为我们的提示定义显示规则 我们的提示 -
使用 TipGroup 对提示进行 分组 -
调整 显示频率
技术要求
学习提示的重要性
理解提示框架的基础

-
确保提示在用户取消或删除后不再显示 的 -
触发提示,以确保提示不会相互重叠 的 -
根据 特定规则 显示提示
提示是什么样子?

-
内联技巧 :内联技巧会嵌入到屏幕布局中,其出现会相应地修改和推动其他视图。 内联技巧 非常适合 VStacks 或 Lists ,我们可以在不 干扰屏幕交互 的情况下查看它们。 -
弹出提示 :与内联提示不同,弹出提示出现在当前屏幕上方,通常与按钮或其他控件相关联。 使用弹出提示时,用户必须关闭提示或执行其操作才能继续使用应用程序。 此外,我们无法同时显示多个弹出提示。
添加我们的第一个技巧
configure()
import TipKit
@main
struct MyApp: App {
init() { <st c="6439">try?</st> <st c="6444">Tips.configure()</st> }
}
<st c="6514">configure</st>
try? Tips.configure([
.<st c="6794">datastoreLocation</st>(.groupContainer(identifier:
"MyAppGroupContainer"))])
<st c="6954">MyAppGroupContainer</st>
<st c="7262">T</st><st c="7263">ip</st>
定义我们的提示模型
<st c="7300">Tip</st>
struct MarkAsFavoriteTip: Tip {
var id: String { "InlineTipView"}
var title: Text {
Text("Save as a Favorite")
}
var message: Text? {
Text("You can mark items as Favorite and add them
to your favorites list.")
}
var image: Image? {
Image(systemName: "star")
}
}
<st c="7730">MarkAsFavoriteTip</st><st c="7771">Tip</st> <st c="7801">MarkAsFavoriteTip</st>

<st c="8146">MarkAsFavoriteTip</st>
struct InlineTipView: View { <st c="8250">var tip = MarkAsFavoriteTip()</st> var body: some View {
VStack { <st c="8311">TipView(tip)</st> List(workouts) { workout in
WorkoutView(workout: workout)
}
}
}
}
<st c="8528">MarkAsFavoriteTip</st> <st c="8580">TipView</st>

<st c="9191">VStack</st>
添加弹出提示
<st c="9735">popoverTip</st><st c="9773">tip</st>
struct PopoverTipView: View { <st c="9872">var tip = PopoverTip()</st> var body: some View {
List {
// some list information
}
.navigationTitle("Popover Tip")
.toolbar(content: {
Button("Settings", systemImage: "gearshape") {
}
.buttonStyle(.plain) <st c="10073">.popoverTip(tip, arrowEdge: .top)</st> })
}
}

忽略提示
<st c="11710">invalidate()</st>
List(workouts) { workout in
WorkoutView(workout: workout,
onFavoriteButtonTap: { <st c="11814">tip.invalidate(reason:</st>
<st c="11836">.actionPerformed)</st> })
}
<st c="11893">invalidate()</st>
<st c="12201">actionPerformed</st>
定义提示 ID
var id: String { "InlineTipView"}
<st c="12707">id</st> <st c="12734">Tip</st>
<st c="13350">resetDatastore</st>
struct MyApp: App {
init() { <st c="13418">try?</st> <st c="13423">Tips.resetDatastore()</st> try? Tips.configure()
}
}
注意,我们在<st c="13530">configure</st>函数之前调用了<st c="13495">resetDatastore</st>函数。
提示标识符是<st c="13584">Tip</st>协议的一部分,在这个例子中,标识符在所有<st c="13659">struct</st>实例之间共享:
var id: String { "InlineTipView"}
由于标识符是共享的,一旦你使其中一个无效,基于<st c="13774">struct</st>实例的所有提示视图都将关闭。
在大多数情况下,这被认为是正常行为和最佳实践。如果用户将特定行标记为收藏夹,他们已经了解这个功能,即使它出现在另一个屏幕上。
然而,情况并不总是如此,因此相应地规划标识符。
现在我们知道了如何展示提示,无论是内联还是弹出式。我们也知道如何关闭它,甚至重置持久状态。然而,TipKit 提供了更多功能。让我们看看我们如何自定义我们的提示。
自定义我们的提示
因此,TipKit 为我们应用中展示基于持久性的提示提供了一个优秀的基础设施。然而,TipKit 框架的开发者知道,处理提示需要比仅仅用图像和两个文本使普通视图无效更多的思考。
让我们看看我们如何根据我们的需求自定义提示。我们将从它们的显示风格开始。
自定义我们的提示显示风格
与苹果提供的许多其他基于 UI 的框架不同,TipKit 允许我们很好地自定义提示视图。这可能是因为 SwiftUI 是一个声明性框架,表达视觉内容变得更加自然。然而,在 TipKit 的情况下,苹果理解开发者将 TipKit 设计与其应用程序对齐的需求。
有两种方式可以自定义提示的显示风格。第一种是修改提示的属性,应用基本更改而不改变提示的布局和组件。
第二种方式是实现一个新的提示视图样式,这允许你完全控制提示的感觉和外观。让我们从第一种方式开始:修改提示属性。
修改提示属性
如我之前所述,SwiftUI 的其中一个优点是其表达性框架,我们可以使用视图修饰符来调整提示的显示风格以符合我们的风格。
让我们再次看看提示的<st c="15605">title</st> <st c="15610">属性</st>:
var title: Text { Text("Save as a Favorite") }
注意,我们返回的不是<st c="15709">String</st> <st c="15715">而是</st> <st c="15722">Text</st> <st c="15726">值</st>,这是一个 SwiftUI 视图。<st c="15759">这意味着我们可以修改其外观,使其看起来像任何其他</st> <st c="15816">SwiftUI 视图</st>。
例如,我们可以通过应用<st c="15894">foregroundStyle</st> <st c="15909">视图修饰符</st>来改变标题文本颜色:
var title: Text {
Text("Save as a Favorite") <st c="15970">.foregroundStyle(.red)</st> }
代码示例很简单:我们取了文本视图并改变了其外观。<st c="16075">此外,因为我们可以通过组合多个文本视图来构建一个</st> <st c="16108">Text</st> <st c="16112">视图,所以我们可以混合样式和颜色</st> <st c="16172">:</st>。
var title: Text {
Text("Save as a ") <st c="16221">.fontWeight(.light)</st> +
Text("Favorite") <st c="16260">.fontWeight(.bold)</st>
<st c="16278">.foregroundStyle(.red)</st> }
在这个例子中,我们取了我们的<st c="16331">Save as a Favorite</st> <st c="16349">文本</st>并将<st c="16371">Favorite</st> <st c="16379">文本</st>改为红色和粗体,以区分它与其他<st c="16439">标题</st>。
我们也可以对<st c="16449">Image</st> <st c="16485">属性</st>进行修改,例如改变其颜色或<st c="16532">渲染模式</st>:
var image: Image? {
Image(systemName:
"externaldrive.fill.badge.icloud") <st c="16621">.symbolRenderingMode(.multicolor)</st> }
在*<st c="16659">第六章</st>*中,我们了解到 SF 符号有多个层级,这样我们就可以将不同的颜色应用到不同的层级。在这个例子中,我们将我们的符号的渲染模式改为<st c="16842">多色</st> <st c="16852">。</st>。
修改提示属性是给我们的提示视图用户界面添加基本触感的好方法。然而,我们知道设计在 iOS 应用中是多么关键,有时,仅仅改变颜色和字体样式是不够的。因此,我们可以使用<st c="17096">TipViewStyle</st> <st c="17108">进行</st> <st c="17113">进一步定制</st>。
使用<st c="17135">TipViewStyle</st>
给定的提示视图设计仅在我们需要不同的 UI 布局或更复杂的提示视图时才有效。因此,我们必须考虑不同的设计模式来满足该需求。
我最喜欢提到的最重要的开发原则之一是关注点分离原则——即不同的组件应该有不同的责任。
当我们查看<st c="17571">Tip</st> <st c="17574">协议</st>的工作方式时,一些责任被混合在一起。一方面,Tip 协议结构定义了我们的提示内容——标题、消息和图像。另一方面,结构也定义了其设计,这可能是不同的责任。
Button("Sign In", action: signIn) <st c="18326">Button</st>) but apply a specific style.
<st c="18363">In TipKit, we can also define our tip appearance by applying a custom</st> <st c="18434">view style:</st>
struct ImageAtTheCornerViewStyle: TipViewStyle {
VStack {
if let title = configuration.title, let message
= configuration.message {
title
.multilineTextAlignment(.center)
.font(.title2) <st c="18691">分隔符()</st> message
.multilineTextAlignment(.leading)
.font(.body)
}
HStack {
Spacer()
Image(systemName: "star")
}
.padding()
}
}
}
<st c="18820">The View</st> <st c="18830">Style we just created takes a</st> `<st c="18860">Tip</st>` <st c="18863">view and returns a new view with the same content but a different layout and design.</st> <st c="18949">It even adds a new view component, such as a</st> `<st c="18994">Divider</st>` <st c="19001">and</st> `<st c="19006">Spacer</st>` <st c="19012">component.</st> <st c="19024">The magic happens in the</st> `<st c="19049">makeBody</st>` <st c="19057">function, which receives a</st> `<st c="19085">Configuration</st>` <st c="19098">parameter that contains all the</st> <st c="19131">tip information.</st>
<st c="19147">To apply our new View Style on a tip, we can use the</st> `<st c="19201">tipViewStyle</st>` <st c="19213">method:</st>
TipView(tip)

<st c="19466">图 7.6:使用 TipViewStyle 自定义我们的提示</st>
<st c="19515">命名</st> <st c="19524">`TipViewStyle` <st c="19550">协议时使用一个通用且描述性的名称,例如</st> `<st c="19616">ImageAtTheCornerViewStyle</st>`<st c="19641">,这样将更容易与我们的其他提示共享。</st> <st c="19693">。</st>
<st c="19702">到目前为止,我们已经学习了如何定义提示、在不同位置展示它以及如何设计它。</st> <st c="19812">然而,我们的提示丰富之旅并没有结束,因为我们还可以通过添加</st> <st c="19920">操作</st> **<st c="19927">来添加一些用户交互**<st c="19934">。</st>
<st c="19935">添加操作</st>
<st c="19950">操作是提示信息中非常有价值的补充。</st> <st c="19996">在许多情况下,我们的提示建议用户采取</st> <st c="20047">行动——例如,转到设置屏幕、添加新任务或进入我们应用的新编辑模式。</st> <st c="20149">在提示视图中添加一个执行该特定操作的按钮不是更好吗?</st>
<st c="20239">除了标题、消息和图像外,提示协议还包含一个操作属性——一个描述提示将显示的按钮的结构数组。</st>
<st c="20399">让我们通过一个例子来看看这个属性:</st> <st c="20427">示例:</st>
struct ChangeEmailTip: Tip { <st c="20468">var actions: [Action] {</st>
<st c="20491">Action(id: "go-to-settings", title: "Go to</st>
<st c="20534">settings")</st>
<st c="20545">Action(id: "change-now", title: "Change email now")</st>
<st c="20597">}</st> }
<st c="20601">代码示例展示了</st> `<st c="20626">ChangeEmailTip</st>` <st c="20640">结构包含两个操作。</st> <st c="20669">(注意这个提示是部分展示的;假设我们已经实现了其余的属性,例如</st> `<st c="20774">标题</st>` <st c="20779">和</st> `<st c="20784">消息</st>`<st c="20791">。) </st>
<st c="20793">每个操作初始化函数有两个参数:</st> `<st c="20850">标题</st>` <st c="20855">和</st> `<st c="20860">id</st>`<st c="20862">。 `<st c="20868">标题</st>` <st c="20873">参数</st> <st c="20884">表示按钮上显示的标题。</st> `<st c="20933">id</st>` <st c="20937">参数描述了这个操作的目标,我们用它来确定用户点击了哪个按钮。</st>
*<st c="21041">图 7</st>**<st c="21050">.7</st>* <st c="21052">展示了操作在弹出提示中的外观:</st>

<st c="21222">图 7.7:弹出提示视图中的两个操作</st>
<st c="21267">与其它属性一样,TipKit 决定如何布局显示操作,以及按钮的外观。</st> <st c="21366">按钮的外观。</st>
<st c="21379">现在我们已经定义并展示了操作,让我们看看我们如何响应用户的选择。</st> <st c="21476">现在我们为每个操作都有一个 ID,响应用户的选择变得容易了。</st> <st c="21548">我们在</st> *<st c="21597">添加弹出提示</st> * <st c="21617">部分讨论的</st> `<st c="21552">popoverTip</st>` <st c="21562">视图修饰符有一个额外的闭包来处理操作选择。</st> <st c="21683">让我们看看一个代码示例:</st> <st c="21708">来展示这个:</st>
Button("Settings", systemImage: "gearshape") {
gotoSettings = true
}
.buttonStyle(.plain)
.popoverTip(tip, arrowEdge: .top) <st c="21842">{ action in</st>
<st c="21853">if action.id == "go-to-settings" {</st>
<st c="21888">gotoSettings = true</st>
<st c="21908">}</st>
<st c="21910">}</st>
<st c="21912">这个代码示例展示了精确的弹出提示实现,现在包含处理</st> <st c="22008">所选操作</st> <st c="22029">的闭包。</st> <st c="22029">在闭包内部,我们检查操作 ID 并执行所需操作(例如,导航到设置屏幕)。</st>
<st c="22154">将这些 ID 添加到静态常量中更清晰:</st>
struct ChangeEmailTip: Tip {
// rest of the tip
var actions: [Action] {
Action(id: <st c="22304">ChangeEmailTip.goToSettingsAction</st>,
title: "Go to settings")
Action(id: ChangeEmailTip.changeEmailAction, title:
"Change email now")
}
<st c="22438">static let goToSettingsAction = "go-to-settings"</st> static let changeEmailAction = "change-now"
}
…
.popoverTip(tip, arrowEdge: .top) { action in
if action.id == <st c="22597">ChangeEmailTip.goToSettingsAction</st> {
gotoSettings = true
}
}
<st c="22656">这个代码示例展示了在应用</st> <st c="22722">最佳实践</st> <st c="22737">时 Swift 可以多么美丽!</st>
<st c="22737">说到美丽,我们讨论了如何使用</st> `<st c="22803">TipViewStyle</st>`<st c="22815">来设计我们的提示,因此我们也可以使用相同的技巧来设计我们的操作:</st> <st c="22861">以下是一个代码示例:</st>
List(<st c="22882">configuration.actions</st>) { action in
Button(action:{
// perform action
}) {
action.label()
}
}
<st c="22976">在这个代码示例中,我们创建了一个按钮列表,每个按钮处理不同的操作。</st> <st c="23062">我们需要将这段代码添加到</st> `<st c="23102">makeBody</st>` <st c="23110">函数中,我们在</st> *<st c="23144">使用</st> * *<st c="23150">TipViewStyle</st> * <st c="23162">部分学到的。</st>
<st c="23171">到目前为止,我们已经学到了很多关于提示的知识!</st> <st c="23221">好消息是我们还有更多惊喜。</st> <st c="23282">让我们揭示它们并讨论**<st c="23316">规则</st>**<st c="23321">功能。</st>
<st c="23330">添加提示规则</st>
<st c="23348">在本章中,我们迄今为止主要关注提示显示的 UI 方面。</st> <st c="23439">然而,我们已经知道提示不仅仅是美观的视图——它们必须与某些应用逻辑或状态相对应。</st> <st c="23527">例如,也许当用户登录时,我们会展示一些提示。</st> <st c="23556">在照片应用中,当用户拍摄了一定数量的照片后,我们可以显示一个提示,建议添加相册。</st>
<st c="23743">提示必须经常让用户意识到他们的流程和状态。</st> *<st c="23763">让用户意识到</st>** <st c="23773">用户的状态。</st> <st c="23802">这就是为什么 TipKit 还包含一个名为规则的功能。</st>
<st c="23857">有两种规则类型:</st>
+ **<st c="23883">基于状态</st>**<st c="23898">:根据特定状态显示或隐藏提示。</st> <st c="23949">用户登录,执行特定操作,</st> <st c="24000">等等。</st>
+ **<st c="24009">事件跟踪</st>**<st c="24025">:根据用户执行的事件数量显示或隐藏提示。</st> <st c="24099">例如,如果用户在过去一周内多次进入设置中的特定屏幕,我们可以为他们提供创建该屏幕的快捷方式。</st> <st c="24245">的屏幕。</st>
<st c="24257">让我们从添加基于状态的规则开始。</st>
<st c="24306">添加基于状态的规则</st>
<st c="24337">基于状态创建规则</st> <st c="24353">是建立提示显示逻辑的常见方法。</st> <st c="24401">什么是状态?</st> <st c="24420">状态可以是一个认证状态(用户是否已登录?),解锁商品,功能使用,</st> <st c="24535">等等。</st>
<st c="24544">实现基于状态的规则有三个步骤:</st>
1. **<st c="24616">添加参数</st>**<st c="24635">:我们需要添加一个规则将基于的变量。</st>
1. **<st c="24696">定义规则</st>**<st c="24712">:规则在提示内部定义,应考虑我们讨论的参数。</st>
1. **<st c="24799">将参数连接到应用逻辑</st>**<st c="24838">:如果我们想让规则基于我们应用的真正状态,我们需要维护并与其同步应用状态。</st>
<st c="24956">信不信由你,基于规则的提示实现甚至比看起来还要简单!</st> <st c="25036">让我们尝试构建一个提示,鼓励我们的用户使用仅限高级功能的操作,比如更改应用主题。</st> <st c="25134">应用主题。</st>
<st c="25144">添加参数</st>
<st c="25163">规则需要依赖于应用可以轻松修改的持久数据来跟踪应用状态。</st> <st c="25260">为了做到这一点,我们使用</st> <st c="25283">@parameter</st> <st c="25293">宏将跟踪状态变量添加到</st> <st c="25336">我们的提示中。</st>
<st c="25344">宏是什么?</st>
<st c="25361">宏是</st> <st c="25373">Swift 的一个特性,它帮助编译器根据当前代码和参数生成代码。</st> <st c="25469">您可以在</st> *<st c="25498">第十章</st>*<st c="25508">中了解更多关于宏的信息。</st>
<st c="25509">让我们添加一个名为</st> `<st c="25539">isPremiumUser</st>` <st c="25552">的参数来跟踪</st> <st c="25562">高级资格:</st>
struct ChangeAppThemeTip: Tip {
// rest of the tip implementation <st c="25649">@Parameter</st> static var isPremiumUser: Bool = false
}
<st c="25700">展开宏可以看到一个</st> <st c="25731">简单的实现:</st>
static var $isPremiumUser: Tips.Parameter<Bool> =
Tips.Parameter(Self.self, "+isPremiumUser", false)
{
get {
$isPremiumUser.wrappedValue
}
set {
$isPremiumUser.wrappedValue = newValue
}
}
<st c="25941">让我们深入了解宏的实现。</st> <st c="25985">由于 TipKit 想要与通用类型一起工作,宏创建了一个名为</st> `<st c="26075">$isPremiumUser</st>` <st c="26089">的变量,它是</st> `<st c="26097">Tips</st>` <st c="26103">参数</st> <st c="26112">类型(基于</st> `<st c="26128">Bool</st>`<st c="26132">)的,并且默认值为</st> `<st c="26158">false</st>` <st c="26163">(如我们最初在静态变量中定义的那样)。</st>
<st c="26210">该宏</st> <st c="26221">还创建了一个</st> **<st c="26236">获取器</st>** <st c="26242">和一个</st> **<st c="26249">设置器</st>** <st c="26255">,这样我们的提示就可以响应应用</st> <st c="26286">状态变化。</st>
<st c="26300">然而,宏处理了另一件有助于我们的事情:使我们的参数值</st> **<st c="26384">持久化</st>**<st c="26394">。在这种情况下,对于“用户是否是高级用户?”这个问题,答案可能已经是持久的了。</st> <st c="26491">然而,有些情况并不那么明显。</st> <st c="26545">例如,功能使用跟踪通常不是</st> <st c="26588">持久的。</st>
<st c="26608">现在我们有了参数,让我们添加第一个</st> <st c="26658">显示规则。</st>
<st c="26671">定义我们的显示规则</st>
<st c="26698">我们是在定义显示“规则”</st> <st c="26732">(复数形式)吗?
<st c="26742">是的!</st> <st c="26748">TipKit</st> <st c="26755">支持多个显示规则以支持更复杂的情况。</st> <st c="26823">然而,首先,让我们从</st> <st c="26856">一个提示</st> <st c="26863">开始:</st>
struct ChangeAppThemeTip: Tip {
@Parameter
static var isPremiumUser: Bool = false <st c="26947">var rules: [Rule] {</st>
<st c="26966">[</st>
<st c="26968">#Rule(Self.$isPremiumUser) {</st>
<st c="26997">$0 == true</st>
<st c="27008">}</st>
<st c="27010">]</st>
<st c="27012">}</st> }
<st c="27016">在这个代码示例中,我们使用宏创建了一个名为</st> `<st c="27081">Rule</st>` <st c="27085">的数据类型,它包含一个谓词表达式。</st> <st c="27124">该谓词表达式将给定的类型与一个</st> <st c="27179">特定值进行比较。</st>
<st c="27194">在这种情况下,我们比较了</st> `<st c="27224">$isPremiumUser</st>` <st c="27238">的值</st> <st c="27245">与</st> `<st c="27248">true</st>`<st c="27252">。</st>
<st c="27253">现在,让我们回到</st> <st c="27268">rules</st> <st c="27285">变量。</st> <st c="27296">我们可以添加更多支持我们的提示显示逻辑的规则。</st> <st c="27354">TipKit 在不同的提示之间执行一个</st> `<st c="27373">AND</st>` <st c="27376">运算符,如果结果是 true,则显示提示(除非用户或应用显然将其关闭)。</st>
<st c="27520">我们如何修改规则所依据的值?</st> <st c="27571">让我们看看。</st>
<st c="27581">将参数连接到我们的应用逻辑</st>
<st c="27623">我们需要将提示参数连接到我们的应用逻辑以完成我们的工作。</st> <st c="27701">注意参数</st> <st c="27727">是一个静态变量。</st> <st c="27749">这意味着我们可以从我们的应用的任何地方修改它,即使我们没有提示实例的引用。</st>
<st c="27863">让我们看看一个重要的</st> <st c="27887">参数修改:</st>
let tip = ChangeAppThemeTip()
var body: some View {
VStack {
Button("Change isPremium parameter") { <st c="28011">ChangeAppThemeTip.isPremiumUser.toggle()</st> }
TipView(tip)
}
}
<st c="28070">此代码示例展示了具有一个切换静态</st> `<st c="28144">isPremiumUser</st>` <st c="28157">变量(我们在之前的提示中创建)的按钮的基本 UI。</st> <st c="28205">切换该值也会在 VStack 中显示和隐藏</st> `<st c="28250">TipView</st>` <st c="28257">视图。</st>
<st c="28279">然而,添加一个切换提示的按钮并不是使用规则参数的真实世界示例。</st> <st c="28379">一个更实际的例子是将它直接连接到用户的付费状态,使用一个</st> `<st c="28472">Combine</st>` <st c="28479">流 – 类似于以下代码:</st>
let premiumManager = PremiumPurchaseManager()
let premiumStatusSubscription =
premiumManager.premiumPurchasePublisher <st c="28642">.assign(to: \.isPremiumUser, on:</st>
<st c="28796">isPremiumUser</st> parameter. This is a more elegant way to link the rule logic to our app.
<st c="28882">Now let’s discuss the other type of rules –</st> <st c="28927">events.</st>
<st c="28934">Adding a rule based on events</st>
<st c="28964">When we display a tip based on a state, it’s usually only displayed when the user can use a particular</st> <st c="29068">feature.</st> <st c="29077">However, there are cases when we want to display</st> <st c="29126">a tip when we think the user is ready to take our app to the following</st> <st c="29197">usage level.</st>
<st c="29209">For example, if we create a music app and the user adds a few songs, maybe it’s a good idea to tell them about making a playlist.</st> <st c="29340">Or, if we are working on a dating app, maybe it is worth suggesting modifying the search filter if the user hasn’t chosen any of the</st> <st c="29473">profiles viewed.</st>
<st c="29489">For these types of tips, we can create a rule based on tracking events.</st> <st c="29562">The idea is to define an event representing the user’s relevant action.</st> <st c="29634">For example, I can add a task, view a profile, and more.</st> <st c="29691">Afterward, we create a rule based on the number of events tracked within a time frame</st> <st c="29777">or generally.</st>
<st c="29790">Let’s see a code example for a tip suggesting the user add a list of to-dos.</st> <st c="29868">We’ll start by defining</st> <st c="29892">our tip:</st>
struct AddListTip: Tip {
}
<st c="30065">The tip goal is to suggest the user add to a list of to-dos.</st> <st c="30127">We create an event called</st> `<st c="30153">didAppTaskEvent</st>` <st c="30168">that helps us track the number of times the user adds a</st> <st c="30225">new to-do.</st>
<st c="30235">The second thing</st> <st c="30253">we do here is to create a new rule that returns</st> `<st c="30301">true</st>` <st c="30305">if the number of tracked events</st> <st c="30338">exceeds three.</st>
<st c="30352">This is a different</st> <st c="30373">rule constructor that handles event tracking instead of</st> <st c="30429">a state.</st>
<st c="30437">The last piece of the puzzle shows the tip and track of</st> <st c="30494">an event:</st>
struct EventRuleTipExample: View {
let tip = AddListTip()
@State var todos: [Todo] = []
var body: some View {
VStack {
TipView(tip)
List(todos) { todo in
Text(todo.title)
}
Spacer()
Button("添加任务") {
todos.append(Todo(title: "新建任务")) <st c="30745">任务{ await</st>
}
}
<st c="30802">The event</st> <st c="30813">tracking operation is referred to as</st> `<st c="30850">donate()</st>`<st c="30858">, while the</st> <st c="30870">total number of tracked events is known</st> <st c="30910">as</st> **<st c="30913">donations</st>**<st c="30922">.</st>
<st c="30923">We can also</st> <st c="30936">check for events tracked in a specific</st> <st c="30975">time range:</st>
$0.donations.donatedWithin(.days(3)).count > 3
$0.donations.donatedWithin(.week).count < 3
<st c="31077">This example checks whether the number of events exceeds three in the last three days or</st> <st c="31167">one week.</st>
<st c="31176">Now, it’s important to distinguish between the number of events tracked and just checking the database for the number</st> <st c="31295">of to-dos.</st>
<st c="31305">We could easily check the user’s number of to-dos in their database and change that to a state-based rule.</st> <st c="31413">However, this solves a different use case – not the number of times the user added a task with the app, but rather the number of tasks the user has</st> <st c="31561">in general.</st>
<st c="31572">Grouping tips with TipGroup</st>
<st c="31600">When our app becomes more extensive and feature-rich, handling a large set of tips can become</st> <st c="31695">cumbersome.</st> <st c="31707">Trying to coordinate all these tips using rules can lead to a situation wherein tips appear outside the intended order and</st> <st c="31830">even together.</st>
<st c="31844">To address that, we can use the</st> `<st c="31877">TipGroup</st>` <st c="31885">class to group tips and present them individually in a</st> <st c="31941">particular order.</st>
<st c="31958">Let’s see an example for a</st> `<st c="31986">TipGroup</st>` <st c="31994">class usage:</st>
Button("设置") {
}.popoverTip(<st c="32128">tips.currentTip</st>)
}
<st c="32148">In this example, we created a state variable called</st> `<st c="32201">tips</st>` <st c="32205">of the TipGroup type.</st> <st c="32228">We passed</st> `<st c="32238">.ordered</st>` <st c="32246">for its priority parameter and added two tips using its builder.</st> <st c="32312">In the code itself, we attached our</st> `<st c="32348">TipGroup</st>` <st c="32356">instance to a button using the</st> `<st c="32388">popoverTip</st>` <st c="32398">view modifier, passing the group’s</st> <st c="32434">current tip.</st>
<st c="32446">Using the .</st>`<st c="32458">ordered</st>` <st c="32466">parameter ensures that the tips will appear in the order in which we added them to the builder.</st> <st c="32563">TipKit will show the next tip once all the previous tips have</st> <st c="32625">been invalidated.</st>
<st c="32642">The other parameter we can use is</st> `<st c="32677">firstAvailable</st>`<st c="32691">, which shows the next tip that is eligible</st> <st c="32735">for display.</st>
<st c="32747">Grouping tips together can help manage a large collection of tips in our project.</st> <st c="32830">However, looking at the code example again, we can see that there might be a problem with the way we implemented the TipGroup in the view.</st> <st c="32969">Imagine we have a TipGroup with a</st> `<st c="33003">SettingsTip</st>` <st c="33014">type and a</st> `<st c="33026">ProfileTip</st>` <st c="33036">type.</st> <st c="33043">When using the TipGroup for settings and profile buttons, we can’t control which tip</st> <st c="33128">appears where.</st>
<st c="33142">To solve</st> <st c="33152">that, we can cast the</st> `<st c="33174">currentTip</st>` <st c="33184">variable to the desired tip type.</st> <st c="33219">Let’s see that in the</st> <st c="33241">following code:</st>
@State var tips = TipGroup(.ordered) {
SettingsTip()
ProfileTip()
}
var body: some View {
Button("设置") {
}.popoverTip(<st c="33381">tips.currentTip as?</st> <st c="33402">SettingsTip</st>)
Button("个人资料") {
}.popoverTip(<st c="33449">tips.currentTip as?</st> <st c="33470">ProfileTip</st>)
}
<st c="33484">In this code example, we have a TipGroup with two tips – for the settings button and for the</st> <st c="33578">profile button.</st>
<st c="33593">When we use the</st> `<st c="33610">popoverTip</st>` <st c="33620">view builder, we cast the</st> `<st c="33647">currentTip</st>` <st c="33657">instance to the corresponding type according to the button.</st> <st c="33718">This technique takes advantage of how the</st> `<st c="33760">popoverTip</st>` <st c="33770">signature looks:</st>
public func popoverTip(_ tip: (any Tip)?...)
<st c="33832">Since</st> `<st c="33839">popoverTip</st>` <st c="33849">accepts</st> `<st c="33858">nil</st>` <st c="33861">as an argument, we can ensure that only relevant tips will appear from</st> <st c="33933">the TipGroup.</st>
<st c="33946">Rules are only one aspect of defining the appearance logic.</st> <st c="34007">Another crucial element is determining its frequency.</st> <st c="34061">Let’s see how to customize that</st> <st c="34093">as well.</st>
<st c="34101">Customizing display frequency</st>
<st c="34131">I</st><st c="34133">n the previous section, we discussed creating display logic for our tips using rules and tip groups.</st> <st c="34234">However, tips can overwhelm users; there’s a fine line between helping the user and</st> <st c="34318">disturbing them.</st> <st c="34335">Adjusting all the rules to set a reasonable limit on the number of tips the user sees can be challenging.</st> <st c="34441">For that problem, we can manage the frequency at which our</st> <st c="34500">tips display.</st>
<st c="34513">Let’s start with setting the max display count for</st> <st c="34565">a tip.</st>
<st c="34571">Setting the max display count for a specific tip</st>
<st c="34620">The first and essential thing we can do is set the maximum number of a specific tip type that can</st> <st c="34719">be displayed.</st>
<st c="34732">We do</st> <st c="34739">that by adding a new variable to our tip</st> <st c="34780">called</st> `<st c="34787">options</st>`<st c="34794">:</st>
struct AddListTip: Tip {
var options: [TipOption] { <st c="34849">最大显示次数(2)</st> }
}
<st c="34876">In this code example, we use the</st> `<st c="34910">MaxDisplayCount</st>` <st c="34925">static function of the</st> `<st c="34949">Tips</st>` <st c="34953">namespace.</st> <st c="34965">That definition means that the tip will be displayed a maximum of two times, and afterward, it will be invalidated, overriding the rest of the rule’s logic.</st> <st c="35122">That’s a great way to ensure that a specific tip doesn’t</st> <st c="35179">overwhelm users.</st>
<st c="35195">However, there’s another excellent way to ensure a calmer user experience:</st> <st c="35271">display frequency.</st>
<st c="35289">Setting our tips’ display frequency</st>
<st c="35325">We just learned how to limit a particular tip to a certain number of appearances.</st> <st c="35408">Another</st> <st c="35416">way to handle tip appearance is to define</st> <st c="35458">its frequency.</st>
<st c="35472">Let’s look at the</st> <st c="35491">following code:</st>
struct MyApp: App {
init() { <st c="35536">try?</st> <st c="35541">配置显示频率为每日</st> }
}
<st c="35588">The code example shows how we can limit the total number of tips displayed to one</st> <st c="35671">per day.</st>
<st c="35679">The</st>`<st c="35683">.displayFrequency(.daily)</st>` <st c="35708">expression means that TipKit will show no more than one tip per day.</st> <st c="35778">Obviously, we have additional frequency options: hourly, weekly, monthly,</st> <st c="35852">and immediate.</st>
<st c="35866">We can configure specific tips to ignore the system</st> <st c="35919">display frequency:</st>
struct AddListTip: Tip {
var options: [TipOption] { <st c="35990">忽略显示频率</st> }
}
<st c="36028">In this code example, the</st> `<st c="36055">AddListTip</st>` <st c="36065">tip ignores the system definition for general</st> <st c="36112">display frequency.</st>
<st c="36130">Setting the max display count for a specific tip and defining a display frequency for all tips is a great way to fine-tune the user’s</st> <st c="36265">tips experience.</st>
<st c="36281">Summary</st>
<st c="36289">In this chapter, we discussed the importance of TipKit, added our first tip, customized its design and behavior, learned how to manage tips better by grouping them, and minimized their appearance by setting their display frequency.</st> <st c="36522">By now, we are fully prepared to implement TipKit in</st> <st c="36575">our apps.</st>
<st c="36584">TipKit touches on a severe app aspect: engagement and feature exploration.</st> <st c="36660">It looks like it supports many</st> <st c="36691">product requirements!</st>
<st c="36712">In the next chapter, we’ll discuss how to work seamlessly with one of our most important data sources:</st> <st c="36816">the network.</st>
第九章:8
从网络连接和获取数据
找到一个不与服务器连接的应用程序极其困难。
-
理解 移动网络 -
处理 HTTP 请求,包括 它们的响应 -
在应用程序流程中集成网络调用 -
探索 Combine 如何与网络 一起工作
技术要求
理解移动网络
<st c="1565">URLSession</st>
-
UI 层 :这是负责向用户展示 UI,包括响应用户输入。 UI 层包括 SwiftUI/UIKit 视图和 视图模型。 -
业务逻辑 :这是负责在管理基本 应用程序逻辑的同时操作数据 。 -
数据层 :这是负责存储和检索与业务逻辑相关的数据实体的。
我猜这个三层架构不会让你感到惊讶,因为大多数移动应用都采用类似的架构。
当我们开始理解网络任务的位置时,我们将不得不查看数据层,在某些特定情况下,业务逻辑层(例如,当与分析或第三方库一起工作时)也需要查看。然而,为什么我们需要查看这些层呢?为了理解为什么我们的网络活动与数据层相关,让我们回顾我们的主要网络目标:
-
同步信息 到和从 我们的后端 -
处理
认证 -
需要服务器的逻辑活动
在大多数应用中,网络是同步数据与后端所必需的。数据层作为实体真实信息的首要存储库运行。尝试从其他层直接从网络访问实体将破坏这一原则。

图 8.1:基本应用架构
在
例如,在一个音乐应用中,网络层可能会连接到后端,获取专辑和歌曲,并将它们存储在本地存储中,例如
我们可以将网络操作想象成一个工厂生产线。我们请求一条信息,并处理返回的数据包,将其通过几个阶段传输,直到我们将其正确地存储在我们的本地存储中或展示出来。
在我们回顾数据包可能经历的各个阶段之前,让我们尝试一起构建一个网络请求。我们将从回顾基本的 HTTP 请求方法开始。
处理 HTTP 请求
基本的 HTTP 请求方法
-
<st c="4796">GET</st>:这用于仅从服务器检索信息。 它应该是一个安全的调用,这意味着执行一个 <st c="4914">GET</st>请求不应该影响 后端数据。 -
<st c="4960">POST</st>:通常使用 <st c="4972">POST</st>方法向后端提交 数据。 在许多情况下, <st c="5048">POST</st>方法在后台数据存储中执行更改或更改 用户状态。 -
<st c="5127">PUT</st>:我们使用 <st c="5141">PUT</st>来创建或更新 对象。 与 <st c="5185">POST</st>方法不同, <st c="5198">PUT</st>被认为是幂等的。 我们可以发送多个相同的 <st c="5259">PUT</st>请求,并期望与发送一个请求相同的效果。 -
<st c="5322">DELETE</st>:正如其名所示,我们使用 <st c="5359">DELETE</st>来删除 对象。 显然,我们可以使用 <st c="5407">POST</st>来做这件事,但使用 <st c="5433">DELETE</st>,我们与 标准保持一致。
值得一提的是,技术上我们甚至可以使用<st c="5534">GET</st>
<st c="5746">URLSession</st>
使用 URLSession
<st c="5814">URLSession</st> <st c="5865">URLSession</st>
<st c="5983">URLSession</st> <st c="6100">URLSession</st> <st c="6103">GET</st>
let urlString =
"https://jsonplaceholder.typicode.com/posts"
if let url = URL(string: urlString) {
var request = <st c="6245">URLRequest</st>(url: url)
request.httpMethod = "GET"
<st c="6294">let session = URLSession(configuration: .default)</st> let task = session.dataTask(with: request) { (data,
response, error) in
}
task.resume()
}
<st c="6477">URLRequest</st> <st c="6519">URLRequest</st>
-
请求 基本 URL -
请求方法 – <st c="6700">GET</st>, <st c="6705">POST</st>, <st c="6711">PUT</st>, 或 <st c="6719">DELETE</st> -
请求 HTTP 头
<st c="6767">URLRequest</st> <st c="6869">URLSession</st>
<st c="6993">URLSession</st>
-
我们可以调用静态的 <st c="7037">shared</st>属性 并将其用作一个 单例 。如果我们想简化我们的实现,而不需要自定义处理请求的方式,或者在不同区域有不同的要求,我们就这样做: let session = URLSession.shared -
如果我们需要更多的灵活性,我们可以创建一个 <st c="7338">URLSession</st>实例 (就像上一个代码示例中那样)并用我们自己的配置初始化它。
<st c="7676">URLSession</st>
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 10
configuration.requestCachePolicy =
.reloadIgnoringLocalCacheData
let session = <st c="8024">timeoutIntervalForRequest</st> value to <st c="8059">10</st>, and defined the cache policy to be ignored.
<st c="8106">When we work</st> <st c="8119">with a shared</st> `<st c="8134">URLSession</st>` <st c="8144">object, there’s no way to customize</st> <st c="8180">its configuration, and it will use the</st> <st c="8220">default one.</st>
<st c="8232">Now that we know how to perform a basic</st> `<st c="8273">GET</st>` <st c="8276">or</st> `<st c="8280">POST</st>` <st c="8284">request, let’s see what we can do with</st> <st c="8324">the response.</st>
<st c="8337">Handling the response</st>
<st c="8359">The request response</st> <st c="8380">is handled</st> <st c="8391">using three stages: error handling, serialization, and data storage.</st> <st c="8461">We need to handle each one of the stages carefully and even consider having a dedicated class or function to simplify the process and separate</st> <st c="8604">the concerns.</st>
<st c="8617">As mentioned, the first stage is error handling.</st> <st c="8667">Let’s discuss it, as it is a crucial part</st> <st c="8709">of networking.</st>
<st c="8723">Implementing error handling</st>
<st c="8751">I believe error handling</st> <st c="8776">wouldn’t get a whole section</st> <st c="8805">in many frameworks.</st> <st c="8826">It is usually a straightforward topic: we perform a task, something goes wrong, and we receive</st> <st c="8921">an error.</st>
<st c="8930">However, with networking, we are working in a volatile environment where many things have the potential to fail</st> <st c="9043">the process.</st>
<st c="9055">Here’s a partial list of things that can</st> <st c="9097">go wrong:</st>
* <st c="9106">There is</st> <st c="9116">no network</st>
* <st c="9126">There’s a network, but the device cannot reach</st> <st c="9174">the internet</st>
* <st c="9186">The device can reach the internet but with a very</st> <st c="9237">slow connection</st>
* <st c="9252">We have a stable connection, but the request cannot reach</st> <st c="9311">the backend</st>
* <st c="9322">The request found the backend, but it</st> <st c="9361">didn’t respond</st>
<st c="9375">The error list can go on and on, ranging from network issues to security to</st> <st c="9452">server errors.</st>
<st c="9466">To simplify the idea, we can divide the errors into two main groups: network-related issues and</st> <st c="9563">server-side problems.</st>
<st c="9584">To understand the difference between network and server-related issues, let’s have another look at how we created a</st> <st c="9701">data task:</st>
let task = session.dataTask(with: request) { (data,
response, error)
<st c="9780">We can see that the data task response</st> <st c="9819">contains three</st> <st c="9834">parameters –</st> `<st c="9848">data</st>`<st c="9852">,</st> `<st c="9854">response</st>`<st c="9862">,</st> <st c="9864">and</st> `<st c="9868">error</st>`<st c="9873">.</st>
<st c="9874">Network-related errors are part of the</st> `<st c="9914">error</st>` <st c="9919">object, and server-related errors are mostly part of the</st> `<st c="9977">response</st>` <st c="9985">object and sometimes even part of the</st> `<st c="10024">data</st>` <st c="10028">object.</st>
<st c="10036">To handle a network error, we should look</st> <st c="10079">into</st> `<st c="10084">URLError</st>`<st c="10092">:</st>
if let error = error as? URLError {
switch error.code {
case .cannotFindHost:
// 通知用户。default:
print("错误:\(error)")
}
return
}
<st c="10237">In this code example, we performed a switch statement to understand our network error.</st> <st c="10325">In this case, we decided to handle one use case of</st> `<st c="10376">cannotFindHost</st>`<st c="10390">. However, there are at least 20 different error codes we can handle.</st> <st c="10460">To read the full and updated list, we should look at Apple documentation</st> <st c="10533">at</st> [<st c="10536">https://developer.apple.com/documentation/foundation/urlerror</st>](https://developer.apple.com/documentation/foundation/urlerror)<st c="10597">.</st>
<st c="10598">Unlike network-related errors, server-related errors are more complex.</st> <st c="10670">First, we are dependent on another partner—our server.</st> <st c="10725">How the server implements its error-handling logic significantly influences how we handle it in</st> <st c="10821">our app.</st>
<st c="10829">Let’s understand that by examining the</st> <st c="10869">server response:</st>
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200..<300:
print("成功:
\(httpResponse.statusCode)")
case 400..<500:
print("客户端错误:
\(httpResponse.statusCode)")
case 500..<600:
print("服务器错误:
\(httpResponse.statusCode)")
default:
print("其他状态码:
\(httpResponse.statusCode)")
}
} else {
print("无效的 HTTP 响应")
}
<st c="11272">We first cast the response</st> <st c="11299">into the</st> `<st c="11309">HTTPURLResponse</st>` <st c="11324">type, representing</st> <st c="11343">a general</st> <st c="11354">URL response.</st>
<st c="11367">The response includes a status code, which the server sends back to us.</st> <st c="11440">In most cases, the code will be part of the following</st> <st c="11494">three groups:</st>
* `<st c="11507">200..299</st>`<st c="11516">: The server successfully responded to</st> <st c="11556">our request</st>
* `<st c="11567">400..499</st>`<st c="11576">: The server returns an error due to a bad</st> <st c="11620">client request</st>
* `<st c="11634">500..599</st>`<st c="11643">: The server returned an error due to an internal</st> <st c="11694">server error</st>
<st c="11706">In short, there are three cases – everything went well, it is the client’s fault, or it is the</st> <st c="11802">server’s fault.</st>
<st c="11817">However, in real life, things</st> <st c="11847">are more complex.</st> <st c="11866">Sometimes, the server</st> <st c="11887">returns a response code of</st> `<st c="11915">200</st>` <st c="11918">(success) but includes an error in the response data.</st> <st c="11973">There is nothing wrong with doing that – the server can choose how to handle problems.</st> <st c="12060">It’s our responsibility to parse the</st> <st c="12097">response correctly.</st>
<st c="12116">If we need to parse the response ourselves to extract the error, it is better to create a function that receives the data, response, and error parameters and throws an error in case it</st> <st c="12302">finds one:</st>
func handleResponse(data: Data?, response: URLResponse?, error: Error?) throws {
if let error = error {
throw error
}
guard let httpResponse = response as? HTTPURLResponse
else {
throw NetworkingError.invalidResponse
}
switch httpResponse.statusCode {
case 200..<300:
if let responseData = data {
if let errorData = try? JSONDecoder().decode(ErrorResponse.self,
from: responseData) {
throw NetworkingError.dataError
}
}
case 400..<500:
throw NetworkingError.clientError(statusCode:
httpResponse.statusCode)
case 500..<600:
throw NetworkingError.serverError(statusCode:
httpResponse.statusCode)
default:
throw NetworkingError.otherError
}
}
<st c="12952">This long</st> `<st c="12963">handleResponse</st>` <st c="12977">function</st> <st c="12986">does precisely</st> <st c="13001">what we’ve discussed.</st> <st c="13024">In case of a successful response, it checks the error object, the response code, and the</st> <st c="13113">data itself.</st>
<st c="13125">To use that function, we need to call it within the</st> <st c="13178">response closure:</st>
let task = session.dataTask(with: request) { (data,
response, error) in
do {
try handleResponse(data: data, response: response,
error: error)
} catch let error {
print("错误:\(error)")
}
}
<st c="13386">The great thing about the</st> `<st c="13413">handleResponse</st>` <st c="13427">function is that we can ensure that we can continue handling the response data after the</st> `<st c="13517">try</st>` <st c="13520">statement because we have dealt with</st> <st c="13558">any error.</st>
<st c="13568">If you look again</st> <st c="13586">at the</st> `<st c="13594">handleResponse</st>` <st c="13608">function, you’ll see that we decode</st> <st c="13644">the response to look for</st> <st c="13670">an error.</st>
<st c="13679">Deserializing the response is a major step in handling a network response.</st> <st c="13755">Let’s discuss it a little</st> <st c="13781">bit further.</st>
<st c="13793">Deserializing a network response</st>
<st c="13826">In most apps, the response</st> <st c="13853">we get from the server is based on JSON data</st> <st c="13898">structure.</st> <st c="13910">JSON is an industry standard for delivering network responses along</st> <st c="13978">with XML.</st>
<st c="13987">Swift has built-in support for parsing JSON structures into Swift structures, using tools such as the</st> `<st c="14090">Codable</st>` <st c="14097">protocol and</st> `<st c="14111">JSONDecoder</st>` <st c="14122">classes.</st>
<st c="14131">In theory, it sounds perfect—all we need to do is decode our response to a data model.</st> <st c="14219">However, there are more factors we need</st> <st c="14259">to consider:</st>
* `<st c="14413">handleResponse</st>` <st c="14427">function example, we saw a response that may have contained an error message.</st> <st c="14506">This means that when we think about our data models, general network responses should be</st> <st c="14595">among them.</st>
* **<st c="14606">Assuming there’s always an object array</st>**<st c="14646">: Decoding a single object is straightforward, but in many cases, we also need to handle decoding an array of objects.</st> <st c="14766">That sounds trivial, but supporting both formats can be a hassle.</st> <st c="14832">To simplify the decoding process, it is better to always support an array of objects, which is a decision that we need to coordinate with our</st> <st c="14974">backend developers.</st>
* **<st c="14993">Mixed structures</st>**<st c="15010">: A response can contain different model types and even nested data structures.</st> <st c="15091">This is not always trivial, so our data structures must be more dynamic and modular to support</st> <st c="15186">various responses.</st>
* **<st c="15204">Model transformations</st>**<st c="15226">: Our local app models are structured to be efficient and convenient to use with the business logic and UI layers.</st> <st c="15342">However, who said that the backend response structure is aligned with what is suitable for our app?</st> <st c="15442">This means we must transform the response data model to our local</st> <st c="15508">data model.</st>
<st c="15519">Deserializing data models</st> <st c="15545">is indeed a complex task, and trying to match our data models</st> <st c="15607">to the response structure we receive from our backend is only sometimes the best idea.</st> <st c="15695">Remember that our data models must suit our app needs and not necessarily follow the</st> <st c="15780">backend methodology.</st>
<st c="15800">Let’s take a simple JSON received from</st> <st c="15840">the server:</st>
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
<st c="15912">That’s a contact structure.</st> <st c="15941">However, we want to use different names in our app so we can use the</st> `<st c="16010">CodingKey</st>` <st c="16019">protocol to ensure we perform the</st> <st c="16054">transformation correctly:</st>
struct Contact: Codable {
let id: Int
let fullName: String
let userEmail: String
// 定义自定义编码键以匹配 JSON 键
private enum CodingKeys: String, CodingKey {
case id
case fullName = "name"
case userEmail = "email"
}
}
<st c="16313">Decoding the server response using the</st> `<st c="16353">Contact</st>` <st c="16360">structure now becomes</st> <st c="16383">much simpler:</st>
let errorData = try? JSONDecoder().decode(Contact.self,
from: responseData)
<st c="16472">In this example, we map the</st> `<st c="16501">name</st>` <st c="16505">value to</st> `<st c="16515">fullName</st>` <st c="16523">and</st> `<st c="16528">email</st>` <st c="16533">to</st> `<st c="16537">userEmail</st>`<st c="16546">. We decode it using the</st> `<st c="16571">JSONDecoder</st>` <st c="16582">class.</st> <st c="16590">Understanding the</st> `<st c="16608">CodingKey</st>` <st c="16617">protocol is a crucial key to decoding</st> <st c="16656">server responses.</st>
<st c="16673">There are cases where the whole structure</st> <st c="16715">of the server response</st> <st c="16738">is entirely different than our data models, and in those cases, we need to create a dedicated structure to parse the response.</st> <st c="16866">However, sometimes, we can still use our data model as part of the structure.</st> <st c="16944">Let’s look at the</st> <st c="16962">following example:</st>
struct ServerResponse: Codable {
let responseID: String
let timestamp: String
let orgID: String
let contact: Contact
}
let jsonString = """
{
"responseID": "12345",
"timestamp": "2024-03-25T12:00:00Z",
"orgID": "5678",
"contact": {
"id": 1,
"fullName": "John Doe",
"userEmail": "john@example.com"
}
}
"""
let jsonData = jsonString.data(using: .utf8)! let response = try
JSONDecoder().decode(ServerResponse.self, from: jsonData)
<st c="17408">In this code example, the server returns additional information besides the contact object.</st> <st c="17501">So, we can create a dedicated data structure for the response—</st>`<st c="17563">ServerResponse</st>` <st c="17578">(in this case).</st> <st c="17595">In addition to general information, the</st> `<st c="17635">ServerResponse</st>` <st c="17649">struct contains the</st> `<st c="17670">Contact</st>` <st c="17677">struct.</st> <st c="17686">So, we can see a modular approach here—we can parse our server response</st> <st c="17757">using</st> `<st c="17764">Codable</st>` <st c="17771">and still use our data model objects</st> <st c="17808">to receive</st> <st c="17820">the information.</st>
<st c="17836">The next step is to store our data model in our</st> <st c="17885">data store.</st>
<st c="17896">Building a data store</st>
<st c="17918">A disclaimer: not every network</st> <st c="17950">call requires</st> <st c="17964">us to store the results in a data store.</st> <st c="18006">For instance, authentication and logic calls have different goals.</st> <st c="18073">However, this chapter will focus mainly on data-related calls responsible for building our local</st> <st c="18170">data store.</st>
<st c="18181">That leads us to our next point: what is the role of the</st> <st c="18239">data store?</st>
<st c="18250">So, a data store is a structured mechanism for managing and storing data that serves the application’s main business logic</st> <st c="18374">and UI.</st>
<st c="18381">Unlike many online examples, the application business logic usually doesn’t work directly with the network responses – these need to be adjusted and saved in our store, which acts as the UI</st> <st c="18572">data source.</st>
<st c="18584">Let’s look at</st> *<st c="18599">Figure 8</st>**<st c="18607">.2</st>*<st c="18609">:</st>

<st c="18682">Figure 8.2: Working with the datastore</st>
*<st c="18720">Figure 8</st>**<st c="18729">.2</st>* <st c="18731">shows how the data layer works directly with the data store, as the network layer fills the data store with</st> <st c="18840">more information.</st>
<st c="18857">The data store</st> <st c="18872">doesn’t have to be persistent—that’s an engineering</st> <st c="18925">decision.</st> <st c="18935">However, in most cases, it is a structured store.</st> <st c="18985">A structured store has pre-defined models, relations between entities, and often even query capabilities.</st> <st c="19091">These characteristics distinguish the data store from simply caching the</st> <st c="19164">network responses.</st>
<st c="19182">To follow the separation of concerns principle, it is better to have dedicated classes to handle each step of</st> <st c="19293">the process.</st>
<st c="19305">First, we’ll create a</st> `<st c="19328">DataStore</st>` <st c="19337">class:</st>
class DataStore {
private var contacts: [Contact] = []
func updateContacts(with newContacts: [Contact]) {
contacts = newContacts
}
func getAllContacts() -> [Contact] {
return contacts
}
}
<st c="19532">The</st> `<st c="19537">DataStore</st>` <st c="19546">class not only stores the data but also has methods that help store and</st> <st c="19619">retrieve entities.</st>
<st c="19637">Assuming we already have a network</st> <st c="19672">handler from the previous</st> <st c="19698">examples, we are now going to create a sync class that coordinates the process of fetching the data and</st> <st c="19803">storing it:</st>
class SyncManager {
private let dataStore: DataStore
init(dataStore: DataStore) {
self.dataStore = dataStore
}
func syncData() {
NetworkHandler.fetchData { result in
switch result {
case .success(let data):
do {
let contacts = try JSONDecoder().decode([Contact].self, from: data)
self.dataStore.updateContacts(with: contacts)
print("数据同步成功")
} catch {
print("JSON 解码数据错误:", error)
}
case .failure(let error):
print("获取数据错误:", error)
}
}
}
}
<st c="20294">The</st> `<st c="20299">SyncManager</st>` <st c="20310">class uses the</st> `<st c="20326">NetworkHandler</st>` <st c="20340">class to fetch the information from our backend, parses the results into</st> `<st c="20414">Contact</st>` <st c="20421">entities, and stores them in our data store.</st> <st c="20467">Using this design</st> <st c="20484">pattern, we can easily replace the data store implementation</st> <st c="20545">to be persistent without modifying the</st> <st c="20585">other classes.</st>
<st c="20599">Now that we have a data store, let’s try to understand how to make our app</st> <st c="20675">more efficient.</st>
<st c="20690">Integrating network calls within app flows</st>
<st c="20733">We already know how to perform</st> <st c="20764">a network call, parse</st> <st c="20786">it to data objects, and create a data store.</st> <st c="20832">We also know how to handle errors, and we learned that it’s important to separate the concerns into different classes</st> <st c="20950">and components.</st>
<st c="20965">However, it feels like a technical discussion.</st> <st c="21013">Performing a URL connection in iOS is one of the most basic tasks.</st> <st c="21080">Let’s try to upgrade our discussion and</st> <st c="21120">discuss methodology.</st>
<st c="21140">First, we should think of streaming data from the network as an atomic task in our app’s data synchronization mechanism.</st> <st c="21262">It’s up to us to decide when to call our server for more data.</st> <st c="21325">From our discussions, it looks like we need to contact the server just before we want to display the information, but it doesn’t have to be</st> <st c="21465">like that.</st>
<st c="21475">Let’s discuss the different strategies we can use when working with our backend.</st> <st c="21557">We’ll start with the</st> **<st c="21578">just-in-time</st>** <st c="21590">fetching technique.</st>
<st c="21610">Just-in-time fetching</st>
<st c="21632">The just-in-time fetching technique</st> <st c="21668">is very common and also the simplest one.</st> <st c="21711">With it, we don’t present anything on the screen before we get a response from the server.</st> <st c="21802">Instead, we show a loader indicating that we are</st> <st c="21851">fetching data.</st>
<st c="21865">In just-in-time fetching, we don’t preserve the information in a data store; instead, we store the information in the view state or the view model.</st> <st c="22014">Here’s a simple example of</st> <st c="22041">just-in-time fetching:</st>
import SwiftUI
struct ContactsView: View {
@State private var contacts: [Contact] = []
@State private var isLoading = false
var body: some View {
NavigationView {
List(contacts) { contact in
VStack(alignment: .leading) {
Text(contact.name).font(.headline)
Text(contact.phoneNumber).font(.subheadline)
}
}
.navigationTitle("联系人")
.onAppear {
fetchContacts()
}
.overlay {
if isLoading {
ProgressView("加载中...")
}
}
}
}
private func fetchContacts() {
isLoading = true
NetworkHandler().fetchData { fetchedContacts in
contacts = fetchedContacts
isLoading = false
}
}
}
<st c="22635">In this code example, we have a list that is based on the state variable of contacts.</st> <st c="22722">When the view appears, we call the</st> `<st c="22757">fetchContacts</st>` <st c="22770">method to fetch the list of contacts and, in the meantime, show a</st> <st c="22837">loading message.</st>
<st c="22853">Besides its simplicity, the just-in-time technique is great for apps that must ensure that the data they display is up to date, such as financial apps or live sports scores.</st> <st c="23028">The downside here is that we provide a poor user experience</st> <st c="23087">and depend on the</st> <st c="23106">network state.</st>
<st c="23120">If possible, we should pick a slightly better technique, often called</st> **<st c="23191">read-through cache</st>**<st c="23209">.</st>
<st c="23210">Read-through cache</st>
<st c="23229">The read-through cache technique</st> <st c="23262">is also a popular way to present data to the user, even though most developers are unaware of</st> <st c="23357">its name.</st>
<st c="23366">Using the read-through cache approach, we display our local data to the user while going to our backend to refresh</st> <st c="23482">our data.</st>
<st c="23491">Let’s see a code example</st> <st c="23517">for that:</st>
import SwiftUI
struct ContactsView: View {
@State private var contacts: [Contact] = []
var body: some View {
NavigationView {
List(contacts) { contact in
VStack(alignment: .leading) {
Text(contact.name).font(.headline)
Text(contact.phoneNumber).font(.subheadline)
}
}
.navigationTitle("联系人")
.onAppear {
loadContacts()
}
}
}
private func loadContacts() {
contacts = loadFromCache()
NetworkHandler().fetchData { fetchedContacts in
contacts = fetchedContacts
saveToCache(contacts: fetchedContacts)
}
}
}
<st c="24033">In this code example, we load the contacts from the cache (or from the local store) when the screen appears and then go to the network to refresh our data set.</st> <st c="24194">The read-through cache technique is great when quick access to data is crucial because it is not up-to-date, for example, in news or</st> <st c="24327">e-commerce apps.</st>
<st c="24343">You’ve probably noticed that both the just-in-time and read-through cache techniques require us to load the page information fully from the backend, regardless of the amount of information</st> <st c="24533">we have.</st>
<st c="24541">Now, what if we know upfront</st> <st c="24570">that we have a huge number of records to fetch, so big that it can even cause our request to time out?</st> <st c="24674">In this case, we can choose the</st> **<st c="24706">incremental</st>** **<st c="24718">loading</st>** <st c="24725">technique.</st>
<st c="24736">Incremental loading</st>
<st c="24756">There are cases wherein</st> <st c="24780">we can expect to fetch a vast number of records.</st> <st c="24830">A social feed, for instance, can have an infinite number of posts.</st> <st c="24897">Well, it’s not really infinite, but we can relate to that number</st> <st c="24962">as infinity.</st>
<st c="24974">When the number is too big to fetch in one request, we can use</st> <st c="25038">incremental loading.</st>
<st c="25058">With incremental loading, we fetch a set of records each time with each request and store the last record index for the</st> <st c="25179">next time.</st>
<st c="25189">Here’s an example of</st> <st c="25211">incremental loading:</st>
class IncrementalLoader {
var currentPage = 1
let itemsPerPage = 10
var contacts = [Contact]()
func loadNextPage() {
guard let url = URL(string:
"https://api.example.com/contacts?page=(currentPage)&limit=(itemsPerPage)") else {
print("无效的 URL")
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
guard let self = self else { return }
do {
let newContacts = try JSONDecoder().decode([Contact].self, from: data)
DispatchQueue.main.async {
self.contacts.append(contentsOf: newContacts)
print("获取联系人: \(newContacts)")
self.currentPage += 1
}
} catch {
print("JSON 解码错误: \(error)")
}
}
task.resume()
}
}
<st c="25905">In this example, we have a class named</st> `<st c="25945">IncrementalLoading</st>`<st c="25963">, which is responsible for loading the next set of records with the function named</st> `<st c="26046">loadNextPage</st>`<st c="26058">. Incremental loading is also called</st> `<st c="26177">IncrementalLoading</st>` <st c="26195">example, we have an index that points to the last record index fetched, and a variable named</st> `<st c="26289">itemsPerPage</st>` <st c="26301">that defines how many items to fetch on</st> <st c="26342">each page.</st>
<st c="26352">While incremental loading solves handling a large amount of data, there are several factors</st> <st c="26444">we need</st> <st c="26453">to consider:</st>
* `<st c="26720">List</st>` <st c="26724">view or a UIKit</st> `<st c="26741">TableView</st>` <st c="26750">view.</st> <st c="26757">In these cases, we would like to fetch the next set of records when the user reaches the bottom of the list.</st> <st c="26866">However, things can become complex when we allow the user to edit or delete records since that can affect the</st> <st c="26976">index variable.</st>
* **<st c="26991">Memory consumption</st>**<st c="27010">: It’s true that incremental loading is optimized to handle a significant amount of information.</st> <st c="27108">However, we are still talking about storing a large amount of information in our memory.</st> <st c="27197">While the user is paging through our data, our local data store can become bigger, mainly if it contains rich media such as images and videos.</st> <st c="27340">It is essential to have a mechanism that can release records in case of high</st> <st c="27417">memory usage.</st>
* **<st c="27430">Contextual relevance</st>**<st c="27451">: We need to remember that our chosen design pattern needs to support a specific product need.</st> <st c="27547">Incremental loading is relevant in cases wherein we don’t need all the data at once.</st> <st c="27632">Social feeds or search results are great examples of data that can be browsed chunk by chunk.</st> <st c="27726">However, in cases where the user requires immediate access to all the data, such as in data analysis, incremental loading</st> <st c="27847">might not</st> <st c="27858">be suitable.</st>
<st c="27870">Considering the different factors mentioned, we understand that, similar to many design patterns in computer science, incremental loading presents a tradeoff between different aspects such as performance, complexity, experience, and more.</st> <st c="28110">It’s up to us to choose the right design pattern that fits</st> <st c="28169">our needs.</st>
<st c="28179">The three design patterns we discussed now require different endpoints for different types of data and other screens, which sounds logical.</st> <st c="28320">However, there’s another way to handle data that changes over time and still provides an amazing experience to the user –</st> <st c="28442">delta updates.</st>
<st c="28456">Full data sync with delta updates</st>
<st c="28490">Before we discuss full data sync with the delta updates method, let’s talk about problems that we have with</st> <st c="28598">multiple endpoints:</st>
* **<st c="28618">Efficient network calls</st>**<st c="28642">: The need to request the same data repeatedly, even if nothing has changed, seems inefficient.</st> <st c="28739">We can use the cache to present previous results, but that only solves performance issues.</st> <st c="28830">We still need to perform the same request to understand whether there</st> <st c="28900">are updates.</st>
* **<st c="28912">Incomplete database</st>**<st c="28932">: Each endpoint retrieves different data and sometimes different entities.</st> <st c="29008">We know that in many cases, the entities are related (such as to-one and to-many relationships), and having multiple endpoints to fetch them probably means our data won’t be complete.</st> <st c="29192">That seems acceptable – we’re focused on a mobile app and not a server.</st> <st c="29264">However, having an incomplete data store can result in a poor experience.</st> <st c="29338">Users may encounter updated information on one screen, navigate to another, and view outdated data while waiting for the screen to refresh from the server.</st> <st c="29494">If both screens contain related data, it can result in a</st> <st c="29551">poor experience.</st>
* **<st c="29567">App performance</st>**<st c="29583">: We often believe that performance is only about CPU and Swift code efficiency.</st> <st c="29665">However, our devices are strong enough to handle most tasks without a hiccup.</st> <st c="29743">In contrast, network requests</st> <st c="29772">cause users to wait even if they have the latest hardware.</st> <st c="29832">Having a network call on each screen greatly impacts the</st> <st c="29889">user experience.</st>
<st c="29905">Delta updates</st> <st c="29919">are a solution that can handle some of the problems we described with endpoints in the previous section.</st> <st c="30025">With delta updates, we fetch all the information at the app’s initial launch and, from this point, retrieve only</st> <st c="30138">the changes.</st>
<st c="30150">We do that by storing a bookmark representing our data’s last updated timestamp.</st> <st c="30232">When we ask the server, “Do you have any updates for me?”, we send this bookmark, get the new changes (if any), receive a new bookmark, and</st> <st c="30372">store it.</st>
<st c="30381">Here’s a code example for contacts</st> <st c="30416">delta sync.</st> <st c="30429">We start with the</st> `<st c="30447">syncContacts</st>` <st c="30459">function:</st>
class ContactsSyncManager {
let userDefaults = UserDefaults.standard
let lastUpdatedKey = "lastUpdatedTime"
let syncEndpoint = URL(string:
"https://example.com/api/sync/contacts")! func syncContacts() {
var request = URLRequest(url: syncEndpoint)
request.httpMethod = "POST"
request.addValue("application/json",
forHTTPHeaderField: "Content-Type")
let lastUpdatedTime = userDefaults.double(forKey:
lastUpdatedKey)
let requestBody = ["lastUpdatedTime":
lastUpdatedTime]
request.httpBody = try? JSONSerialization.data(withJSONObject:
requestBody)
URLSession.shared.dataTask(with: request) { [weak
self] data, response, error in
self?.processDeltaUpdates(response: response)
}.resume()
}
<st c="31154">The code example does exactly</st> <st c="31184">what we described earlier—it saves a bookmark called</st> `<st c="31238">lastUpdatedDate</st>`<st c="31253">. Initially, we fetch all the data and save the new</st> `<st c="31305">lastUpdatedDate</st>` <st c="31320">value we get from the server.</st> <st c="31351">The next time we perform the sync operation, we get only the changes.</st> <st c="31421">Now, let’s implement the</st> `<st c="31446">processDeltaUpdates</st>` <st c="31465">function:</st>
private func processDeltaUpdates(response:
ContactsDeltaUpdateResponse) {
// 这里可以根据需要处理新增、删除和更新的联系人
print("新联系人:
\(response.newContacts.count)")
print("Deleted Contacts:
\(response.deletedContacts.count)")
print("Updated Contacts:
\(response.updatedContacts.count)")
userDefaults.set(response.lastUpdated, forKey:
lastUpdatedKey)
}
}
<st c="31863">The</st> `<st c="31868">processDeltaUpdates</st>` <st c="31887">function receives a response that contains only the changes that have occurred in the server since the</st> <st c="31991">last sync.</st>
<st c="32001">That’s why the response</st> <st c="32025">is structured into three groups: deleted, new, and updated.</st> <st c="32086">With each one, we need to handle the</st> <st c="32123">data differently.</st>
<st c="32140">Some critical notes we need to consider</st> <st c="32180">here are</st> <st c="32190">as follows:</st>
* **<st c="32201">Extremally large data sets</st>**<st c="32228">: The delta updates pattern is not relevant for very large data sets.</st> <st c="32299">For example, a social app feed can have millions of records, and fetching all of them from the start is impossible.</st> <st c="32415">For that issue, we can</st> <st c="32438">use pagination.</st>
* **<st c="32453">The initial loading can be long</st>**<st c="32485">: Since we fetch all the data at the beginning, we need to deliver a corresponding</st> <st c="32569">user experience.</st>
* **<st c="32585">Deleted items</st>**<st c="32599">: Syncing deleted items is always a crucial topic.</st> <st c="32651">We need to actively delete items that no longer exist, so the response from the server should contain items we need</st> <st c="32767">to delete.</st>
* **<st c="32777">Sync triggers</st>**<st c="32791">: Since we perform the sync operation at the beginning, it looks like it’s the only time we should do that.</st> <st c="32900">However, there are more occasions when we need to refresh our data.</st> <st c="32968">For example, when we perform data changes such as calling the server to add a new item or receiving a push notification, we should think about the different cases when something can change in our server during the app runtime</st> <st c="33193">and try to refresh</st> <st c="33213">our data.</st>
<st c="33222">It’s important to understand that none of the solutions are perfect.</st> <st c="33292">Sometimes, it is a good idea to combine different approaches—for example, use delta sync in general, but maybe use pagination for a</st> <st c="33424">specific screen.</st>
<st c="33440">We should consider the different approaches as a toolbox with several tools, each suitable for various problems or</st> <st c="33556">data structures.</st>
<st c="33572">Now that we understand how to handle requests and use different patterns to incorporate the calls in our app flows, let’s see another way to handle networking</st> <st c="33732">in iOS.</st>
<st c="33739">Exploring Networking and Combine</st>
<st c="33772">Networking is a great place</st> <st c="33800">to start if you</st> <st c="33816">haven’t worked with Combine.</st> <st c="33846">Combine is a framework that declaratively handles a stream of values over time while supporting</st> <st c="33942">asynchronous operations.</st>
<st c="33966">Based on that description, it looks like Combine was made for</st> <st c="34029">networking operations!</st>
<st c="34051">In this chapter, we are not going to discuss what Combine is – for that, we’ve got</st> *<st c="34135">Chapter 11</st>*<st c="34145">. However, we are going to discuss it now because Combine is a great way to solve many networking</st> <st c="34243">operations problems.</st>
<st c="34263">Since Combine is built upon publishers and operators, it is simple to create new publishers that stream data from</st> <st c="34378">the network.</st>
<st c="34390">Let’s try to request the list of contacts from previous examples using a Combine stream.</st> <st c="34480">We’ll start with creating a publisher that performs data fetching from the network and publish a list</st> <st c="34582">of contacts:</st>
class ContactRequest {
func fetchData() -> AnyPublisher<[Contact], Error> {
let url = URL(string:
"https://api.example.com/contacts")! return URLSession.shared.dataTaskPublisher(for:
url)
.map { $0.data }
.decode(type: [Contact].self, decoder:
JSONDecoder())
.eraseToAnyPublisher()
}
}
<st c="34880">The publisher utilizes</st> <st c="34903">URLSession’s</st> `<st c="34917">dataTaskPublisher</st>` <st c="34934">method</st> <st c="34941">to execute the network request and publish the retrieved data.</st> <st c="35005">We then extract the data using the map operation and decode it into a list of</st> `<st c="35083">Contact</st>` <st c="35090">items.</st> <st c="35098">If something goes wrong, the publisher will report an Error.</st> <st c="35159">We wrap this function in a class named</st> `<st c="35198">ContactRequest</st>` <st c="35212">to</st> <st c="35216">maintain separation.</st>
<st c="35236">Now, let’s create a small</st> `<st c="35263">DataStore</st>` <st c="35272">class so we can store the results and</st> <st c="35311">publish them:</st>
class DataStore {
@Published var contacts: [Contact] = []
}
<st c="35384">The</st> `<st c="35389">@Published</st>` <st c="35399">property wrapper creates a publisher for contacts so that we can observe the</st> <st c="35477">changes easily.</st>
<st c="35492">Now, we can use the</st> `<st c="35513">fetchData()</st>` <st c="35524">function</st> <st c="35533">to read the results</st> <st c="35553">and</st> <st c="35558">store them:</st>
class ContactsSync {
let contactRequest = ContactRequest()
let dataStore = DataStore()
func syncContacts() { <st c="35679">contactRequest.fetchData()</st> .sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Data fetch completed
successfully")
case .failure(let error):
print("Error fetching data: \(error)")
}
}, receiveValue: { [weak self] contacts in <st c="35935">self?.dataStore.contacts = contacts</st> })
.store(in: &cancellables)
}
private var cancellables = Set<AnyCancellable>()
}
let contactsSync = ContactsSync()
contactsSync.syncContacts()
<st c="36114">The</st> `<st c="36119">ContactsSync</st>` <st c="36131">job is to fetch</st> <st c="36147">contacts using the</st> `<st c="36167">ContactRequest</st>` <st c="36181">class and to store</st> <st c="36200">them in the data store using the</st> `<st c="36234">DataStore</st>` <st c="36243">class.</st>
<st c="36250">The Combine example has</st> <st c="36274">several advantages:</st>
* **<st c="36294">Clear and consistent interface</st>**<st c="36325">: The publisher interface is consistent and known.</st> <st c="36377">It is always built from data/void and an optional error.</st> <st c="36434">New developers don’t need to learn and understand how to</st> <st c="36491">read/use it.</st>
* **<st c="36503">Built-in error handling</st>**<st c="36527">: Not only do we have a consistent interface that also contains errors, but also, when one of the stages encounters an error, it interrupts the flow and channels it downstream.</st> <st c="36705">We have already seen that error handling is a critical topic in networking in</st> <st c="36783">many cases.</st>
* **<st c="36794">Asynchronous operations support</st>**<st c="36826">: We often think that a network operation contains one asynchronous operation: the request itself.</st> <st c="36926">However, many steps in the stream can be asynchronous – including preparing the request by reading local data, processing the response, and storing the data at the end of the stream.</st> <st c="37109">Combine streams are perfect for performing all those</st> <st c="37162">steps asynchronously.</st>
* **<st c="37183">Modularity</st>**<st c="37194">: The capability of building</st> <st c="37223">a modular code is reserved not only for the Combine framework, but the custom publishers and the different operators make Combine streams a joyful framework to implement when dealing with networking.</st> <st c="37424">Remember that we said that networking is like a production line (under the</st> *<st c="37499">Understanding mobile networking</st>* <st c="37530">section)?</st> <st c="37541">So, Combine makes it easier to insert more steps into the stream; some of them are even built into</st> <st c="37640">the framework.</st>
<st c="37654">Adding reactive methods</st> <st c="37678">to our code doesn’t mean we need to discard all the design patterns and principles we discussed when we covered networking—it’s just another</st> <st c="37819">way to</st> <st c="37827">implement them.</st>
<st c="37842">For example, let’s try to implement the delta updates design pattern using the</st> <st c="37922">Combine framework:</st>
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { output in
guard let response = output.response as? HTTPURLResponse, response.statusCode ==
200 else {
throw URLError(.badServerResponse)
}
return output.data
}
.decode(type: ContactsDeltaUpdateResponse.self,
decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("Error during sync:
\(error.localizedDescription)")
}
}, receiveValue: { [weak self] response in
self?.processDeltaUpdates(response:
response)
})
.store(in: &cancellables)
<st c="38552">Looking at the code</st> <st c="38572">example, we can</st> <st c="38588">see that it looks pretty much like the previous Combine code—that’s part of the idea of consistent interface and modular code.</st> <st c="38716">We perform the request, check the response code, decode it, change it to the main thread, and process the</st> <st c="38822">response data.</st>
<st c="38836">Summary</st>
<st c="38844">Connecting to our backend and retrieving data is a basic task in most mobile apps.</st> <st c="38928">Doing so lets us present valuable and interesting information to</st> <st c="38993">our users.</st>
<st c="39003">Performing a simple request is easy – however, there are many other factors to bear in mind, and doing that properly is crucial to having an</st> <st c="39145">efficient app.</st>
<st c="39159">This chapter reviewed the different network components, such as the request itself, error handling, and data storage.</st> <st c="39278">We also discussed our different design patterns to work with our backend.</st> <st c="39352">We ended up incorporating Combine into our flows.</st> <st c="39402">We should now be perfectly able to set up a fantastic network infrastructure for</st> <st c="39483">our app.</st>
<st c="39491">Now, let’s flip to the other side of our architecture, the UI, and discuss a library that can enrich our app easily –</st> **<st c="39610">Charts</st>**<st c="39616">!</st>
第十章:9
使用 Swift Charts 创建动态图表
-
了解为什么我们需要在应用中使用图表 我们的应用中 -
了解 Swift Charts 框架 -
创建条形图、折线图、饼图、面积图和 点图 -
使用图表可视化函数 图表 -
使用 ChartProxy 实现图表的 用户交互 -
通过遵循 可绘制协议 允许不同数据类型与图表一起工作
技术要求
为什么需要图表?
介绍 Swift Charts 框架
import Charts
struct BarMarkView: View {
struct Sales: Identifiable {
var id: UUID = UUID()
let itemType: String
let qty: Int
}
let data: [Sales] = [
Sales(itemType: "Apples", qty: 50),
Sales(itemType: "Oranges", qty: 60),
Sales(itemType: "Watermelons", qty: 30)
]
var body: some View {
VStack { <st c="3731">Chart(data) {</st>
<st c="3744">BarMark(</st>
<st c="3753">x: .value("Fruit", $0.itemType),</st>
<st c="3786">y: .value("qty", $0.qty)</st>
<st c="3811">)</st>
<st c="3813">}</st> }
}
}
<st c="3924">BarMark</st> <st c="3991">Sales</st> <st c="4080">data</st>
<st c="4201">Chart</st> 的新视图,并将</st> <st c="4220">数组作为参数。</st> <st c="4243">在那个</st><st c="4260">视图中,我们添加了一个</st><st c="4283">视图——一种以条形展示数据信息的方式——传递来自我们</st><st c="4376">结构体的</st><st c="4348">和</st>

<st c="4625">列表</st> <st c="4635">垂直堆叠</st>
创建图表
<st c="4921">Chart</st>
<st c="4928">Chart</st>(data) { <st c="4943">BarMark</st>(
x: .value("Fruit", $0.itemType),
y: .value("qty", $0.qty)
)
}
<st c="5138">条形标记</st> <st c="5213">ForEach</st>
<st c="5323">Chart</st> { <st c="5332">ForEach</st>(data, id:\.id) { item in <st c="5366">BarMark</st>(x: .value("Fruit",
item.itemType),
y: .value("qty", item.qty))
}
}
<st c="5522">ForEach</st> <st c="5549">条形标记</st>
<st c="5722">条形标记</st>
创建条形标记图表
<st c="5786">条形标记</st>
添加堆叠标记
例如,让我们拿我们刚刚创建的销售图表来讨论苹果的销售情况。
<st c="6686">Sales</st>
struct Sales: Identifiable {
var id: UUID = UUID()
let itemType: String
let qty: Int <st c="6816">var fruitColor: String = ""</st> }
<st c="6870">fruitColor</st> <st c="6897">Sales</st>
let data: [Sales] = [
Sales(itemType: "Apples", qty: 20, fruitColor:
"Green"),
Sales(itemType: "Apples", qty: 30, fruitColor:
"Red"),
Sales(itemType: "Oranges", qty: 60),
Sales(itemType: "Watermelons", qty: 30)
]
Chart(data) {
BarMark(x: .value("Fruit", $0.itemType),
y: .value("qty", $0.qty)) <st c="7469">.foregroundStyle(by: .value("Color",</st>
<st c="7505">$0.fruitColor))</st> }
<st c="7580">foregroundStyle</st>

添加一维条形标记
<st c="8484">黄色</st>
let data: [Sales] = [
Sales(itemType: "Apples", qty: 20, fruitColor:
"Green"),
Sales(itemType: "Apples", qty: 30, fruitColor:
"Red"), <st c="8655">Sales(itemType: "Apples", qty: 40, fruitColor:</st>
<st c="8701">"Yellow"),</st> ]
<st c="8743">绿色</st><st c="8750">红色</st><st c="8759">黄色</st>
Chart(data) {
BarMark( <st c="8876">x: .value("Qty", $0.qty)</st> )
.foregroundStyle(by: .value("Color",
$0.fruitColor))
}
<st c="8999">x</st> <st c="9000">BarMark</st> <st c="9046">BarMark</st> <st c="9118">x</st>
public init<X>(<st c="9316">init()</st> function in this code example is the method that we are using. Now, let’s see what the chart we create looks like when it’s only one-dimensional (*<st c="9469">Figure 9</st>**<st c="9478">.3</st>*):

<st c="9510">Figure 9.3: A 1D chart</st>
<st c="9532">In</st> *<st c="9536">Figure 9</st>**<st c="9544">.3</st>*<st c="9546">, our data is presented in a one-dimensional chart presenting three different types</st> <st c="9630">of apples.</st>
<st c="9640">One thing still bothers us here: notice</st> <st c="9680">that the fruit colors don’t match</st> <st c="9714">the actual colors the Charts framework assigned to each fruit when it created the chart.</st> <st c="9804">That’s because the Charts framework generates the colors while encoding the value.</st> <st c="9887">If we want to match the fruit color to the chart presented color, we need to use the</st> `<st c="9972">chartForegroundStyleScale</st>` <st c="9997">view modifier:</st>
Chart(data) {
BarMark(
x: .value("Qty", $0.qty)
)
.foregroundStyle(by: .value("Color",
$0.fruitColor))
} <st c="10118">.chartForegroundStyleScale("Green" :</st>
*<st c="10431">图 9</st>**<st c="10440">.4</st>* <st c="10442">显示了如何将颜色匹配到</st> <st c="10503">名称后图表的外观:</st>
![图 9.4:具有自定义颜色的 1D 图表
<st c="10547">图 9.4:具有自定义颜色的 1D 图表</st>
<st c="10588">我们可以使用</st> `<st c="10600">chartForegroundStyleScale</st>` <st c="10625">不仅适用于 1D 图表,也适用于所有其他类型的</st> <st c="10678">图表。</st>
<st c="10688">我们看到了如何</st> <st c="10699">使用 BarMarks</st> <st c="10715">进行堆叠和一维标记。</st> <st c="10755">另一种我们可以使用 BarMarks 的方法是用于区间</st> <st c="10807">条形图。</st>
<st c="10818">添加区间条形图</st>
<st c="10845">我们使用</st> **<st c="10853">区间条形图</st>** <st c="10872">来表示按区间分组的数据,例如</st> <st c="10898">时期、年龄组、或</st> <st c="10920">数值范围。</st>
<st c="10965">例如,假设我们想显示一份工人及其在一天中工作的时间间隔的列表。</st>
<st c="11080">首先,让我们创建一个表示一系列</st> <st c="11138">工作时段的数据集:</st>
let emma = "Emma Johnson"
let liam = "Liam Patel"
let sophia = "Sophia Garcia"
let data: [EmployeDayWork] = [
EmployeDayWork(name:emma, startTime: 10, endTime:
12),
EmployeDayWork(name:liam, startTime: 8, endTime:
11),
EmployeDayWork(name: sophia, startTime: 10.5,
endTime: 11.5),
EmployeDayWork(name: emma, startTime: 14, endTime:
15),
EmployeDayWork(name: liam, startTime: 13.5,
endTime: 14.2),
EmployeDayWork(name: sophia, startTime: 15,
endTime: 16)
]
<st c="11610">数据</st> `<st c="11628">数组中的每个项目</st>` <st c="11632">代表一名员工的工时。</st> <st c="11681">请注意,我们并不关心项目的顺序——Charts 框架负责正确地排序它们。</st> <st c="11795">然而,我们关心与员工姓名的一致性,因此 Charts 框架也可以正确地</st> <st c="11904">分组</st> <st c="11905">这些项目。</st>
<st c="11914">让我们看看我们如何基于</st> <st c="11941">那个数据集</st> <st c="11965">构建</st> <st c="11969">一个区间图表:</st>
Chart(data) {
BarMark( <st c="12006">xStart</st>: .value("Start", $0.startTime), <st c="12046">xEnd</st>: .value("End", $0.endTime), <st c="12080">y</st>: .value("Employee", $0.name)
)
}
<st c="12114">在这个代码示例中,我们创建了一个包含新参数的 BarMark 初始化器——</st>`<st c="12198">xStart</st>`<st c="12205">,它表示区间开始的价值,</st> `<st c="12261">xEnd</st>`<st c="12265">,详细说明它结束的位置,以及</st> `<st c="12296">y</st>`<st c="12297">,员工的姓名。</st>
<st c="12319">现在,让我们看看当我们运行它时区间图表看起来像什么(</st>*<st c="12379">图 9</st>**<st c="12388">.5</st>*<st c="12390">):</st>

<st c="12444">图 9.5:一个区间图表</st>
<st c="12473">在</st> *<st c="12477">图 9</st>**<st c="12485">.5</st>*<st c="12487">中,我们可以看到一个时间线,其中每个员工都代表一行,他们的工作周期是这个时间线中的</st><st c="12600">区间。</st> <st c="12622">区间条形图是一个复杂的组件的例子,从头开始构建可能很复杂,而 Charts 框架可以简化</st> <st c="12771">这个过程。</st>
<st c="12783">BarMark 看起来是一个非常灵活的图表类型,这也是它如此常见的原因之一。</st> <st c="12878">它允许我们展示不同类型的信息,无论是比较值还是随时间变化的不同趋势,在堆叠、一维或</st> <st c="13019">区间布局中。</st>
<st c="13036">然而,有时,选择一个更具体的图表</st> <st c="13077">来更精确地表达数据</st> <st c="13107">可能是一个更好的选择。</st>
<st c="13143">所以,让我</st><st c="13156">们设置</st> <st c="13164">LineMark 图表。</st>
<st c="13179">创建 LineMark 图表</st>
<st c="13204">在表格中展示数据的一个挑战是展示趋势和模式。</st> <st c="13245">尽管 BarMark 图表类型比表格做得更好,但还有更好的方法来展示趋势,尤其是在处理大量</st> <st c="13439">信息时。</st>
<st c="13454">为了更有效地展示趋势和模式,我们可以使用 LineMark 图表,它使用表示一系列</st> <st c="13590">数据点的线来表示数据。</st>
<st c="13602">让我们以一个显示随时间变化的手机销售图表为例。</st> <st c="13670">我们创建了一个名为</st> `<st c="13698">SalesFigure</st>` <st c="13709">的结构</st>,其中包含有关产品类型、销售日期和</st> <st c="13790">总金额的信息:</st>
struct SalesFigure: Identifiable {
var id: UUID = UUID()
let product: String
let day: Date
let amount: Double
}
<st c="13915">现在我们有了结构,让我们创建</st> <st c="13958">我们的数据集,就像我们在所有</st> <st c="13990">之前的示例中所做的那样:</st>
let phoneProduct = "Phone"
let salesFigures: [SalesFigure] = [
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714078800), amount:
100),
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714165200), amount:
120),
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714251600), amount:
90),
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714338000), amount:
70)
]
<st c="14450">`salesFigures`</st> <st c="14455">变量包含有关四天销售的信息。</st> <st c="14467">LineMark 图表适合处理多个条目,但我们只使用四个进行</st> <st c="14611">演示目的。</st>
<st c="14634">现在,让我们使用</st> `<st c="14658">salesFigures</st>` <st c="14670">变量通过</st> `<st c="14701">LinkMark</st>` <st c="14709">视图连接到一个图表:</st>
Chart(salesFigures) { <st c="14738">LineMark(</st>
<st c="14747">x: .value("time", $0.day),</st>
<st c="14774">y: .value("amount", $0.amount)</st>
<st c="14805">)</st> }
<st c="14809">我们在图表内创建了一个 LineMark,将日期设置为</st> *<st c="14872">x</st>* <st c="14873">轴,数量设置为</st> *<st c="14901">y</st>* <st c="14902">轴。</st> <st c="14909">运行此代码应显示一个类似于</st> *<st c="14966">图 9</st>**<st c="14974">.6</st>*<st c="14976">:</st>

<st c="15010">图 9.6:一个 LineMark 图表</st>
<st c="15038">图</st> *<st c="15052">9</st>**<st c="15060">.6</st>* <st c="15062">显示了数据集期间手机销售的下降趋势。</st> <st c="15129">关于折线图的好处是,它很容易比较一个 LineMark 与另一个。</st> <st c="15213">我们只需要更新我们的数据集。</st> <st c="15257">因此,让我们也添加平板电脑销售以与</st> <st c="15308">手机销售进行比较:</st>
<st c="15320">let tabletProduct = "Tablet"</st> let salesFigures: [SalesFigure] = [
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714078800), amount:
100), <st c="15481">SalesFigure(product: tabletProduct, day:</st>
<st c="15521">Date(timeIntervalSince1970: 1714078800), amount:</st>
<st c="15570">70),</st> // …
SalesFigure(product: phoneProduct, day:
Date(timeIntervalSince1970: 1714338000), amount:
70), <st c="15675">SalesFigure(product: tabletProduct, day:</st>
<st c="15715">Date(timeIntervalSince1970: 1714338000), amount:</st>
<st c="15764">110)</st> ]
<st c="15771">在这个代码示例中,我们通过向数组中添加平板电脑销售数据项来更新</st> <st c="15804">我们的数据集。</st>
<st c="15866">为了使图表在两种产品类型之间区分开来,我们使用</st> `<st c="15936">foregroundStyle</st>` <st c="15951">视图修饰符:</st>
LineMark(
x: .value("time", $0.day),
y: .value("amount", $0.amount)
)<st c="16099">foregroundStyle</st> view modifier applies different styles to different product types. Looking at the code, we can see that the chart can distinguish between these two types.
<st c="16269">Let’s see what the chart looks like after we have added the tablet sales figures (</st>*<st c="16352">Figure 9</st>**<st c="16361">.7</st>*<st c="16363">):</st>

<st c="16416">Figure 9.7: LineMark chart with two types of product sales figures</st>
*<st c="16482">Figure 9</st>**<st c="16491">.7</st>* <st c="16493">shows tablet sales compared to phone sales.</st> <st c="16538">We can see that while the phone sales declined, the tablet sales increased.</st> <st c="16614">That’s an insight that is difficult to get just from</st> <st c="16667">the dataset.</st>
<st c="16679">Thus far, we have created two primary</st> <st c="16717">types of charts: bar and line charts.</st> <st c="16756">These two types are pretty popular, as they are simple to understand and work for many</st> <st c="16843">use cases.</st>
<st c="16853">Another popular chart type Apple added in iOS 17 is</st> **<st c="16906">SectorMark</st>**<st c="16916">, also known as a</st> <st c="16934">pie chart.</st>
<st c="16944">Creating a SectorMark chart</st>
<st c="16972">A SectorMark, or pie, chart</st> <st c="17000">provides a way to visualize the proportions of different values.</st> <st c="17066">Unlike the other charts, the pie chart is based on a circular shape divided into slices, and each slide represents a different</st> <st c="17193">item value.</st>
<st c="17204">Apparently, the SectorMark chart looks like another form of Stacked Marks we covered earlier (under</st> *<st c="17305">Adding</st>* *<st c="17312">Stacked Marks</st>*<st c="17325">).</st>
<st c="17328">However, SectorMark charts became more popular than Stacked Marks as they are visually appealing and easier to understand.</st> <st c="17452">Moreover, StackedMark and SectorMark charts are excellent for comparing different parts and seeing their contribution to the whole.</st> <st c="17584">However, stacked marks are practical when we want to compare one whole to another, and SectorMark charts are helpful when we want to focus on</st> <st c="17726">one whole.</st>
<st c="17736">Like the previous examples, to create a SectorMark chart, we need to prepare a dataset.</st> <st c="17825">So, let’s create a dataset representing a poll result about</st> <st c="17885">consuming fruits:</st>
let data: [最喜欢的水果] = [
最喜欢的水果(名称:"Apple",价值:30),
最喜欢的水果(名称:"Banana",价值:25),
最喜欢的水果(名称:"Orange",价值:20),
最喜欢的水果(名称:"Strawberries",价值:15),
最喜欢的水果(名称:"Grapes",价值:10)
]
<st c="18148">In this example, we created a structure named</st> `<st c="18195">FavoriteFruit</st>`<st c="18208">, which contains the name of the fruit and the number of people who chose</st> <st c="18282">that fruit.</st>
<st c="18293">To use the data dataset, we will add a</st> `<st c="18333">SectorMark</st>` <st c="18343">view to</st> <st c="18352">our chart:</st>
图表(数据){item in
item.name))
}
<st c="18480">The</st> `<st c="18485">SectorMark</st>` <st c="18495">structure has an angle parameter that reflects the numeric value of the slice.</st> <st c="18575">We also added the</st> `<st c="18593">foregroundStyle</st>` <st c="18608">view modifier, which colors the slice according to the item’s</st> <st c="18671">fruit property.</st>
<st c="18686">Let’s look at what the SectorMark chart looks like when running our code (</st>*<st c="18761">Figure 9</st>**<st c="18770">.8</st>*<st c="18772">):</st>

<st c="18777">Figure 9.8: SectorMark chart</st>
*<st c="18805">Figure 9</st>**<st c="18814">.8</st>* <st c="18816">shows a beautiful, colorful pie</st> <st c="18848">chart, including the legend titles.</st> <st c="18885">We can even set an inner radius to add a</st> **<st c="18926">donut style</st>** <st c="18937">to</st> <st c="18941">the chart:</st>
图表(数据){item in
扇形标记(角度:.value("Value", item.value),<st c="19020">内半径:50</st>)
.foregroundStyle(by: .value("Fruit", item.name))
}
<st c="19088">The addition of the inner radius creates a</st> **<st c="19132">hole</st>** <st c="19136">in the pie chart, as we can see in</st> *<st c="19172">Figure 9</st>**<st c="19180">.9</st>*<st c="19182">:</st>

<st c="19185">Figure 9.9: A SectorMark chart with an inner radius</st>
*<st c="19236">Figure 9</st>**<st c="19245">.9</st>* <st c="19247">shows a donut-shaped SectorMark chart.</st> <st c="19287">This shape allows us to provide more information in the center of the chart.</st> <st c="19364">Some even say that this form is more readable to users as it eliminates the need to</st> <st c="19448">compare angles.</st>
<st c="19463">Until now, we have</st> <st c="19482">created</st> `<st c="19491">BarMark</st>`<st c="19498">,</st> `<st c="19500">LineMark</st>`<st c="19508">, and</st> `<st c="19514">SectorMark</st>` <st c="19524">charts.</st> <st c="19533">The following chart combines two charts we discussed – the LineMark and stacked BarMark charts.</st> <st c="19629">That’s the</st> `<st c="19640">AreaMark</st>` <st c="19648">chart.</st>
<st c="19655">Creating an AreaMark chart</st>
<st c="19682">The stacked BarMark chart</st> <st c="19708">we discussed under the</st> *<st c="19732">Adding Stacked Marks</st>* <st c="19752">section shows two important figures – the total value of a category and how that total is divided into sub-categories while observing the different proportions.</st> <st c="19914">The LineMark chart, on the other hand, shows the trend or patterns between different</st> <st c="19999">data points.</st>
<st c="20011">However, what if we want to combine these two types of marks, showing how a value is composed of different categories</st> <st c="20130">over time?</st>
<st c="20140">That’s what the AreaMark chart</st> <st c="20172">is for.</st>
<st c="20179">Let’s take our LineMark sales figures example.</st> <st c="20227">We have a dataset representing phone and tablet sales over time.</st> <st c="20292">Now, we want to see the total sales of these two types of products over time while still observing the different trends of</st> <st c="20415">each product.</st>
<st c="20428">So, we can create an</st> `<st c="20450">AreaMark</st>` <st c="20458">chart based on the</st> <st c="20478">same dataset:</st>
图表(销售数据){ data in
x: .value("Date", data.day),
y: .value("Sales", data.amount)
)
.foregroundStyle(by: .value("Product",
data.product))
}
<st c="20651">Our code example is identical</st> <st c="20681">to the LineMark example we discussed under the</st> *<st c="20729">Creating LineMark charts</st>* <st c="20753">section; the only difference is that we are now using AreaMark instead</st> <st c="20825">of LineMark.</st>
<st c="20837">However, the result is different (</st>*<st c="20872">Figure 9</st>**<st c="20881">.10</st>*<st c="20884">):</st>

<st c="20889">Figure 9.10: An AreaMark chart for total sales</st>
<st c="20935">At first glance,</st> *<st c="20953">Figure 9</st>**<st c="20961">.10</st>* <st c="20964">shows similar information as</st> *<st c="20994">Figure 9</st>**<st c="21002">.7</st>*<st c="21004">—trends of product sales figures.</st> <st c="21039">However, there are differences.</st> <st c="21071">The filled area in</st> *<st c="21090">Figure 9</st>**<st c="21098">.10</st>* <st c="21101">represents the</st> **<st c="21117">total sales</st>** <st c="21128">of products for both phones and tablets, and each color represents a different product type.</st> <st c="21222">On the other hand,</st> *<st c="21241">Figure 9</st>**<st c="21249">.7</st>* <st c="21251">only shows a comparison between these two product types, side</st> <st c="21314">by side.</st>
<st c="21322">The AreaMark chart</st> <st c="21341">is great for market share analysis, financial data visualization, and general information, including data trends and</st> <st c="21459">cumulative totals.</st>
<st c="21477">However, charts can give us much more than data comparison and trends.</st> <st c="21549">Let’s meet our final chart, PointMark, which can provide a different level</st> <st c="21624">of insight.</st>
<st c="21635">Creating a PointMark chart</st>
<st c="21662">Until now, we have discussed</st> <st c="21691">marks that have helped us compare sales figures or observe trends.</st> <st c="21759">What about areas such as correlation analysis or predictive modeling?</st> <st c="21829">To fulfill that need, the PointMark chart, also known as the</st> **<st c="21890">scatterplot chart</st>**<st c="21907">, aims to show the relatio</st><st c="21933">nships</st> <st c="21940">between</st> <st c="21949">two variables.</st>
<st c="21963">Let’s find the correlation</st> <st c="21990">between students’ study hours and grades.</st> <st c="22033">First, we create a dataset representing</st> <st c="22073">the data:</st>
struct StudentData: Identifiable {
var id: UUID = UUID()
var hoursStudied: Double
var examScore: Double
}
let studentDataSet: [StudentData] =
学生数据(学习时长:1.7 小时,考试成绩:61.8 分),
学生数据(学习时长:7.9 小时,考试成绩:78.6 分),
学生数据(学习时长:4.1 小时,考试成绩:44.3 分),
学生数据(学习时长:4.7 小时,考试成绩:63.4 分),
学生数据(学习时长:7.8 小时,考试成绩:90.4 分),
学生数据(学习时长:8.6 小时,考试成绩:83.2 分),
学生数据(学习时长:2.8 小时,考试成绩:29.7 分),
学生数据(学习时长:6.3 小时,考试成绩:72.9 分),
学生数据(学习时长:6.4 小时,考试成绩:73.8 分),
`)`
<st c="28753">chartOverlay</st> view modifier in this code example.
<st c="22717">This code example has a</st> `<st c="22742">StudentData</st>` <st c="22753">structure containing information about student study time and grades.</st> `<st c="22824">studentsDataSet</st>` <st c="22839">is an array that contains information about</st> <st c="22884">ten students.</st>
<st c="22897">Now, let’s create a</st> `<st c="22918">PointMark</st>` <st c="22927">chart based on</st> <st c="22943">that array:</st>
<st c="29988">图 9.14:图表和 chartOverlay 结构</st>
<st c="23025">y: .value("score", $0.examScore))</st> }
<st c="23061">Like previous charts, the</st> `<st c="23087">PointMark</st>` <st c="23096">structure</st> <st c="23106">has</st> `<st c="23111">x</st>` <st c="23112">and</st> `<st c="23117">y</st>` <st c="23118">parameters.</st> <st c="23131">The</st> `<st c="23135">x</st>` <st c="23136">parameter represents the hours studied, and the</st> `<st c="23185">y</st>` <st c="23186">parameter represents</st> <st c="23208">the score.</st>
*<st c="23218">Figure 9</st>**<st c="23227">.11</st>* <st c="23230">shows what the</st> `<st c="23246">PointMark</st>` <st c="23255">chart looks like when running</st> <st c="23286">the code:</st>

<st c="24895">Figure 9.12: A LinePlot chart</st>
<st c="24924">In</st> *<st c="24928">Figure 9</st>**<st c="24936">.12</st>*<st c="24939">, we can see the</st> `<st c="24956">LinePlot</st>` <st c="24964">chart generated from a simple</st> <st c="24995">mathematical function.</st>
<st c="25017">As mentioned earlier</st> <st c="25038">in this section, the second chart type we can use to visualize</st> <st c="25101">functions is</st> `<st c="25115">AreaPlot</st>`<st c="25123">, the equivalent</st> <st c="25140">of</st> `<st c="25143">AreaMark</st>`<st c="25151">:</st>
y: .value("amount", $0.amount)
`return sin(x)`
`.foregroundStyle(by: .value("Product",`
}
<st c="25210">In this code example, we only changed the chart type from</st> `<st c="25269">LinePlot</st>` <st c="25277">to</st> `<st c="25281">AreaPlot</st>`<st c="25289">.</st> `<st c="25291">AreaPlot</st>` <st c="25299">visualizes the function by filling the area it defines.</st> <st c="25356">Let’s see the output in</st> *<st c="25380">Figure 9</st>**<st c="25388">.13</st>*<st c="25391">:</st>

<st c="25394">Figure 9.13: The AreaPlot chart type</st>
*<st c="25430">Figure 9</st>**<st c="25439">.13</st>* <st c="25442">shows the same sinus function graph, now filled</st> <st c="25491">with color.</st>
<st c="25502">Using the LinePlot and AreaPlot chart types to visualize math functions is about much more than just showing how the sinus function behaves.</st> <st c="25644">It is excellent for education, scientific research, finance, health, and business apps.</st> <st c="25732">Now that we know how to create LinePlot and AreaPlot, we have whole</st> <st c="25800">new options.</st>
<st c="25812">We went over many chart</st> <st c="25836">types, and by now, we can quickly</st> <st c="25870">create charts, just like creating a</st> <st c="25907">simple list!</st>
<st c="25919">The</st> **<st c="25924">List</st>** <st c="25928">type provides a way to interact</st> <st c="25960">with its items, allowing us to navigate or delve into more information.</st> <st c="26033">So, let’s see how to make our</st> <st c="26063">charts interactive!</st>
<st c="26082">Allowing interaction using ChartProxy</st>
<st c="26120">Now that we know how to create</st> <st c="26151">charts, let’s discover more hidden tricks by adding user interaction capabilities.</st> <st c="26235">User interaction in charts, with its many uses, allows users to explore the chart’s data using touch.</st> <st c="26337">Here are some use cases for user interaction</st> <st c="26382">with charts:</st>
* `<st c="26446">BarMark</st>` <st c="26453">or</st> `<st c="26457">SectorMark</st>` <st c="26467">charts, the user can navigate to a new screen that shows additional information about the particular data point.</st> <st c="26581">For example, if the</st> `<st c="26601">BarMark</st>` <st c="26608">chart shows information about watermelon sales, we can navigate the user to a screen that details the watermelon</st> <st c="26722">sales deals.</st>
* `<st c="26794">LineMark</st>` <st c="26802">charts, for example, provides insights to the user on data points not originally part of the dataset if our</st> `<st c="26911">LinkMark</st>` <st c="26919">chart includes information about the growing population in a specific city over time, touching a particular point the chart can display the population value on a</st> <st c="27082">specific date.</st>
* **<st c="27096">Comparing data marks</st>**<st c="27117">: The user can highlight and compare multiple data marks, which is extremely useful in</st> <st c="27205">BarMark-based charts.</st>
<st c="27226">Moreover, learning how to add interaction capabilities can help us explore more things with our charts, such as how the charts are built and how their calculation</st> <st c="27390">logic works.</st>
<st c="27402">To understand how interaction works, we need to get to know more Swift Charts</st> <st c="27481">framework components:</st>
* `<st c="27502">chartOverlay</st>`<st c="27515">: This is a view modifier that helps us add an overlay view to a chart.</st> <st c="27588">We can use the</st> `<st c="27603">chartOverlay</st>` <st c="27615">view modifier to add more graphic details to our chart, such as rulers and texts.</st> <st c="27698">We can also use the</st> `<st c="27718">chartOverlay</st>` <st c="27730">view modifier to observe gestures and</st> <st c="27769">user interaction.</st>
* `<st c="27786">ChartProxy</st>`<st c="27797">: This is a proxy that lets us access the chart values based on the chart area.</st> <st c="27878">Using</st> `<st c="27884">ChartProxy</st>`<st c="27894">, we can convert locations to values and</st> <st c="27935">vice versa.</st>
`<st c="27946">ChartOverlay</st>` <st c="27959">and</st> `<st c="27964">ChartProxy</st>` <st c="27974">are essential components when handling user interaction; therefore, they come hand in hand.</st> <st c="28067">When adding a</st> `<st c="28081">chartOverlay</st>` <st c="28093">view modifier, it comes with a prox</st><st c="28129">y</st> <st c="28131">to have complete access to</st> <st c="28159">the chart.</st>
<st c="28169">Let’s try to take a LineMark chart and add a horizontal ruler that allows users to drag their fingers across it.</st> <st c="28283">We’ll start by adding</st> <st c="28305">an overlay.</st>
<st c="28316">Adding an overlay to our chart</st>
<st c="28347">The solution for providing</st> <st c="28374">an overlay to our chart consists</st> <st c="28407">of a common practice in SwiftUI using a view modifier.</st> <st c="28463">Look at the following</st> <st c="28485">code example:</st>
Chart(salesFigures){}
`<st c="29629">我们将根据用户的</st>` `<st c="29680">点击位置</st>`
`x: .value("time", $0.day),`
`]`
`}`
`<st c="28801">我们可以看到</st>` `<st c="28818">chartOverlay</st>` `<st c="28830">附带了一个</st>` `<st c="28844">代理</st>` `<st c="28849">变量,它就是之前讨论过的</st>` `<st c="28873">ChartProxy</st>` `<st c="28883">组件。</st>`
`$0.product))`
`<st c="28644">.chartOverlay { proxy in`
StudentData(hoursStudied: 6.1, examScore: 77.6)
`LineMark(`
`<st c="28915">ChartOverlay</st>` `<st c="28928">不是一个视图,而是一个视图修饰符,它允许我们在图表中添加新的视图。</st>` `<st c="29004">因此,为了识别手势并添加一个仪表,我们可以添加一个带有拖动手势的透明视图,并添加一个</st>` `<st c="29107">仪表视图:</st>`
.chartOverlay { proxy in
ZStack(alignment: .topLeading) {
Rectangle().fill(.clear)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
}
)
let lineHeight = proxy.plotSize.height
Rectangle()
.fill(.red)
.frame(width: 2, height:
lineHeight)
.position(x: markerX, y:
lineHeight/2)
}
}
`Chart { <st c="24633">LinePlot</st>(x:"x", y:"y") { x in`
@State var markerX: CGFloat = 50
``
`<st c="29420">在这个代码示例中,我们添加了一个</st>` `<st c="29454">ZStack</st>` `<st c="29460">视图,它有一个清晰的矩形覆盖整个图表,在其上方是一个红色的仪表视图。</st>` `<st c="29554">仪表视图</st>` `<st c="29569">x</st>` `<st c="29570">轴是一个</st>` `<st c="29581">状态变量:</st>`
`<st c="29924">要查看我们的视图结构,请看</st>` `<st c="29960">图</st>**<st c="29968">9</st>**<st c="29971">.14</st>` `<st c="29971">:</st>`
`<st c="30310">响应用户的手势</st>`
`}`
`<st c="30034">图</st>**<st c="30043">9</st>**<st c="30046">.14</st>` `<st c="30046">显示了我们的图表视图和通过</st>` `<st c="30105">chartOverlay</st>` `<st c="30117">视图修饰符添加的矩形。</st>` `<st c="30133">我们还可以看到它们是通过</st>` `<st c="30183">代理对象连接的。</st>`
`<st c="30196">此外,我们还添加了</st>` `<st c="30211">一个拖动手势</st>` `<st c="30226">到矩形上。</st>` `<st c="30245">让我们看看如何使用它来相应地改变我们的仪表</st>` `<st c="30289">位置。</st>`
`<st c="29694">注意,我们使用了我们的</st>` `<st c="29719">代理</st>` `<st c="29724">对象来确定</st>` `<st c="29744">仪表视图的图表大小。</st>` `<st c="29780">这是一个关键的代理</st>` `<st c="29801">使用,因为我们还需要在其他场合使用它,例如在特定位置显示不同视图的计算。</st>`
`<st c="30343">为了响应用户的手势</st>` `<st c="30376">并将水平仪表移动到最接近的数据点,我们需要实现</st>` `<st c="30459">onChanged</st>` `<st c="30468">闭包:</st>`
.onChanged { value in <st c="30499">mark</st><st c="30503">erX = value.location.x</st>
<st c="30526">if let closestDate = getClosestDateForLocation(x:</st>
<st c="30576">value.location.x, proxy: proxy) {</st>
<st c="30610">if let positionX = proxy.position(forX:</st>
<st c="30650">closestDate) {</st>
<st c="30665">markerX = positionX</st> }
}
<st c="30689">The</st> `<st c="30694">onChanged</st>` <st c="30703">closure implementation does</st> <st c="30732">three things:</st>
+ <st c="30745">首先,它</st> *<st c="30756">根据点击位置和代理找到最近的销售额数据点</st>* <st c="30790">。 <st c="30836">我们将在一分钟内介绍</st> `<st c="30856">getClosestDateForLocation</st>` <st c="30881">函数。</st>
+ <st c="30903">在我们根据点击位置找到最近的销售数据点后,我们使用代理对象来</st> *<st c="31007">检索它在图表上的位置</st>* <st c="31028">。 <st c="31043">代理的一个功能是将数据点转换为位置,反之亦然。</st>
+ <st c="31128">当我们获得最近的数据点位置时,我们通过设置</st> `<st c="31226">markerX</st>` <st c="31233">状态变量</st>来调整尺子位置。
<st c="31249">这段代码是使用代理对象可以做什么的一个很好的</st> <st c="31278">示例。</st>
<st c="31333">有关代理对象的使用,让我们看看</st> `<st c="31377">getClosestDateForLocation</st>` <st c="31402">函数。</st>
<st c="31412">找到用户触摸点最近的数据点</st>
<st c="31463">`<st c="31468">getClosestDateForLocation</st>` <st c="31493">`函数的目标是</st> <st c="31507">根据一个特定的位置找到最近的数据点。</st> <st c="31569">`根据一个特定的位置找到最近的数据点。</st>
<st c="31587">该函数接收两个参数 - 位置(</st>`<st c="31641">CGFloat</st>`<st c="31649">)和</st> <st c="31660">代理对象:</st>
func getClosestDateForLocation(x: CGFloat, proxy: ChartProxy) -> Date? {
var returnedSalesFigure: SalesFigure? if let date = proxy.value(atX: x) as Date? {
var mDistance: TimeInterval = .infinity
for salesFigure in salesFigures {
let distance =
abs(salesFigure.day.distance(to: date))
if distance < mDistance {
returnedSalesFigure = salesFigure
mDistance = distance
}
}
}
return returnedSalesFigure?.day
}
<st c="32079">记住我们的图表看起来像什么 -</st> *<st c="32121">y</st>* <st c="32122">轴代表时间线,而</st> *<st c="32161">x</st>* <st c="32162">轴代表特定日期的总销售额。</st>
<st c="32214">因此,我们可以使用代理对象来找到特定</st> `<st c="32279">x</st>` <st c="32280">值的日期,这是我们</st> <st c="32303">的第一步:</st>
if let date = proxy.value(atX: x) as Date? {
<st c="32359">代理的</st> `<st c="32371">value(atX:)</st>` <st c="32382">函数计算特定</st> `<st c="32433">x</st>` <st c="32434">值的日期值。</st>
<st c="32441">然而,返回的值是任意的;为了定位最近的数据点,我们必须遍历我们的数据集并搜索最近的</st> `<st c="32581">SalesFigure</st>` <st c="32592">对象。</st> <st c="32601">一旦确定,函数就可以返回它。</st>
<st c="32650">尽管允许用户与图表交互并不复杂,但它包括一些有趣的视图修改器和对象,使我们能够访问图表数据,执行计算,并显示叠加 UI 组件。</st> <st c="32866">我们不必仅仅为了交互而使用</st> `<st c="32891">代理</st>` <st c="32896">对象和</st> `<st c="32912">chartOverlay</st>` <st c="32924">视图修改器——我们可以显示更多信息,改进图表设计,在罕见的情况下,甚至可以</st> <st c="33054">创建自己的图表。</st>
<st c="33064">到目前为止,我们使用的是具有基础类型的数据集 –</st> `<st c="33118">String</st>`<st c="33124">,</st> `<st c="33126">Double</st>`<st c="33132">, 和</st> `<st c="33138">Date</st>`<st c="33142">。然而,当我们查看 Swift Charts 框架的头文件时,我们发现</st> <st c="33212">一些有趣的东西:</st>
func position<P>(forX value: P) -> CGFloat? where P : <st c="33289">Plottable</st> public struct LineMark {
init<X, Y>(x: <st c="33338">PlottableValue</st><X>, y: PlottableValue<Y>)
where X : <st c="33389">Plottable</st>, Y : <st c="33404">Plottable</st> }
<st c="33415">似乎不同的图表函数</st> <st c="33458">只与符合</st> `<st c="33500">Plottable</st>` <st c="33509">协议的类型一起工作。</st> <st c="33520">让我们来看看</st> <st c="33540">那是什么。</st>
<st c="33548">符合 Plottable 协议</st>
<st c="33585">到目前为止,我们一直假设</st> <st c="33630">我们抛到图表上的任何数据集都会工作。</st> <st c="33684">然而,我们发现代理对象可以执行一些有趣的计算,而这些计算用任何数据都是不可能完成的,这也是我们数据类型需要支持在</st> <st c="33885">图表中绘制能力的原因之一。</st>
<st c="33893">因此,Swift Charts 框架只与符合</st> `<st c="33979">Plottable</st>` <st c="33988">协议的数据类型一起工作,这允许数据在</st> <st c="34032">图表中绘制。</st>
<st c="34040">首先,每个原始数据类型已经符合</st> `<st c="34098">Plottable</st>` <st c="34107">协议。</st> <st c="34118">此外,我们在上一个示例中使用的</st> `<st c="34128">Date</st>` <st c="34132">类也符合</st> `<st c="34188">Plottable</st>` <st c="34197">协议。</st> <st c="34208">我们甚至可以在苹果</st> <st c="34242">头文件中看到这一点:</st>
extension Date : Plottable, PrimitivePlottableProtocol
extension String : Plottable, PrimitivePlottableProtocol
<st c="34367">然而,仅与原始类型或 Foundation 类型一起工作并不</st> <st c="34432">总是实用。</st>
<st c="34449">以,例如,我们的</st> `<st c="34479">Sales</st>` <st c="34484">结构体</st> 从 *<st c="34504">Adding Stacked</st>* *<st c="34519">Marks</st>* <st c="34524">部分:</st>
struct Sales: Identifiable {
var id: UUID = UUID()
let itemType: String
let qty: Int <st c="34619">var fruitColor: String = ""</st> }
<st c="34648">将</st> `<st c="34662">itemType</st>` <st c="34670">属性声明为字符串</st> <st c="34691">并不总是最佳实践。</st> <st c="34723">通常,类型是封闭列表的一部分,使用字符串可能会导致拼写错误和重复。</st> <st c="34819">我们可能更愿意使用枚举,因为它</st> <st c="34872">更适合处理类型列表:</st>
enum FruitType {
case Apples
case Oranges
case Watermelons
}
struct Sales: Identifiable {
var id: UUID = UUID() <st c="35032">let itemType: FruitType</st> let qty: Int
var fruitColor: String = ""
}
<st c="35098">在这个例子中,我们创建了一个</st> `<st c="35129">FruitType</st>` <st c="35138">枚举来替换来自 `<st c="35178">String</st>` 的</st> `<st c="35159">itemType</st>` <st c="35167">类型。</st>
<st c="35185">我们的下一步是使</st> `<st c="35215">FruitType</st>` <st c="35224">枚举符合</st> `<st c="35241">Plottable</st>`<st c="35250">:</st>
extension FruitType: <st c="35274">Plottable</st> {
var <st c="35290">primitivePlottable</st>: String {
rawValue
}
}
<st c="35332">在这个例子中,我们使用了</st> `<st c="35362">primitivePlottable</st>` <st c="35380">变量获取器来返回类型的原始值。</st> <st c="35435">这将使</st> `<st c="35455">FruitType</st>` <st c="35464">类型有资格在 Charts 中使用。</st>
<st c="35504">尽管不是每种类型都可以在图表中使用,但我们可以轻松地使它们有资格。</st> <st c="35543">遵守</st> `<st c="35621">Plottable</st>` <st c="35630">协议既简单又直接,并允许我们在图表中使用自定义类型。</st> <st c="35713">这样,我们就可以在我们的图表中使用几乎任何我们想要的数据类型。</st>
<st c="35727">总结</st>
<st c="35735">Swift Charts 框架非常令人兴奋。</st> <st c="35776">它允许我们使用简单的数据集创建令人惊叹的图表,这使得展示数据洞察、趋势和比较变得容易得多。</st>
<st c="35919">本章回顾了 Swift Charts 框架的不同图表类型,包括 BarMark、LineMark、SectorMark、AreaMark 和 PointMark。</st>
<st c="36064">我们还讨论了每个图表的不同用途和目标,学习了如何自定义它们,并添加了用户交互以增加更多功能。</st> <st c="36207">最后,我们回顾了</st> `<st c="36233">Plottable</st>` <st c="36242">协议,它允许我们的图表使用几乎任何我们想要的数据类型。</st> <st c="36314">到目前为止,我们应该能够快速在我们的</st> <st c="36367">应用中实现图表。</st>
<st c="36380">我们的下一章包括一个高级但非常强大的主题——</st> <st c="36445">Swift 宏。</st>
第二部分:使用高级技术完善你的 iOS 开发
-
第十章 , Swift 宏 -
第十一章 , 使用 Combine 创建管道 -
第十二章 , 利用 Apple 智能和 ML 变得聪明 -
第十三章 , 通过应用意图将您的应用暴露给 Siri -
第十四章 , 使用 Swift 测试提高应用质量 -
第十五章 , 探索 iOS 架构
第十一章:10
Swift 宏
-
了解 Swift 宏 -
探索 <st c="694">SwiftSyntax</st>库,它是 Swift 宏 背后的 -
创建我们的第一个 Swift 宏 -
处理错误并在出错时提供更多清晰度 -
测试我们的宏,确保它按预期运行 随时间推移
技术要求
什么是 Swift 宏?
#define SQUARE(x) ((x) * (x))
int num = 5;
int result = SQUARE(num);
<st c="1845">SQUARE</st> <st c="1886">X</st><st c="1923">(</st>``<st c="1924">x) *(x)</st>
-
代码重用 :请注意,代码重用不是“功能重用”。代码重用是指我们取一个实际的代码片段,并在不同的地方重用它。 例如,如果我们经常在声明类时重复相同的行序列,宏可以帮助我们避免 重复自己。 -
提高抽象 :宏可以帮助我们在代码中添加另一个抽象层。 想象一下编写一个生成函数声明的宏。 这是我们可以构建的另一个代码构建 层次。 -
性能 :在某些情况下,宏可以帮助我们优化代码。 有时,优化与可读性/简单性之间的权衡可以通过宏来解决。 宏可以生成更难阅读的代码,但仍然可以进行优化。 宏可以优化的一个特性是 循环展开 ——一种通过指令级并行性更快地迭代循环的方法。 循环展开 生成的代码可读性较低,但速度要快得多。
<st c="3706">log(issue:String)</st> <st c="3852">@AddDebugLogger</st>
<st c="3898">@AddDebugerLogger</st> class MyClass {
}
<st c="3984">MyClass</st> <st c="4019">@AddDebugerLogger</st>
class MyClass {
func printLog(issue: String) {
#if DEBUG
print("In class named MyClass - \(issue)")
#endif
}
}
<st c="4218">printLog()</st>
<st c="4594">SwiftSyntax</st>
探索 SwiftSyntax
<st c="4680">SwiftSyntax</st> <st c="4810">SwiftSyntax</st>
<st c="4873">SwiftSyntax</st>

<st c="5219">*.o</st> <st c="5340">SwiftSyntax</st>
-
解析和抽象语法树(AST) :编译器将我们的源代码转换成一个 AST。 AST 代表我们的代码的层次结构,包括类、结构体、变量和表达式。 -
语义分析(sema) :在这个 阶段,编译器对我们的生成的 AST 执行语义分析。 分析检查我们的代码中的语义问题,并处理类型检查、名称解析等问题(当我们构建阶段看到“语义”问题时;这是该阶段的结果)。 -
Swift 中间语言生成(SILGen) :在这个阶段,编译器 生成一个表示,它捕捉了代码的语义结构。 -
中间表示生成 (IRGen) :在 IRGen 中,编译器将 SILGen 的结果 转换为接近机器级代码的二进制代码。 这个过程是在 低级虚拟机 ( LLVM ) 的帮助下完成的,代码会经过 几个优化。 -
LLVM 链接 :LLVM 将所有内容链接在一起,并为最终 二进制创建做好准备。
解析和 AST
<st c="6919">SwiftSyntax</st> <st c="6966">SwiftSyntax</st>
设置 SwiftSyntax
<st c="7451">SwiftSyntax</st>
<st c="7788">SwiftSyntax</st>
-
让我们从打开 Xcode 并添加一个 新项目 开始。 -
然后,我们将通过选择 <st c="7967">SwiftSyntax</st>Swift 包来添加我们的 文件 | **添加 包依赖… 。 现在,我们处于 Xcode 的添加依赖项窗口中( 图 10 **.2 ):

-
回到 Xcode – 在添加依赖项窗口的右上角,我们可以填写 <st c="9674">SwiftSyntax</st>GitHub 仓库: https://github.com/apple/swift-syntax -
我们将从左侧列中选择 <st c="9762">swift-syntax</st>包,并点击 **添加 **包 **按钮。 -
现在,Xcode 将解析 Swift 包并展示其库,以便我们可以选择想要导入到项目中的内容( 图 10 **.3 ):

<st c="10465">SwiftSyntax</st> <st c="10542">SwiftSyntax</st>
<st c="10639">SwiftSyntax</st>
构建我们的抽象语法树
<st c="10740">SwiftSyntax</st>
import SwiftSyntax
import SwiftSyntaxParser <st c="10949">let</st> <st c="10952">sourceCode</st> = """
func hello() {
print("Hello World")
}
"""
<st c="11069">SwiftSyntax</st> <st c="11085">SwiftSyntaxParser</st><st c="11108">SwiftSyntaxParser</st> <st c="11147">SwiftParser</st>
<st c="11354">sourceCode</st>
<st c="11448">SwiftParser</st>
do { <st c="11467">let syntax = try SyntaxParser.parse(source: sourceCode)</st> } catch {
print("Error parsing code: \(error)")
}
<st c="11610">SyntaxParser</st> <st c="11655">sourceCode</st> <st c="11805">SourceFileSyntex</st>
调查树
<st c="12334">SourceFileSyntax</st>

<st c="13850">代码块项目列表语法</st>
<st c="14011">函数声明语法</st>
<st c="14081">函数声明语法</st> <st c="14195">标识符</st>
<st c="14256">函数声明语法</st> <st c="14282">体</st> <st c="14401">print</st>
<st c="14437">SwiftParser</st>
if let <st c="14575">funcDecl</st> = syntax.statements.first?.item.as(<st c="14619">FunctionDeclSyntax</st>.self) {
// We'll fill that part soon
}
在前面的代码中,我们正在尝试将第一个语句项转换为函数声明类型。
-
<st c="14975">变量声明语法</st>:这是用于 变量的 -
<st c="15018">枚举声明语法</st>:这是用于 枚举声明 -
<st c="15064">类声明语法</st>:这是用于 类声明 -
<st c="15112">协议声明语法</st>:这是用于 协议声明 -
<st c="15166">类型别名声明语法</st>:这是用于类型 别名声明 -
<st c="15223">初始化器声明语法</st>:这是用于 构造声明 -
<st c="15280">运算符声明语法</st>:这是用于 运算符声明
<st c="15575">FunctionDeclSyntax</st>
if let <st c="15603">funcCallExpression</st> = funcDecl.body?.statements.first?.item.as(<st c="15665">FunctionCallExprSyntax</st>.self) {
// Checking the print function
}
<st c="15971">FunctionCallExprSyntax</st><st c="16057">print()</st>
let functionName = funcCallExpression.<st c="16197">calledExpression</st>.firstToken?.text
if functionName == "print" {
let value = funcCallExpression.<st c="16292">argumentList</st>.first?.expression.as(StringLiteralExprSyntax.self)?
.segments
.first?.firstToken?.text
}
<st c="16395">funcCallExpression</st> <st c="16421">calledExpression</st>
<st c="16516">firstToken</st> <st c="16736">text</st>
<st c="16823">print</st><st c="16957">StringLiteralExprSyntax</st><st c="17041">value</st>
<st c="17144">SwiftSyntax</st>
<st c="17448">funcCallExpression</st><st c="17468">calledExpression</st> <st c="17488">StringLiteralExprSyntax</st><st c="17589">SwiftSyntax</st>
<st c="17735">SwiftSyntax</st> <st c="17782">SwiftSyntax</st>
生成 Swift 代码
let initString: String = "<st c="18052">init(title: String) {</st>
<st c="18150">SwiftSyntax</st> types:
let initSyntax = try
}
<st c="18273">In the preceding code,</st> `<st c="18297">InitializerDeclSyntax</st>` <st c="18318">is a constructor declaration, and</st> `<st c="18353">ExprSyntax</st>` <st c="18363">is a base type</st> <st c="18379">for expressions.</st>
<st c="18395">In the context of Swift Macros, in most cases, using</st> `<st c="18449">String</st>` <st c="18455">literals will be enough.</st> <st c="18481">That’s because the</st> `<st c="18500">SwiftSyntax</st>` <st c="18511">types support</st> `<st c="18526">String</st>` <st c="18532">literals.</st> <st c="18543">However, using the built-in</st> <st c="18570">expressions will ensure the generated code will be valid in future</st> <st c="18638">Swift updates.</st>
<st c="18652">Speaking of Swift Macros, let’s create our first Swift macro now that we know what</st> `<st c="18736">SwiftSyntax</st>` <st c="18747">is and how</st> <st c="18759">it works.</st>
<st c="18768">Creating our first Swift macro</st>
<st c="18799">As I</st> <st c="18805">mentioned earlier (in the</st> *<st c="18831">What is a Swift macro?</st>* <st c="18853">section), the Swift Macros feature is part of the</st> `<st c="18904">SwiftSyntax</st>` <st c="18915">library.</st> <st c="18925">Macros don’t run as part of our app but as a plugin in</st> <st c="18980">the IDE.</st>
<st c="18988">Macros can be created by adding a new Swift package with a</st> <st c="19048">macro template.</st>
<st c="19063">It is obvious why Apple selected the Swift package feature to create macros – a Swift package is a great way to encapsulate code, including tests</st> <st c="19210">and documentation.</st>
<st c="19228">Let’s add our first Swift macro by creating a new</st> <st c="19279">Swift package.</st>
<st c="19293">Adding a new Swift macro</st>
<st c="19318">To create a new</st> <st c="19335">Swift macro, we should open Xcode and follow</st> <st c="19380">these steps:</st>
1. <st c="19392">Select</st> **<st c="19400">File</st>** <st c="19404">|</st> **<st c="19407">New</st>** <st c="19410">|</st> **<st c="19413">Package…</st>**<st c="19421">.</st>
2. <st c="19422">Then, select</st> **<st c="19436">Swift Macro</st>** <st c="19447">followed by tapping on</st> **<st c="19471">Next</st>** <st c="19475">(see</st> *<st c="19481">Figure 10</st>**<st c="19490">.5</st>*<st c="19492">):</st>

<st c="19632">Figure 10.5: Selecting Swift Macro in the choose template window</st>
1. <st c="19696">In the</st> <st c="19704">opening screen, we will give a name for our struct and press the</st> `<st c="19936">StructInit</st>` <st c="19946">(see</st> *<st c="19952">Figure 10</st>**<st c="19961">.6</st>*<st c="19963">):</st>

<st c="20054">Figure 10.6: Adding a StructInit macro</st>
1. <st c="20092">After saving, Xcode</st> <st c="20113">opens a window with our new package containing an</st> <st c="20163">example macro.</st>
<st c="20177">Let’s see how a Swift Macros package is</st> <st c="20218">built next!</st>
<st c="20229">Examining our Swift Macros package structure</st>
<st c="20274">Now that we</st> <st c="20286">have a Swift Macros package, we can reveal its file’s structure (</st>*<st c="20352">Figure 10</st>**<st c="20362">.7</st>*<st c="20364">):</st>

<st c="20612">Figure 10.7: The Swift Macros package file’s structure</st>
<st c="20666">Looking at the Swift Macros package (</st>*<st c="20704">Figure 10</st>**<st c="20714">.7</st>*<st c="20716">), we can see that</st> `<st c="20736">SwiftSyntax</st>` <st c="20747">is defined as a dependency of the package for us, with the latest stable version already linked to</st> <st c="20847">our package.</st>
<st c="20859">The macro itself is built upon three different</st> <st c="20907">source files:</st>
* `<st c="20920">StructInit</st>`<st c="20931">: That’s our macro definition file.</st> <st c="20968">Here, we define the macro name</st> <st c="20999">and type.</st>
* `<st c="21008">StructInitClient</st>`<st c="21025">: That’s our Swift Macros package executable product.</st> <st c="21080">This is where we add an executable code that uses</st> <st c="21130">our macro.</st>
* `<st c="21140">StructInitMacros</st>`<st c="21157">: That’s our macro implementation and where all the</st> <st c="21210">magic happens.</st>
<st c="21224">In addition, we also have a</st> `<st c="21253">Test</st>` <st c="21257">target where we can test our</st> <st c="21287">macro code.</st>
<st c="21298">Our first step toward the</st> `<st c="21325">StructInit</st>` <st c="21335">macro is by declaring its name</st> <st c="21367">and type.</st>
<st c="21376">Declaring our macro</st>
<st c="21396">If we</st> <st c="21402">open the</st> `<st c="21412">StructInit</st>` <st c="21422">file, we can see it has a concise yet</st> <st c="21461">important declaration:</st>
@独立的表达式
public macro stringify
类型:"StringifyMacro")
<st c="21631">This short declaration has</st> <st c="21659">many components:</st>
* `<st c="21675">@freestanding(expression)</st>`<st c="21701">: That’s the macro role.</st> <st c="21727">We’ll go over roles in the</st> *<st c="21754">Giving our macro a</st>* *<st c="21773">role</st>* <st c="21777">section.</st>
* `<st c="21786">public macro stringify<T></st>`<st c="21812">: The</st> <st c="21819">macro name.</st>
* `<st c="21830">(_ value: T) -> (T, String)</st>`<st c="21858">: The macro parameters</st> <st c="21882">and output.</st>
* `<st c="21893">#externalMacro</st>`<st c="21908">: This means that the macro will be used as a plug in</st> <st c="21963">the compiler.</st>
* `<st c="21976">module: "StructInitMacros"</st>`<st c="22003">: The name of the plugin that will</st> <st c="22039">be used.</st>
* `<st c="22047">type: "StringifyMacro"</st>`<st c="22070">: That’s the macro type, as defined in the</st> `<st c="22114">Package.swift</st>` <st c="22127">file.</st>
<st c="22133">The first component is the macro role, so let’s discuss what</st> <st c="22195">roles are.</st>
<st c="22205">Giving our macro a role</st>
**<st c="22229">Macro roles</st>** <st c="22241">define</st> <st c="22249">the fundamental behavior of our macros.</st> <st c="22289">There are two primary</st> <st c="22311">role categories:</st>
* `<st c="22473">#</st>` <st c="22474">sign.</st>
<st c="22479">Here’s an example of a</st> <st c="22503">freestanding macro:</st>
```
#URL("https://swift.org/")
```swift
<st c="22549">The</st> `<st c="22554">#URL</st>` <st c="22558">macro checks whether the provided value is a valid URL.</st> <st c="22615">If not, it raises an error on compile time.</st> <st c="22659">Otherwise, it returns a</st> <st c="22683">non-optional value.</st>
<st c="22702">We can see</st> <st c="22713">that the</st> `<st c="22723">#URL</st>` <st c="22727">macro can be anywhere in our code.</st> <st c="22763">That’s why it is</st> <st c="22780">called</st> *<st c="22787">freestanding</st>*<st c="22799">.</st>
* `<st c="22974">@</st>` <st c="22975">sign.</st>
<st c="22980">Here’s an example of an</st> <st c="23005">attached macro:</st>
```
<st c="23020">@StructInit</st> struct Book {
var id: Int
var title: String
var subtitle: String
var description: String
var author: String
}
```swift
<st c="23142">In the preceding code, the</st> `<st c="23170">@StructInit</st>` <st c="23181">macro is “attached” to the</st> `<st c="23209">Book</st>` <st c="23213">struct and inserts an</st> `<st c="23236">init</st>` <st c="23240">function based on the</st> <st c="23263">struct properties.</st>
<st c="23281">The two categories of macro types, namely freestanding and attached, represent distinct sets of roles.</st> <st c="23385">Here is the list of</st> <st c="23405">all roles:</st>
* `<st c="23415">#freestanding(expression)</st>`<st c="23441">: This</st> <st c="23448">just returns a new expression based on an</st> <st c="23491">existing one</st>
* `<st c="23503">#freestanding(declaration)</st>`<st c="23530">: This creates a</st> <st c="23548">new declaration</st>
* `<st c="23563">@attached(peer)</st>`<st c="23579">: This adds new declaration next to the</st> <st c="23620">attached one</st>
* `<st c="23632">@attached(accessor)</st>`<st c="23652">: This adds accessors to</st> <st c="23678">a property</st>
* `<st c="23688">@attached(memberAttribute)</st>`<st c="23715">: This adds attributes to the declarations in the type it’s</st> <st c="23776">attached to</st>
* `<st c="23787">@attached(member)</st>`<st c="23805">: This adds new declarations inside the type it’s</st> <st c="23856">attached to</st>
* `<st c="23867">@attached(conformance)</st>`<st c="23890">: This</st> <st c="23898">adds conformance to the type it’s</st> <st c="23932">attached to</st>
<st c="23943">The role we define when we declare the macro tells the created plugin</st> *<st c="24014">how to</st>* <st c="24020">change an</st> <st c="24031">existing code.</st>
<st c="24045">The role is the first part of declaring a macro.</st> <st c="24095">Let’s continue with the rest of</st> <st c="24127">the declaration.</st>
<st c="24143">Defining the StructInit macro</st>
<st c="24173">Our</st> `<st c="24178">StructInit</st>` <st c="24188">macro goal is to</st> <st c="24205">create the init method for a struct.</st> <st c="24243">Our macro doesn’t exist independently; its purpose is to insert new declarations into an existing struct.</st> <st c="24349">Therefore, we will choose the</st> `<st c="24379">@attached(member)</st>` <st c="24396">macro from the roles list in the</st> *<st c="24430">Giving our macro a</st>* *<st c="24449">role</st>* <st c="24453">section:</st>
@附加的成员
<st c="24480">However, mentioning the role type is not enough.</st> <st c="24530">We also need to specify what declaration types we expect our macro to generate.</st> <st c="24610">In this case, we expect the macro to generate an</st> `<st c="24659">init</st>` <st c="24663">function.</st> <st c="24674">Let’s add that to the</st> <st c="24696">role declaration:</st>
@附加的成员,名称:命名(init)
<st c="24751">Adding role types helps the compiler cover different cases where the macro generates something else that was not declared.</st> <st c="24875">It also behaves as a documentation for</st> <st c="24914">our macro.</st>
<st c="24924">Here is another example of</st> `<st c="24952">names</st>` <st c="24957">argument usage:</st>
@附加的成员,名称:命名(rawValue)
<st c="25015">In this case, the</st> `<st c="25034">names</st>` <st c="25039">argument declares a usage of the</st> `<st c="25073">RawValue</st>` <st c="25081">declaration.</st>
<st c="25094">We can also add</st> `<st c="25111">arbitrary</st>` <st c="25120">for</st> <st c="25125">general purposes:</st>
@附加的成员,名称:任意
<st c="25178">Using</st> `<st c="25185">arbitrary</st>` <st c="25194">counts for all types</st> <st c="25216">of declarations.</st>
<st c="25232">Moving forward, we will reconfigure the predefined macro with the</st> <st c="25299">following declaration:</st>
@附加的成员,名称:命名(init)
public macro
"<st c="25541">StructInit</st>.
<st c="25552">这个宏虽然简短,但讲述了它的目标和行为。</st> <st c="25617">接下来是重要的部分——</st> <st c="25653">宏的实现。</st>
<st c="25674">实现宏</st>
<st c="25697">与其他 Swift 类型不同,在</st> <st c="25727">宏中,我们将声明和实现分开到不同的文件中。</st> <st c="25804">从某种意义上说,这类似于 Objective-C 或 C++,当时头文件和实现是</st> <st c="25891">其他部分。</st>
<st c="25903">我们将打开我们的</st> `<st c="25921">StructInitMacros</st>` <st c="25937">文件,并清除其内容以从头开始。</st> <st c="25984">之后,我们可以继续导入</st> <st c="26024">相关库:</st>
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
<st c="26140">这些是我们将要编写的宏中的标准库。</st> <st c="26204">请注意,我们有</st> `<st c="26224">SwiftSyntax</st>` <st c="26235">和</st> `<st c="26240">SwiftSyntaxBuilder</st>` <st c="26258">作为我们在</st> *<st c="26296">探索</st> * *<st c="26306">SwiftSyntax</st> * <st c="26317">部分学到的内容的一部分。</st>
<st c="26326">现在,让我们进入主菜——</st> `<st c="26369">StructInit</st>` <st c="26379">结构体。</st>
<st c="26387">声明 StructInit 结构体</st>
<st c="26419">在 Swift 宏中,Apple</st> <st c="26443">继续其主要使用结构体和协议而不是类</st> <st c="26527">和继承的趋势。</st>
<st c="26543">要实现一个新的宏,我们将添加一个新的结构体,其名称与名为</st> <st c="26649">MemberMacro</st> <st c="26660">的协议</st> <st c="26643">相符合</st>:
<st c="26662">public</st> struct StructInit: <st c="26688">MemberMacro</st> {
public static func expansion() {
//Implementation details are detailed in the next section
}
}
<st c="26796">编译器会在</st> *<st c="26900">添加新的 Swift 宏</st>* <st c="26924">部分下寻找与我们之前声明的宏名称相同的结构体。</st> <st c="26934">我们还声明了</st> `<st c="26955">StructInit</st>` <st c="26965">为</st> `<st c="26969">public</st>` <st c="26975"> – 记住,宏是 Swift 包的一部分,因此我们需要确保它可以从其他模块中访问。</st>
<st c="27088">那么,什么是</st> `<st c="27105">MemberMacro</st>` <st c="27116">协议呢?</st> <st c="27127">`<st c="27131">MemberMacro</st>` <st c="27142">协议包含一个执行展开操作的关键函数,其名称非同寻常,为</st> `<st c="27250">expansion()</st>`<st c="27264">。</st>
<st c="27265">然而,我们不会在创建每个宏时都使用</st> `<st c="27288">MemberMacro</st>` <st c="27299">,因为它只与宏的</st> `<st c="27368">attached(member)</st>` <st c="27384">角色相关。</st> <st c="27391">每个角色都有一个我们需要</st> <st c="27437">遵守的协议。</st>
<st c="27448">以下是不同角色及其</st> <st c="27499">对应协议的列表:</st>
+ `<st c="27522">@freestanding(expression) -></st>` `<st c="27552">ExpressionMacro</st>`
+ `<st c="27567">@freestanding(declaration) -></st>` `<st c="27598">DeclarationMacro</st>`
+ `<st c="27614">@attached(peer) -></st>` `<st c="27634">PeerMacro</st>`
+ `<st c="27643">@attached(accessor) -></st>` `<st c="27667">AccessorMacro</st>`
+ `<st c="27680">@attached(memberAttribute) -></st>` `<st c="27711">MemberAttributeMacro</st>`
+ `<st c="27731">@attached(member) -></st>` `<st c="27753">MemberMacro</st>`
+ `<st c="27764">@attached(conformance) -></st>` `<st c="27791">ConformanceMacro</st>`
<st c="27807">由于我们正在使用</st> <st c="27831">Swift 宏并具有</st> `<st c="27853">@attached(member)</st>` <st c="27870">角色,我们将只关注</st> `<st c="27899">MemberMacro</st>`<st c="27910">,尽管这个概念与其他协议类似。</st>
<st c="27970">让我们一起来了解一下!</st>
<st c="27997">实现展开函数</st>
<st c="28033">我将首先</st> <st c="28048">向您展示</st> `<st c="28064">expansion</st>` <st c="28073">函数:</st>
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some
DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [SwiftSyntax.DeclSyntax]
<st c="28269">虽然这个函数可能看起来有点复杂,但我们需要记住</st> <st c="28340">两件事:</st>
1. <st c="28351">在函数签名中提到的多数类型应该对我们来说已经熟悉,因为它们是</st> `<st c="28467">SwiftSyntax</st>` <st c="28478">库的组成部分。</st>
1. <st c="28487">这个协议中只有一个函数。</st> <st c="28532">无需实现</st> <st c="28553">另一个函数!</st>
<st c="28565">扩展函数的目的是接收有关附加对象或宏参数的信息,并返回一个表示为</st> `<st c="28728">SwiftSyntax</st>` <st c="28739">表达式数组(</st>`<st c="28753">DeclSyntax</st>`<st c="28764">)的 Swift 代码片段。</st>
<st c="28767">扩展函数有三个参数:</st>
1. `<st c="28812">节点:AtributeSyntax</st>`<st c="28833">:此节点表示原始 Swift 代码中的实际宏。</st>
1. `<st c="28911">声明:some DeclGroupSyntax</st>`<st c="28945">:描述宏附加到的结构/类的声明结构。</st>
1. `<st c="29028">上下文:some MacroExpansionContext</st>`<st c="29064">:上下文为我们提供了更多关于编译器的信息。</st> <st c="29133">记住,编译器作为宏函数的“环境”。</st>
<st c="29217">现在,我们可以开始创建我们的结构</st> `<st c="29256">init</st>` <st c="29260">方法。</st>
<st c="29268">首先,我们需要有一个包含所有结构属性的列表,包括名称和类型。</st> <st c="29356">为此,我们需要使用</st> `<st c="29402">SwiftSyntax</st>`<st c="29413">分析代码,这是我们本章刚刚学习的内容(在</st> *<st c="29462">探索</st>* *<st c="29472">SwiftSyntax</st>* <st c="29483">部分)。</st>
<st c="29493">因此,让我们获取</st> <st c="29507">我们需要的所有结构信息:</st>
let members = declaration.memberBlock.members <st c="29595">// 1</st> let variableDecl = members.compactMap {
$0.decl.as(VariableDeclSyntax.self) } <st c="29678">// 2</st> let variablesName = variableDecl.compactMap {
$0.bindings.first?.pattern } <st c="29758">// 3</st> let variablesType = variableDecl.compactMap {
$0.bindings.first?.typeAnnotation?.type } <st c="29851">// 4</st>
<st c="29855">让我们逐行解释前面的代码:</st>
1. <st c="29903">我们使用声明参数来获取所有</st> <st c="29952">结构成员。</st>
1. <st c="29967">所有结构成员也包括它们的功能,所以我们只</st> <st c="30042">过滤变量。</st>
1. <st c="30055">我们使用变量的</st> `<st c="30115">pattern</st>` <st c="30122">属性创建所有变量名的数组。</st>
1. <st c="30133">我们使用变量的</st> `<st c="30201">typeAnnotation</st>` <st c="30215">属性创建所有变量类型的数组。</st>
<st c="30226">现在我们有了所有需要的信息,我们可以生成</st> `<st c="30312">init</st>` <st c="30316">函数的 Swift 代码。</st>
<st c="30326">首先,我们根据变量名</st> `<st c="30350">init</st>` <st c="30354">函数签名基于变量名列表和类型:</st>
var code = "<st c="30433">init(</st>"
for (name, type) in zip(variablesName, variablesType) {
code += <st c="30506">"\(name): \(type),</st> "
}
code = String(code.dropLast(2))
code += "<st c="30570">)</st>"
<st c="30573">前面的代码首先创建一个可变字符串,遍历所有变量名和类型,并将它们添加到函数签名中。</st> <st c="30714">一旦代码添加了所有函数参数,它就通过一个</st> <st c="30779">闭括号</st>结束。
<st c="30799">接下来,是时候</st> <st c="30819">添加函数体了。</st> <st c="30842">我们可以使用一个特殊的</st> `<st c="30873">SwiftSyntax</st>` <st c="30884">结构体,它代表一个初始化器声明</st> <st c="30935">称为</st> `<st c="30942">InitializerDeclSyntax</st>`<st c="30963">:</st>
let initializer = try <st c="30988">InitializerDeclSyntax</st>(SyntaxNodeString
(stringLiteral: code)) {
for name in variablesName {
ExprSyntax("self.\(name) = \(name)")
}
}
<st c="31121">`<st c="31126">InitializerDeclSyntax</st>` <st c="31147">“init”函数接收两个参数——函数签名和一个包含“init”体</st> <st c="31260">的闭包,该体由</st> `<st c="31263">ExprSyntax</st>`<st c="31273">表示。</st>
<st c="31274">现在我们有了</st> `<st c="31292">initializer</st>`<st c="31303">,我们可以返回一个</st> <st c="31328">DeclSyntax</st>`<st c="31331">数组:</st>
return [DeclSyntax(initializer)]
<st c="31376">让我们看看</st> <st c="31391">完整的代码:</st>
let members = structDecl.memberBlock.members
let variableDecl = members.compactMap {
$0.decl.as(VariableDeclSyntax.self) }
let variablesName = variableDecl.compactMap {
$0.bindings.first?.pattern }
let variablesType = variableDecl.compactMap {
$0.bindings.first?.typeAnnotation?.type }
var code = "<st c="31700">init(</st>"
for (name, type) in zip(variablesName,
variablesType) {
code += <st c="31773">"\(name): \(type),</st> "
}
code = String(code.dropLast(2))
code += "<st c="31837">)</st>"
let initializer = try InitializerDeclSyntax(SyntaxNodeString
(stringLiteral: code)) {
for name in variablesName {
ExprSyntax("<st c="31967">self.\(name) = \(name)</st>")
}
}
return [DeclSyntax(initializer)]
<st c="32030">该代码接受变量结构体列表并生成自己的</st> `<st c="32097">init</st>` <st c="32101">函数。</st>
<st c="32111">它看起来怎么样?</st> <st c="32130">让我们</st> <st c="32135">用一个</st> <st c="32160">小的结构体</st> <st c="32165">来演示:</st>
struct Book {
var id: Int
var title: String
}
<st c="32219">`<st c="32224">expansion</st>` <st c="32233">方法</st> <st c="32241">创建了以下</st> `<st c="32263">init</st>` <st c="32267">函数:</st>
init(id: Int, title: String) {
self.id = id
self.title = title
}
<st c="32342">但我们定义宏行为的事实并不意味着我们可以使用它。</st> <st c="32424">记住,宏作为编译器插件运行。</st> <st c="32475">这是我们</st> <st c="32486">下一步要做的。</st>
<st c="32496">添加编译器插件</st>
<st c="32523">编译器插件是我们的</st> <st c="32551">宏“product”,或者换句话说,宏</st> <st c="32598">入口点。</st>
<st c="32610">在 iOS 中,宏在无网络访问和系统文件更改的沙盒中调用。</st> <st c="32699">问题是这样的:编译器如何实例化和存储 Swift 宏以用作</st> <st c="32792">插件?</st>
<st c="32801">答案是否定的。</st> <st c="32833">如果我们再次审视我们的代码,我们会注意到 Swift 宏函数都是静态的,这在创建一个</st> <st c="32973">新宏时是一个重要的问题。</st>
<st c="32983">因此,要创建一个编译器插件,我们需要定义一个新的符合</st> `<st c="33069">CompilerPlugin</st>` <st c="33083">协议并具有</st> `<st c="33105">@main</st>` <st c="33110">属性标记的</st>结构体:</st>
@main
struct struct_initial_macroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StructInit.self,
]
}
<st c="33243">前面的代码显示,`<st c="33274">struct_initial_macroPlugin</st>` <st c="33300">实现了一个变量</st> `<st c="33325">get</st>` <st c="33328">方法——</st> `<st c="33338">providingMacros</st>` <st c="33353">——并返回一个宏类型的数组而不是实例。</st>
<st c="33413">在此处需要注意的另一件重要事情是结构体名称(</st>`<st c="33473">struct_initial_macroPlugin</st>`<st c="33500">)。</st> <st c="33504">只要它符合</st> <st c="33566">`<st c="33573">CompilerPlugin</st>`</st> <st c="33587">协议并具有</st> `<st c="33609">@</st>``<st c="33610">main</st>` <st c="33614">属性,我们给它取什么名字都无关紧要。</st>
<st c="33625">现在我们有了编译器插件,我们的编译器已经准备好</st> <st c="33687">运行它。</st>
<st c="33694">使用客户端运行我们的宏</st>
<st c="33727">宏可执行文件与应用程序或库不同,因为它们在编译器环境中运行。</st> <st c="33825">如果我们回到本章中创建 Swift 宏 Swift 包的部分(</st>*<st c="33917">检查我们的 Swift 宏包结构</st>* <st c="33962">部分),我们会看到 Swift 宏还有一个名为</st> `<st c="34020">StructInitClient</st>`<st c="34043">的文件夹。</st>
`<st c="34044">StructInitClient</st>` <st c="34061">是我们 Swift 宏可执行文件,也定义在宏的</st> `<st c="34121">package.swift</st>` <st c="34134">清单文件中:</st>
.executable(
name: "StructInitClient",
targets: ["StructInitClient"]
),
<st c="34220">现在,我们可以将我们在</st> `<st c="34264">main.swift</st>` <st c="34274">文件中的代码改为</st> <st c="34283">以下内容:</st>
import StructInit
import Foundation <st c="34334">@StructInit</st> struct Book {
var id: Int
var title: String
var subtitle: String
var description: String
var author: String
}
<st c="34455">在前面</st> <st c="34473">的代码中,我们有一个名为</st> `<st c="34509">Book</st>`<st c="34513">的简单结构体,但现在,我们还附加了我们刚刚创建的</st> `<st c="34550">@StructInit</st>` <st c="34561">宏。</st>
<st c="34584">右键单击宏本身并选择</st> **<st c="34628">展开宏</st>**<st c="34640">,这将揭示生成的代码(</st>*<st c="34676">图 10</st>**<st c="34686">.8</st>*<st c="34688">):</st>

<st c="35057">图 10.8:Swift 宏展开</st>
<st c="35091">使用我们的宏可执行文件是查看宏实际运行效果的好方法!</st> <st c="35162">此时,一切应该都按预期工作。</st> <st c="35213">现在是时候通过一些</st> <st c="35270">错误处理</st>来提升我们的宏实现。</st>
<st c="35285">处理宏错误</st>
<st c="35308">当我们创建一个</st> <st c="35326">Swift 宏时,对我们这些宏开发者来说显而易见的事情,对我们这些</st> <st c="35409">宏用户来说并不明显。</st>
<st c="35421">我们的</st> `<st c="35426">StructInit</st>` <st c="35436">宏设计为仅与结构体一起使用,而不是类。</st> <st c="35506">因此,我们需要检查附加的元素是否确实是</st> <st c="35573">一个结构体。</st>
<st c="35582">在</st> `<st c="35594">expansion()</st>` <st c="35605">函数内部,我们可以执行一个简单的</st> `<st c="35640">guard</st>` <st c="35645">语句,并在附加的声明不是</st> <st c="35715">结构体的情况下抛出一个错误:</st>
guard let structDecl = declaration.as(StructDeclSyntax.self)
else {
throw StructInitError.onlyStructs
}
<st c="35828">在上面的代码中,</st> `<st c="35852">StructInitError</st>` <st c="35867">是一个符合</st> <st c="35893">的枚举</st> `<st c="35896">Error</st>`<st c="35901">:</st>
enum StructInitError: CustomStringConvertible, Error {
case onlyStructs
var description: String {
switch self {
case . onlyStructs: return "@StructInit can only be applied to a structure"
}
}
}
<st c="36097">拥有一个具有不同错误类型和消息的枚举可以使开发者的生活变得更加容易。</st> <st c="36194">记住,这个错误出现在编译时(</st>*<st c="36244">图 10.9</st>**<st c="36254">.9</st>*<st c="36256">):</st>

<st c="36309">图 10.9:实现 Swift 宏时抛出一个错误信息</st>
<st c="36380">但有时,我们想要处理更复杂的错误。</st> <st c="36435">例如,有时我们只想显示警告,而不仅仅是错误。</st> <st c="36504">或者,在其他情况下,我们甚至想为</st> <st c="36570">开发者的问题提供一个解决方案。</st>
<st c="36584">在这些情况下,我们可以</st> <st c="36608">添加一个名为</st> `<st c="36631">Diagnostic</st>` <st c="36641">的结构体。</st> <st c="36650">一个</st> `<st c="36652">Diagnostic</st>` <st c="36662">结构体更适合在编译器环境中显示错误,并且比仅仅</st> <st c="36768">抛出错误</st> 具有更多功能。
<st c="36784">让我们创建一个</st> `<st c="36800">DiagnosticMessage</st>` <st c="36817">枚举和一个</st> `<st c="36829">Diagnostic</st>` <st c="36839">结构体:</st>
enum CustomDiagnostic: String, DiagnosticMessage {
case notAStruct
var <st c="36919">severity</st>: DiagnosticSeverity { return .error}
var <st c="36970">message</st>: String {
switch self {
case .notAStruct:
return "@StructInit can only be applied to a structure"
}
}
var <st c="37085">diagnosticID</st>: MessageID {
return MessageID(domain: "StructInitMacro",
id: rawValue)
}
} <st c="37174">let diagnostic = Diagnostic(node: node,</st>
<st c="37383">SwiftSyntax</st> library.
<st c="37403">If you wondered why we need the</st> `<st c="37436">context</st>` <st c="37443">parameter in the</st> `<st c="37461">expansion</st>` <st c="37470">function, now you’ll have</st> <st c="37497">the answer:</st>
context.diagnose(diagnostic)
<st c="37537">Remember we said that context links us to the compiler environment?</st> <st c="37606">So, we use it to invoke a</st> <st c="37632">diagnostic message.</st>
<st c="37651">Let’s see the</st> `<st c="37666">guard</st>` <st c="37671">declaration now that we have a</st> `<st c="37703">diagnostic</st>` <st c="37713">structure:</st>
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
let diagnostic = Diagnostic(node: node,
message: MyLibDiagnostic.notAStruct) <st c="37870">context.diagnose(diagnostic)</st> throw StructInitError.onlyAStruct
}
<st c="37934">We can see that</st> `<st c="37951">SwiftSyntax</st>` <st c="37962">is like peeling an onion – we uncover new features every time we dig deeper, and</st> `<st c="38044">Diagnostic</st>` <st c="38054">is one of</st> <st c="38065">these features.</st>
<st c="38080">Now, we have a significant error handling – descriptive and precise.</st> <st c="38150">But what about checking our macro in various</st> <st c="38195">use cases?</st>
<st c="38205">To see our macro at work, we used</st> `<st c="38240">StructInitClient</st>`<st c="38256">. However, relying on the client to verify that</st> <st c="38303">our macro works as expected is not sustainable</st> <st c="38351">over time.</st>
<st c="38361">So, another great feature we get from having a macro written in a Swift package is</st> <st c="38445">unit tests.</st>
<st c="38456">Let’s see how we test</st> <st c="38479">a macro.</st>
<st c="38487">Adding tests</st>
<st c="38500">The principle of testing a macro</st> <st c="38533">is to test a code block</st> *<st c="38558">before and after</st>* <st c="38574">the</st> <st c="38579">macro expansion.</st>
<st c="38595">As part of our Swift Macros package, we have a test target (</st>*<st c="38656">Figure 10</st>**<st c="38666">.10</st>*<st c="38669">):</st>

<st c="38877">Figure 10.10: A testing target for StructInitMacro</st>
<st c="38927">Each Swift package comes with a testing target, and in this case, we already have one test with the</st> `<st c="39028">stringify</st>` <st c="39037">macro that comes when we create a new Swift</st> <st c="39082">Macros package.</st>
<st c="39097">Let’s clear the test file and start our</st> <st c="39138">own test.</st>
<st c="39147">To test a macro, we need to create the</st> `<st c="39187">XCTestCase</st>` <st c="39197">subclass and create a new method called</st> `<st c="39238">testMacro</st>`<st c="39247">. Remember that test functions in</st> `<st c="39281">XCTest</st>` <st c="39287">always start with the phrase “test” followed by the</st> <st c="39340">test name.</st>
<st c="39350">To test a macro expansion, we will use a particular</st> `<st c="39403">SwiftSyntax</st>` <st c="39414">function called</st> `<st c="39431">assertMacroExpansion</st>`<st c="39451">. The most important function parameters are</st> <st c="39496">as follows:</st>
* `<st c="39507">_originalSource</st>`<st c="39523">: The original code before the expansion, including the macro</st> <st c="39586">attribute itself</st>
* `<st c="39602">expandedSource</st>`<st c="39617">: The code</st> *<st c="39629">after</st>* <st c="39634">the expansion</st>
* `<st c="39648">macros</st>`<st c="39655">: The list of macro types</st> <st c="39682">being tested</st>
<st c="39694">Let’s see a basic</st> <st c="39712">test case for testing our</st> `<st c="39739">StructInit</st>` <st c="39749">macro:</st>
let testMacros: [String: Macro.Type] = [
"StructInit": StructInit.self,
]
final class StructInitTests: XCTestCase {
func testMacro() {
assertMacroExpansion(
"""
@StructInit
struct Book {
var id: Int
var title: String
var subtitle: String
}
""",
expandedSource:
"""
struct Book {
var id: Int
var title: String
var subtitle: String
init(id: Int, title: String,
subtitle: String) {
self.id = id
self.title = title
self.subtitle = subtitle
}
}
""",
macros: testMacros
)
}
}
<st c="40226">We can see</st> <st c="40238">that</st> `<st c="40243">assertMacroExpansion</st>` <st c="40263">received the three parameters I</st> <st c="40296">mentioned earlier.</st>
<st c="40314">We compare the</st> `<st c="40330">Book</st>` <st c="40334">struct expansion with the</st> `<st c="40361">Book</st>` <st c="40365">struct desired structure, including the</st> `<st c="40406">init</st>` <st c="40410">function.</st>
`<st c="40420">assertMacroExpansion</st>` <st c="40441">compares the expanded code of the macro to the</st> `<st c="40489">expandedSource</st>` <st c="40503">parameter, and if there are any differences, it fails</st> <st c="40558">the test.</st>
<st c="40567">Testing is a crucial part of Swift packages in general.</st> <st c="40624">Swift packages are meant to be reusable and rely on testing to ensure</st> <st c="40694">their stability.</st>
<st c="40710">Things get even more important when creating Swift macros since they run as a compiler plugin, which makes it harder to debug.</st> <st c="40838">So, we shouldn’t give up tests, especially not</st> <st c="40885">in macros.</st>
<st c="40895">Practice exercises</st>
<st c="40914">Swift Macros is a complex topic, and it is a challenge to understand how to create a Swift macro without trying it yourself.</st> <st c="41040">Here are two exercises that can help you</st> <st c="41081">get started:</st>
* <st c="41093">Create an attached Swift macro that adds a function called</st> `<st c="41153">printVariables</st>`<st c="41167">. The function prints the list of the class properties and</st> <st c="41226">their values.</st>
* <st c="41239">Create a freestanding macro called</st> `<st c="41275">#colorhex</st>` <st c="41284">that receives a hex color value and generates an RGB color expression.</st> <st c="41356">For example,</st> `<st c="41369">#colorhex("#FFFFFF")</st>` <st c="41389">will generate</st> `<st c="41404">Color(red: 0.0, green: 0.0,</st>` `<st c="41432">blue: 0.0)</st>`<st c="41442">.</st>
<st c="41443">In addition, here are some links that can help you get more insights about</st> <st c="41519">Swift Macros:</st>
* **<st c="41532">Swift Macros documentation from the Swift.org</st>** **<st c="41579">projects</st>**<st c="41587">:</st> <st c="41589">https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/</st>
* **<st c="41676">A GitHub repository about great Swift macros we can use and learn</st>** **<st c="41743">from</st>**<st c="41747">:</st> [<st c="41750">https://github.com/krzysztofzablocki/Swift-Macros</st>](https://github.com/krzysztofzablocki/Swift-Macros)
<st c="41799">Summary</st>
<st c="41807">This chapter covered a new and exciting feature of Xcode 15 and iOS 17 –</st> <st c="41881">Swift Macros.</st>
<st c="41894">We explored the</st> `<st c="41911">SwiftSyntax</st>` <st c="41922">library and learned how to set up, parse, and generate Swift code.</st> <st c="41990">We also created our first Swift macro, handled errors, and even wrote</st> <st c="42060">one test.</st>
<st c="42069">Swift Macros is a comprehensive, complex, yet effective feature, and by now, you are ready to implement it in your</st> <st c="42185">own projects!</st>
<st c="42198">In the next chapter, we’ll discuss another exciting framework –</st> <st c="42263">Combine.</st>
第十二章:11 (此处内容为代码,无需翻译)
创建 Combine 中的管道 (此处内容为代码,无需翻译)
数据流是编程的核心主题,不仅限于 iOS 开发。
Apple 的响应式编程版本是 Combine,这是一个提供构建应用中数据流基础设施的框架。
在本章中,我们将进行以下操作:
-
讨论在项目中使用 Combine 的原因
(此处内容为代码,无需翻译) (此处内容为代码,无需翻译) -
复习基本知识
(此处内容为代码,无需翻译) (此处内容为代码,无需翻译) -
深入了解 Combine
(此处内容为代码,无需翻译) (此处内容为代码,无需翻译) -
通过示例学习 Combine
(此处内容为代码,无需翻译) (此处内容为代码,无需翻译)
在我们开始介绍 Combine 框架之前,让我们了解为什么我们应该使用 Combine。
技术要求 (此处内容为代码,无需翻译)
对于本章,从 App Store 下载 Xcode 版本 16.0 或更高版本是至关重要的。
确保您正在使用最新的 macOS 版本(Ventura 或更高版本)。
从以下 GitHub 链接下载示例代码:
为什么使用 Combine?(此处内容为代码,无需翻译)
Apple 的 Combine 框架被认为学习曲线陡峭,但这并不是因为它在技术上复杂。
为了回答这些问题,让我们尝试了解 Combine。
但为什么我们需要一个响应式框架?
让我们看看我们的 iOS SDK 中有哪些内容:
-
通知 允许我们发送任何对象 都可以观察到的消息 -
代表 允许对象响应 由其他对象触发的事件或变化 -
闭包 是自包含的功能 块,我们可以传递并随时调用 -
键值观察 ( KVO ) 允许我们观察对象属性中的值变化
<st c="3181">消息</st> <st c="3246">消息</st>
messageSubscriber = viewModel.$message
.sink { [weak self] message in
self?.label.text = message
}
<st c="3414">sink</st> <st c="3468">消息</st> <st c="3516">消息</st> <st c="3551">消息</st> <st c="3592">文本值</st> 中。
<st c="3656">viewModel</st>
了解基础知识
从发布者开始
URLSession.shared.dataTaskPublisher(for: url)
<st c="4565">发布者</st> <st c="4589">URLSession</st> <st c="4637">计时器</st> <st c="4647">通知中心</st>
NotificationCenter.default.publisher(for:
Notification.Name("DataValueChanged"))
<st c="4806">计时器</st>
let timerPublisher = Timer.publish(every: 1.0, on: .main,
in: .default)
.autoconnect()
extension UserDefaults {
@objc dynamic var test: Int { return integer(forKey:
"myProperty") }
}
let userDefaultsPublisher = UserDefaults.standard
.publisher(for: \. myProperty)
设置订阅者
<st c="5484">订阅者</st> <st c="5625">接收器</st> <st c="5634">分配</st>
<st c="5708">接收器</st>
import Combine
import Foundation
let subscriber = Timer.publish(every: 1.0, on: .main, in:
.default)
.autoconnect()
.sink( receiveValue: { value in
print("Received value: \(value)")
})
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
subscriber.cancel()
}
<st c="6011">计时器</st> <st c="6091">日期</st> <st c="6136">接收器</st>
在这个代码示例中,我们将接收到的值打印到了控制台。
<st c="6796">sink</st> <st c="6916">assign</st>
import Combine
import Foundation
class DateContainer {
var date: Date
init() { date = Date() }
}
let container = DateContainer()
let cancellable = Timer.publish(every: 1.0, on: .main, in:
.default)
.autoconnect()
.assign(to: \.date, on: container)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
cancellable.cancel()
}
<st c="7370">DateContainer</st> <st c="7391">date</st> <st c="7410">assign</st> <st c="7538">键路径</st>
<st c="7566">assign</st>
<st c="7687">sink</st>
.sink( receiveValue: { value in
container.date = value
})
<st c="7777">assign</st>
连接操作符
let numbersPublisher = Array(1...20).publisher
let subscription = numbersPublisher
.filter { $0 % 2 == 0 }
.map { "The number is \($0)" }
.sink(receiveValue: { print($0) })
<st c="8751">1</st> <st c="8757">20</st><st c="8793">publisher</st>
<st c="8812">numbersPublisher</st> <st c="8907">filter</st> <st c="9003">map</st>
<st c="9123">sink</st>

<st c="9425">filter</st> <st c="9436">map</st>
深入探讨 Combine 组件
创建自定义发布者
-
发布者 *向一个或多个订阅者 发出值 -
发布者输出类型 必须与 订阅者的输入 -
发布者还可以 传递错误
<st c="10785">Int</st>
class CustomNumberPublisher: Publisher {
typealias <st c="10923">Output</st> = Int
typealias <st c="10946">Failure</st> = Never
private let numbers: [Int]
init(numbers: [Int]) {
self.numbers = numbers
}
func receive<S: Subscriber>(subscriber: S) where
S.Input == Output, S.Failure == Failure {
for number in numbers {
_ = subscriber.receive(number)
}
subscriber.receive(completion: .finished)
}
}
<st c="11235">CustomNumberPublisher</st>
-
<st c="11289">输出</st>– 这是我们定义发布者输出类型的地方。 在这种情况下,它是一个 <st c="11373">Int</st>类型。 -
<st c="11382">失败</st>– 这是我们定义发布者错误类型的地方。 在这种情况下,发布者从不发出 错误。 -
<st c="11492">receive</st>– 这是主要的发布者方法。 Combine 在订阅者订阅发布者时调用 <st c="11556">receive</st>方法。 我们可以看到, <st c="11642">receive</st>函数具有订阅者的参数,并且它还验证订阅者输入类型和错误是否与 发布者定义匹配。
<st c="11857">receive</st> <st c="11963">receive</st> <st c="11989">completion</st>
<st c="12032">CustomNumberPublisher</st>
let subscriber = CustomNumberPublisher(numbers: [1, 2, 3,
4, 5])
.sink { value in
print(value)
}
<st c="12182">1,2,3,4,5</st>
<st c="12224">CustomNumberPublisher</st>
<st c="12466">Subject</st>
与 Subjects 一起工作
<st c="12597">send(_:)</st>
<st c="12656">PassthroughSubject</st>
理解 PassthroughSubject
import Combine
let subject = PassthroughSubject<Int, Never>()
let subscriber = subject.sink { value in
print("Received value: \(value)")
}
subject.send(1)
subject.send(2)
subject.send(3)
<st c="13000">Subject</st> <st c="13047">PassthroughSubject</st> <st c="13072">PassthroughSubject</st> <st c="13186">send(_:)</st>
<st c="13221">Subject</st>
<st c="13458">send(_:)</st>
subject.send(1)
subject.send(2)
subject.send(completion: .finished)
subject.send(3)
<st c="13651">1</st> <st c="13657">2</st><st c="13723">send</st> <st c="13746">.</st>``<st c="13747">finished</st>
<st c="13821">3</st>
<st c="13988">PassthroughSubject</st>
<st c="14429">CurrentValueSubject</st>
使用 CurrentValueSubject Subject 保持状态
与
让我们看看
import Combine
let subject = CurrentValueSubject<String, Never>("Initial
Value")
let currentValue = subject.value
print("Current value: \(currentValue)")
let subscriber = subject.sink { value in
print("Received value: \(value)")
}
subject.send("New Value")
<st c="15055">"</st>``<st c="15057">Initial Value"</st>
然后,我们将 Subject 的当前值打印到控制台,并使用一个简单的
在最后一行,我们使用我们的 Subject 发送一个新的值。
Current value: Initial Value
Received value: Initial Value
Received value: New Value
<st c="15876">PassthroughSubject</st>
let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<Int, Never>()
let subscriber = subject1
.merge(with: subject2)
.sink { value in
print("Transformed value: \(value)")
}
subject1.send(1)
subject1.send(2)
subject2.send(3)
subject2.send(4)
在这个例子中,我们有两个
Transformed value: 1
Transformed value: 2
Transformed value: 3
Transformed value: 4
<st c="16794">PassthroughSubject</st> <st c="17018">CurrentValueSubject</st>
<st c="17076">sink</st>
创建自定义订阅者
<st c="17603">CustomNumberPublisher</st>
class CustomNumberSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>)
{
print("Received completion: \(completion)")
}
}
<st c="18078">Input</st> <st c="18088">Failure</st><st c="18132">Output</st> <st c="18143">Failure</st>
<st c="18227">接收</st>
接收(subscription: Subscription)
<st c="18326">接收(subscription: Subscription)</st> <st c="18439">subscription</st>
subscription.request(.unlimited)
subscription.request(.max(3))
subscription.request(.none)
<st c="18895">receive(subscription: Subscription)</st>
<st c="19208">receive(_input:Int)</st>
receive(_ input: Int) -> Subscribers.Demand
<st c="19303">CustomNumberPublisher</st>
_ = subscriber.receive(number)
<st c="19494">receive(_ input:Int)</st> <st c="19658">sink</st>
<st c="19732">receive</st> <st c="19757">Subscribers.Demand</st>
func receive(subscription: Subscription) {
subscription.request(.max(2))
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .max(3)
}
-
订阅者订阅发布者,并调用 <st c="20495">receive(subscription: Subscription)</st>函数,返回最大值为 <st c="20574">1</st>。总需求现在是 1 `。 -
发布者向订阅者发出一个值,并调用 <st c="20659">receive(_ input:Int)</st>函数,返回最大值为 <st c="20723">3</st>。总需求现在是 4 `。
receive(completion: Subscribers.Completion)
<st c="21100">receive(completion:)</st>
<st c="21383">receive(completion:)</st>
func receive(completion: Subscribers.Completion<Never>) {
switch completion {
case .finished:
print("Subscription completed successfully.")
case .failure(let error):
print("Subscription failed with error: \(error)")
}
}
receive(completion:)</st>
连接自定义发布者和订阅者
<st c="22056">发布者</st> <st c="22080">接收</st>
func receive<S: Subscriber>(subscriber: S) where
S.Input == Output, S.Failure == Failure {
for number in numbers {
guard subscriber.receive(number) != .none else
{
subscriber.receive(completion: .finished)
return
}
}
subscriber.receive(completion: .finished)
}
<st c="22592">receive(completion:)</st>
使用操作符
运算符帮助我们修改更新、过滤它们、合并它们,并执行许多操作,这使我们能够实现一个理想的结果。
<st c="23375">Combine 框架</st> 内置了许多运算符。现在我们只介绍其中的一些,但您可以在 Apple 网站上查看完整列表:
让我们从一些基本运算符开始。
从基本运算符开始
在 Combine 中,运算符最基本的使用案例之一是用于
例如,我们可以使用 <st c="23824">filter</st> 运算符:
let cancellable = (1...10).publisher
.filter{ $0 % 2 == 0 }
.sink { value in
print(value)
}
在这个代码示例中,我们创建了一个发布者,它会从 <st c="24001">1</st> 到 <st c="24006">10</st> 发出值。<st c="24014">filter</st> 运算符确保只有偶数会继续流向下游。这段代码将在控制台打印 <st c="24108">2</st>,《st c="24109">4,《st c="24111">6</st>,《st c="24112">8和
另一个过滤运算符的例子是 <st c="24186">removeDuplicates</st>:
let cancellable = [1,2,2,3,3,3,4,5].publisher <st c="24251">.removeDuplicates()</st> .sink { value in
print(value)
}
代码示例显示了一个发布者,它会发出重复的值。<st c="24370">removeDuplicates</st> 运算符会过滤掉在最后更新中发送的值。控制台将显示以下内容:
1
2
3
4
5
让我们尝试创建一个自定义运算符来了解运算符是如何在底层工作的。
创建自定义运算符
当我们尝试检查 Apple 的头文件中的 <st c="24644">filter</st> 运算符时,我们可以看到以下内容:
extension Publisher {
…
public func filter(_ isIncluded: @escaping (Self.Output) ->
Bool) -> Publishers.Filter<Self>
}
<st c="24834">filter()</st> 是一个接受具有 <st c="24919">Output</st> 泛型类型参数的闭包并返回发布者的函数。这个函数扩展了我们在 <st c="24977">Publisher</st> 协议。
<st c="25099">过滤器</st>
<st c="25371">运算符</st>,让我们尝试做同样的事情并创建一个 <st c="25426">乘法</st> <st c="25449">乘法</st> <st c="25478">Int</st> <st c="25532">特定因子</st> 的同时重新发布它:
extension Publisher where Output == Int {
func multiply(by factor: Int) -> Publishers.Map<Self,
Int> {
return self.map { value in
return value * factor
}
}
}
<st c="25748">发布者</st> <st c="25786">输出</st> <st c="25807">Int</st>
<st c="25832">乘法</st>
<st c="25948">map</st> <st c="26032">Map</st> <st c="26072">新运算符</st>:
let cancellable = [1, 2, 3, 4, 5].publisher
.multiply(by: 2)
.sink { value in
print("Received value: \(value)")
}
<st c="26217">乘法</st>
Received value: 2
Received value: 4
Received value: 6
Received value: 8
Received value: 10
与 AnyPublisher 一起工作
<st c="26667">乘法</st> <st c="26706">Int</st> <st c="26764">Map 发布者</st>?
<st c="27159">AnyPublisher</st>
<st c="27172">AnyPublisher</st>
<st c="27298">乘法</st> <st c="27343">AnyPublisher</st> <st c="27363">Publisher.Map</st>
extension Publisher where Output == Int {
func multiply(by factor: Int) -> AnyPublisher<Int,
Failure> {
return self.map { value in
return value * factor
}
.eraseToAnyPublisher()
}
}
-
我们将函数的返回类型更改为 <st c="27654">AnyPublisher<Int, Failure></st>。这样,我们隐藏了实现细节以及我们使用了 Map 发布者 的事实。 -
我们使用 <st c="27814">eraseToAnyPublisher()</st>函数擦除了发布者类型,该函数擦除发布者类型并 返回 <st c="27890">AnyPublisher</st>。
<st c="27935">AnyPublisher</st> <st c="28095">AnyPublisher</st>
<st c="28201">AnyPublisher</st> <st c="28309">乘法</st> <st c="28376">AnyPublisher</st>
<st c="28529">AnyPublisher</st>
<st c="28668">filter</st> <st c="28679">removeDuplicates</st> <st c="28718">map</st><st c="28815">merge</st>
探索高级操作符
<st c="29127">map</st> <st c="29130">和</st>
<st c="29407">zip</st> <st c="29410">操作符。</st>
使用 zip 操作符
<st c="29448">zip</st>
<st c="29765">代码示例:</st>
import Combine
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let cancellable = publisher1
.zip(publisher2)
.sink { value in
print("Zipped value: \(value)")
}
publisher1.send(1) // no output
publisher2.send(10) // output is (1,10)
publisher1.send(2) // no output
publisher2.send(20) // output is (2,20)
<st c="30282">publisher1</st> <st c="30350">publisher2</st> <st c="30391">publisher2</st> <st c="30451">(1,10)</st> <st c="30493">zip</st>
<st c="30576">zip</st><st c="30639">zip</st>
<st c="30698">zip</st><st c="30813">merge</st>
<st c="30920">combineLatest</st>
使用 combineLatest 组合多个值
<st c="31007">zip</st>
<st c="31137">combineLatest</st>
<st c="31342">combineLatest</st>
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let cancellable = publisher1
.combineLatest(publisher2)
.sink { value in
print("Combined value: \(value)")
}
publisher1.send(1) // no output
publisher2.send(10) // output will be 1,10
publisher1.send(2) // output will be 2,10
publisher2.send(20) // output will be 2,20
在这个代码示例中,我们还有两个<st c="31827">combineLatest</st>
<st c="31848">publisher1</st> <st c="31882">combineLatest</st> <st c="31929">publisher2</st>
<st c="31962">publisher2</st> <st c="31996">combineLatest</st> <st c="32040">1,10</st>
<st c="32059">publisher1</st> <st c="32090">2</st><st c="32104">combineLatest</st> <st c="32135">publisher2</st> <st c="32191">2,10</st>
<st c="32291">combineLatest</st>
<st c="32579">combineLatest</st>
通过示例学习 Combine
在视图模型中管理基于 UIKit 的视图状态
List
class MyViewModel {
struct Item: Codable {
let title: String
let description: String
}
var dataPublisher: AnyPublisher<[Item], Error> {
return URLSession.shared.dataTaskPublisher(for:
URL(string: "https://api.example.com/data")!)
.map { $0.data }
.decode(type: [Item].self, decoder:
JSONDecoder())
.eraseToAnyPublisher()
}
}
我们已经讨论了AnyPublishermapAnyPublisher的类型。
viewModel.dataPublisher
.sink(receiveCompletion: { completion in
}, receiveValue: { [weak self] data in
self?.updateTableView(with: data)
})
.store(in: &cancellables)
<st c="34981">dataPublisher</st> <st c="35026">数据</st>
<st c="35086">URLSession</st> <st c="35101">dataTaskPublisher</st>
从多个来源执行搜索
func searchLocalDatabase(query: String) -> AnyPublisher<[SearchResult], Never> {
return Just([
SearchResult(id: 1, title: "Local Result 1"),
SearchResult(id: 2, title: "Local Result 2")
])
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func searchServer(query: String) ->
AnyPublisher<[SearchResult], Never> {
return Future { promise in
DispatchQueue.global().asyncAfter(deadline: .now()
+ 2) {
promise(.success([
SearchResult(id: 3, title: "Server Result
1"),
SearchResult(id: 4, title: "Server Result
2")
]))
}
}
.eraseToAnyPublisher()
}
var cancellables = Set<AnyCancellable>()
let query = "example"
var totalResults = [SearchResult]()
searchLocalDatabase(query: query)
.merge(with: searchServer(query: query))
.sink(receiveCompletion: { _ in }, receiveValue: {
results in
totalResults.append(contentsOf: results)
print("Search results: \(totalResults)")
})
.store(in: &cancellables)
-
<st c="36901">Just</st>和 <st c="36910">Future</st>。我们可以使用 <st c="36929">Just</st>来启动流,而 <st c="36957">Future</st>是我们用来执行任务并异步发出值的发布者。 -
<st c="37167">合并</st>操作符。 记住,当其中一个来源发出新值时, 合并 操作符会发出更新。 我们也可以使用 <st c="37291">combineLatest</st>,但 <st c="37310">combineLatest</st>在发出组合值之前会等待所有发布者发出值。 -
<st c="37510">totalResults</st>数组。 我们的数据流不必在这里结束。 我们可以将 <st c="37582">totalResults</st>转换为 <st c="37597">CurrentValueSubject</st>实例,并将结果传递给视图模型或视图本身。 如果我们使用 SwiftUI,我们可以将 <st c="37732">totalResults</st>转换为 <st c="37747">@Published</st>变量以自动刷新搜索结果的 UI。
表单验证
struct FormView: View {
@ObservedObject var viewModel = FormViewModel()
var body: some View {
VStack {
TextField("Username", text:
$viewModel.username)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Password", text:
$viewModel.password)
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Login") {
if viewModel.isFormValid {
print("Login successful!")
} else {
print("Please fill in all fields.")
}
}
.padding()
.disabled(!viewModel.isFormValid)
}
.padding()
}
}
<st c="38930">用户名</st> <st c="38943">密码</st><st c="39028">@Published</st> <st c="39058">用户名</st><st c="39068">密码</st><st c="39082">isFormValid</st><st c="39099">用户名</st> <st c="39112">密码</st>
<st c="39189">FormViewModel</st>
class FormViewModel: ObservableObject {
@Published var username: String = ""
@Published var password: String = ""
@Published var isFormValid: Bool = false
private var cancellables = Set<AnyCancellable>()
init() {
Publishers.combineLatest($username, $password)
.map { username, password in
!username.isEmpty && !password.isEmpty
}
.assign(to: \.isFormValid, on: self)
.store(in: &cancellables)
}
}
<st c="39682">combineLatest</st> <st c="39731">用户名</st> <st c="39744">密码</st> <st c="39768">map</st> <st c="39851">Bool</st><st c="39866">isFormValid</st>
<st c="39910">isFormValid</st>
<st c="40215">用户名</st> <st c="40228">密码</st>
Publishers.combineLatest($username, $password)
.map { username, password in
let isUsernameValid = !username.isEmpty &&
username.count >= 6
let isPasswordValid = !password.isEmpty &&
password.count >= 8 && password.contains(
where: { $0.isNumber })
return isUsernameValid && isPasswordValid
}
.assign(to: \.isFormValid, on: self)
.store(in: &cancellables)
<st c="40838">map</st> <st c="40941">isFormValid</st>
总结
第十三章:12
利用苹果智能和机器学习变得聪明
-
涵盖人工智能和机器学习的基础知识,学习不同术语,了解机器学习的工作原理以及训练 模型 的含义 -
探索内置的机器学习框架,例如 自然语言处理 ( NLP )、视觉和 声音分析 -
向我们的 Core Spotlight 实现 添加语义搜索 -
使用 Create 机器学习 ( ML ) 应用程序和 Core ML 框架 构建和集成自定义机器学习模型
技术要求
<st c="1619">Xcode</st>
github.com/PacktPublishing/Mastering-iOS-18-Development/tree/main/Chapter12
回顾人工智能和机器学习的基础知识
学习人工智能和机器学习之间的区别
机器学习模型是一个执行预测和分类的算法
相反,人工智能是一系列技术和方法,能够创建一个能够执行与人类通常所做任务相似的系统
一个很好的例子是
我们现在明白,机器学习模型是人工智能的一个构建块
让我们讨论这个重要的主题
具有决策树算法的模型包含一个树,其中叶子代表不同的决策或预测
但这里的
在本章接下来的内容中,我们将把构建和创建模型数据称为训练
训练模型
-
数据收集 :我们需要准备一个相对较大的数据集来训练我们的模型。 我们还必须通过处理缺失值、清理无关数据项和归一化值来预处理数据。 -
分割数据集 :现在我们有了数据集,我们必须将其分为训练数据、验证集和测试集。 我们在不同的训练阶段使用这些数据集。 -
选择我们的机器学习算法 :每种算法旨在解决不同的问题。 例如,逻辑回归算法解决分类问题,线性回归算法解决回归问题。 -
前向传播 :我们将训练数据通过模型来做出预测。 -
验证 :我们使用验证数据集来评估模型的表现,并根据结果调整模型。 -
测试 :我们使用测试数据来评估我们的模型在实时使用案例中,使用未见过的输入数据时的性能。
苹果的智能和机器学习
探索内置机器学习框架
使用 NLP 解析文本
import NaturalLanguage
NaturalLanguage
理解 NLP 是如何工作的
预处理
"Running is fun! I love to run."
"run fun love run".
<st c="10120">is</st>
特征提取
{
"run": 2,
"fun": 1,
"love": 1
}
在这个例子中,NLP 模型接收输入字符串并分析每个单词的频率。
建模
<st c="11448">自然语言</st> `
<st c="11535">自然语言</st> `
使用自然语言 API
文本分类
<st c="11802">,我们可以分析文本情感以确定它是正面</st>
The latest update made everything so much better. Great job!
<st c="12054">自然语言</st> <st c="12069">框架分析这个句子的情感,我们将使用</st>
let sentimentAnalyzer = <st c="12135">NLTagger</st>(tagSchemes:
[<st c="12158">.sentimentScore</st>])
sentimentAnalyzer.string = userInput
let (sentiment, _) = <st c="12235">sentimentAnalyzer.tag</st>(at:
userInput.startIndex, unit: .paragraph, scheme:
.sentimentScore)
if let sentiment = sentiment, let score =
Double(sentiment.rawValue) {
// here we can use the analyzed score
} else {
print("Unable to analyze sentiment")
}
<st c="12483">NLTagger</st> <st c="12641">sentimentScore</st>
<st c="13551">词性标注</st> `
<st c="13564">词性标注</st>
<st c="13702">语法类别</st>。
让我们以以下文本的例子:
She enjoys reading books in the library
如果我们尝试将这个句子分解成语法类别,它将类似于
文本的不同部分被称为
<st c="14113">The</st> <st c="14118">NaturalLanguage</st> <st c="14133">框架帮助我们执行分词和标记</st> <st c="14182">其标记。</st>
让我们看看以下代码:
let inputText = "She enjoys reading books in the library"
let tagger = NLTagger(tagSchemes: [.lexicalClass])
tagger.string = inputText
let options: NLTagger.Options = [.omitPunctuation,
.omitWhitespace]
tagger.enumerateTags(in:
inputText.startIndex..<inputText.endIndex, unit: .word,
scheme: .lexicalClass, options: options) { tag,
tokenRange in
if tag == .verb {
verb = String(inputText[tokenRange])
return false
}
return true
}
上述代码示例使用了之前相同的句子,对其进行分词,并定位到它找到的第一个动词。
我们首先初始化 <st c="14797">NLTagger</st>,类似于我们在文本分类中所做的。然而,这次我们通过传递 <st c="14895">lexicalClass</st> 作为 <st c="14911">其方案</st> 来实现这一点。
然后,我们提供输入 <st c="14949">文本</st> 并省略标点和空白。我们这样做是因为我们希望我们的文本尽可能干净。
在我们清理完文本后,我们调用 <st c="15177">enumerateTags</st> 函数。该函数遍历给定范围内的文本中的单词并提取不同的标签。我们比较传递的闭包内的标签类型并将其存储在一个 <st c="15369">实例变量</st> 中。
在我们的例子中,我们定位到第一个动词,它是 <st c="15439">enjoys</st>。
尽管词性标注和文本分类是 <st c="15497">NLTagger</st> 的两个主要用例,但它们也可以用于其他用例,例如用于识别文本的语言:
let tagger = NLTagger(tagSchemes: [<st c="15650">.language</st>])
tagger.string = inputText
if let language = tagger.dominantLanguage {
identifiedLanguage =
Locale.current.localizedString(forLanguageCode:
language.rawValue) ?? "Unknown"
} else {
identifiedLanguage = "Unknown"
}
<st c="15906">NLTagger</st>
guard let embedding = <st c="16557">NLEmbedding.</st>wordEmbedding(for:
.english) else {
neighborsText = "Failed to load word
embedding." return
} <st c="16664">let neighbors = embedding.neighbors(for:</st>
<st c="16704">embedding.vector(for: inputWord) ??</st> <st c="16741">[], maximumCount: 5)</st> if neighbors.isEmpty {
neighborsText = "No neighbors found for
'\(inputWord)'." } else {
neighborsText = neighbors.map { "\($0.0)
(\($0.1))" }.joined(separator: ", ")
}
<st c="16957">NLEmbedding</st>
<st c="17243">NaturalLanguage</st>
使用 Vision 框架分析图像
理解图像分析的工作原理
首先,CNN 扫描图像以检测相似的模式,例如线条、边缘和纹理。
探索 Vision 框架的功能
检测条形码
func analyze(url: URL) async {
let request = <st c="19632">DetectBarcodesRequest()</st> do {
let barcodeObservations = <st c="19687">try await</st>
<st c="19696">request.perform(on: url)</st> barcodeIdentifier = <st c="19742">barcodeObservations.first?.payloadString ??</st> ""
} catch let error {
print("error analyzing image –
\(error.localizedDescription)")
}
}
<st c="19973">DetectBarcodesRequest</st>
<st c="20090">perform</st>
人脸检测
func analyze(url: URL) async {
let request = <st c="20652">DetectFaceRectanglesRequest()</st> do {
let observations = try await
request<st c="20723">.perform</st>(on: url)
if let <st c="20749">observation</st> = observations.first {
rect = observation.boundingBox.cgRect
}
} catch let error {
print(error.localizedDescription)
}
}
<st c="21040">DetectFaceRectanglesRequest</st>
探索更多检测能力
-
图像美学分析 :从美学角度分析图像 -
显著性分析 :用于找到图像中最重要的对象 -
对象跟踪 :用于跟踪对象在一系列 图像中的运动 -
人体检测 :类似于人脸检测,用于在图像中定位手臂、人体、眼睛、嘴巴和鼻子 -
人体和手势姿态 :用于在图像中定位手臂以及检测 它们的姿态。 -
文本检测 :用于检测图像中的文本 -
动物检测 :用于检测图像中的猫和狗以及 它们的姿态 -
背景移除和对象提取 :用于从图像中移除背景和提取对象 的
使用声音分析框架进行音频分类
-
SNAudioFileAnalyzer :协调分析工作的主要类 -
SNClassifySoundRequest :声音 检测请求 -
SNResultsObserving :我们需要实现的一个协议,用于观察分析器的 结果
func analyze(at url: URL) {
do {
let audioFileAnalyzer = try <st c="24082">SNAudioFileAnalyzer</st>(url: url)
let request = try <st c="24131">SNClassifySoundRequest</st>(classifierIdentifier:
.version1)
let resultsObserver = <st c="24210">ClassificationResultsObserver</st>()
try audioFileAnalyzer.add(request,
withObserver: resultsObserver)
audioFileAnalyzer.analyze()
} catch {
print("Error: \(error.localizedDescription)")
}
}
<st c="24434">SNAudioFileAnalyzer</st> <st c="24581">version1</st> <st c="24610">version1</st>
<st c="24774">resultsObserver</st>
<st c="25006">ClassificationResultsObserver</st><st c="25071">SNResultsObserving</st>
class ClassificationResultsObserver: NSObject,
SNResultsObserving {
func request(_ request: SNRequest, didProduce result:
SNResult) {
guard let result = result as? SNClassificationResult else { return }
if let classification =
result.classifications.first {
<st c="25389">let result = classification.identifier</st> }
}
func request(_ request: SNRequest, didFailWithError
error: Error) { }
func requestDidComplete(_ request: SNRequest) {}
}
<st c="25557">SNResultsObserving</st> <st c="25623">didProduce</st><st c="25635">didFailWithError</st><st c="25657">requestDidComplete</st>
<st c="26037">baby_crying</st>
苹果公司尚未正式公布声音分析框架可以识别的声音类别数量。
声音分析框架非常适合监控应用,添加
使用 Core Spotlight 执行语义搜索
<st c="26793">NaturalLanguage</st>
理解语义搜索是什么
-
为员工的管理 -
数据科学 -
数字营销 -
ML 和 AI
<st c="27656">管理</st>
<st c="27819">经理</st>
<st c="27991">NaturalLanguage</st>
探索 Core Spotlight 框架
创建可搜索项
<st c="29753">Book</st> <st c="29771">CSSearchableItem</st>
let searchableItems: [<st c="29812">CSSearchableItem</st>] = books.map { book
in
let attributeSet =
CSSearchableItemAttributeSet(contentType:
.text)
attributeSet.title = book.title
attributeSet.contentDescription = book.author <st c="30000">let item = CSSearchableItem(uniqueIdentifier:</st>
<st c="30045">book.id, domainIdentifier: "books",</st>
<st c="30081">attributeSet: attributeSet)</st> return item
}
<st c="30175">Book</st> <st c="30209">CSSearchableItem</st><st c="30252">CSSearchableItemAttributeSet</st> <st c="30378">CSSearchableItem</st><st c="30408">CSSearchableItemAttributeSet</st> <st c="30501">Book</st>
索引
<st c="30564">CSSearchableItem</st><st c="30672">CSSearchableIndex</st>
let index = <st c="30704">CSSearchableIndex</st>(name: "SpotlightSearchIndex")
index.<st c="30759">indexSearchableItems</st>(searchableItems) { error
in
if let error = error {
print("Indexing error:
\(error.localizedDescription)")
} else {
print("Books successfully indexed!")
}
}
<st c="30981">CSSearchableIndex</st> <st c="31014">indexSearchableItems</st> <st c="31063">CSSearchableItem</st>
查询
let searchContext = CSUserQueryContext()
searchContext.fetchAttributes = ["title"]
searchContext.enableRankedResults = true
var items: [CSSearchableItem] = []
let query = CSUserQuery(userQueryString: query,
userQueryContext: searchContext)
do {
for try await element in query.responses {
switch(element) {
case .item(let item):
items.append(item.item)
break
case .suggestion(let suggestion):
// handle suggestions. break
@unknown default:
break
}
}
self.searchResults = items
} catch let error {
print(error.localizedDescription)
}
<st c="32001">CSUserQuery</st> <st c="32057">responses</st>
<st c="32103">CSSearchableItem</st>
<st c="32313">语义搜索</st>。
实现语义搜索
CSUserQuery.prepare()
<st c="32550">prepare</st>
如果搜索索引由于隐私问题具有保护级别,我们还需要调用
CSUserQuery.prepareProtectionClasses([.completeUnlessOpen])
此功能准备搜索标记为具有
什么是保护级别?
术语
记住,准备 ML 模型需要时间和内存,所以最好在搜索用户界面立即之前调用
我们讨论了各种
使用 CoreML 集成自定义模型
通常,ML 模型是训练来执行特定任务的——识别句子的情感、检测人类或分析声音都是使用不同模型完成的不同任务的例子。
这就是CoreML 框架
最好通过一个例子来说明如何做这件事,比如检测
了解 Create ML 应用程序
-
打开 Xcode。 -
在 Dock 上的 Xcode 图标上 右键单击。 -
选择 打开开发者工具 | Create ML 。
<st c="35374">Create ML</st>



<st c="37663">SpamClassifier</st>
-
左侧面板 :左侧面板列出了项目的不同来源 - 机器学习模型及其数据来源,用于训练和测试 -
设置选项卡 :该 设置 选项卡是我们定义不同阶段和一般 训练参数 -
训练选项卡 :该 训练 选项卡显示了训练操作的进度 -
评估选项卡 :该 评估 选项卡显示了我们的模型在不同阶段 的性能 -
预览选项卡 :我们可以在 预览 *选项卡中* 与我们的机器学习模型互动并体验它 -
输出选项卡 :该 输出 选项卡是我们部署 我们的模型
构建我们的垃圾邮件分类器模型
准备我们的数据
<st c="39116">文本</st> <st c="39125">标签</st><st c="39149">文本</st> <st c="39212">标签</st> <st c="39249">true</st> <st c="39277">false</st>

<st c="40156">真实</st> <st c="40165">虚假</st>
每次迭代中,训练过程可以通过回顾前一次迭代的错误并调整其参数(如神经网络中的权重)来自我调整。我们的直觉可能会说,迭代次数越多,我们的模型就越聪明。然而,事实并非如此。首先,在某个点上,再进行迭代并不能提高模型,只会消耗计算资源。但真正的问题是所谓的过拟合。过拟合是指机器学习模型学习训练数据过于完美,包括其噪声。在这种情况下,分析未见数据将会有问题。
另一个参数是模型算法(图 12.5):

图 12.5:选择模型算法
图 12.5 显示了弹出菜单,我们可以从五个不同的选项中选择模型学习算法。算法概述不在本章的范围内,但简而言之,不同的算法适合不同的需求,并消耗其他资源。例如,BERT算法非常适合语义理解,而条件随机字段算法非常适合序列标注。在我们的案例中,我们将选择最大熵算法,它非常适合分类。
现在我们已经准备好了所有数据集,我们可以点击左上角的训练按钮并开始训练。
执行训练
现在,我们已经到达了主要环节——训练阶段。在训练阶段,Create ML 应用程序会使用我们在准备我们的数据部分定义的算法遍历训练数据集。让我们尝试描述这个过程:
-
在每次迭代中,模型使用验证数据集来验证自身。记住,验证数据集可以是不同的。然而,默认情况下,它是训练数据集的一个子集。
-
训练 *阶段的时间长度由三个主要因素决定 ——数据集大小、迭代次数和 选择的算法。 -
模型不需要执行我们在 设置 选项卡中定义的迭代次数。 如果验证准确率达到高水平, *训练将提前停止 *以节省资源并 避免过拟合。



-
<st c="45825">true</st>或 <st c="45833">false</st>(根据具体类别而定)并且是正确的。 例如,对于 <st c="45931">false</st>类别,93%的精确率意味着模型识别为 <st c="46002">false</st>的所有消息实际上 确实是 <st c="46022">false</st>。 -
<st c="46102">true</st>类别意味着模型正确识别了所有实际 垃圾邮件消息的 93%。 -
F1 分数 :F1 分数是精确率和召回率的平衡。

部署我们的模型

<st c="48229">mlmodel</st>
<st c="48259">mlmodel</st>
使用我们的模型与 Core ML
<st c="48372">The</st> <st c="48377">Core ML framework</st> <st c="48394">’s goal is to allow us</st> <st c="48417">to integrate ML models into</st> <st c="48446">our projects.</st>
<st c="48459">Our first step is to add the</st> <st c="48489">mlmodel</st> <st c="48496">file that we saved from the Create ML application to Xcode.</st> <st c="48557">We can do that by dragging the file to the project navigator</st> <st c="48618">in Xcode.</st>
<st c="48627">The main class in the Core ML framework we will use is</st> <st c="48683">MLModel</st> <st c="48690">, which represents a ML model loaded into the system.</st> <st c="48744">To</st> <st c="48746">load our Spam Classifier model, we initialize the model in</st> <st c="48806">our code:</st>
class MessageClassifier {
let model: MLModel
init(configuration: MLModelConfiguration =
MLModelConfiguration()) throws { <st c="48937">model = try SpamClassifier(configuration:</st>
<st c="48978">configuration).model</st> }
}
<st c="49003">In the preceding code example, we created a new class, called</st> <st c="49066">MessageClassifier</st> <st c="49083">, which encapsulates our ML integration with the Spam</st> <st c="49137">Classifier model.</st>
<st c="49154">We then initiate the class, passing a new</st> <st c="49197">MLModelConfiguration</st> <st c="49217">. This contains different options, but we can pass an empty instance at</st> <st c="49289">this stage.</st>
<st c="49300">Our class also contains an</st> <st c="49328">MLModel</st> <st c="49335">instance.</st> <st c="49346">To initiate the model instance, we use the</st> <st c="49389">SpamClassifier</st> <st c="49403">class, passing</st> <st c="49419">our configuration.</st>
<st c="49437">But wait – where did the</st> <st c="49463">SpamClassifier</st> <st c="49477">class</st> <st c="49484">come from?</st>
<st c="49494">When we added the Spam Classifier</st> <st c="49529">mlmodel</st> <st c="49536">file into our Xcode project, Core ML generated three interfaces – the <st c="49607">SpamClassifier</st> <st c="49621">class,</st> <st c="49629">SpamClassifierInput</st> <st c="49648">,</st> <st c="49650">and</st> <st c="49654">SpamClassifierOutput</st> <st c="49674">.</st>
<st c="49675">Now that we have our model, let’s write a function that can predict whether a message</st> <st c="49762">is spam:</st>
func prediction(text: String) throws -> Bool {
let input = SpamClassifierInput(text: text)
if let result = try? model.prediction(from: input)
{ <st c="49915">let value = result.featureValue(for:</st>
<st c="49951">"label")!.stringValue</st> return value == "true"
}
return false
}
<st c="50013">In the preceding example, we created a</st> <st c="50053">prediction</st> <st c="50063">function that receives a text message as input and</st> <st c="50114">returns</st> <st c="50123">a Boolean.</st>
<st c="50133">It starts by creating a</st> <st c="50158">SpamClassifierInput</st> <st c="50177">instance with the text input.</st> <st c="50208">Then, it generates a prediction result for this input by running the model’s</st> <st c="50285">prediction()</st> <st c="50297">function.</st> <st c="50308">We then get the value from the feature, called</st> <st c="50355">label</st> <st c="50360">, and compare it</st> <st c="50377">to</st> <st c="50380">true</st> <st c="50384">.</st>
<st c="50385">This code example demonstrates how to easily use a custom ML model in our</st> <st c="50460">Xcode projects.</st>
接下来该往哪里去
总结
第十四章:13
使用应用程序意图将您的应用程序暴露给 Siri
-
理解应用程序 意图概念 -
创建一个简单的 应用程序意图 -
使用 应用程序实体 正式化我们的内容 -
调整我们的应用程序意图以与 Apple Intelligence
技术要求
理解应用程序意图概念
创建一个简单的应用意图
<st c="2980">MightyTasksList</st>
<st c="3001">MightyTasksList</st>
<st c="3209">import AppIntents</st> struct GetTasksIntent: AppIntent {
static var title: LocalizedStringResource { "Get the number of opened tasks" }
@MainActor
func perform() async throws -> some ProvidesDialog {
let tasks = TaskManager().tasks <st c="3438">return .result( dialog: "Number of the opened tasks is \(tasks.count)")</st> }
}
-
我们导入了 <st c="3630">AppIntents</st>框架。 在这种情况下,我们需要这个框架来拥有 <st c="3701">AppIntent</st>协议。 -
我们创建了一个符合 <st c="3781">AppIntent</st>协议的 <st c="3734">GetTasksIntent</st>结构。 这个结构定义了我们的 意图功能。 -
作为
<st c="3865">AppIntent</st>协议的一部分,我们必须实现两件事。 第一件事是意图的 <st c="3945">标题</st>,它出现在快捷方式应用中的意图画廊中(我们将在 * 在快捷方式应用中运行意图 部分中很快就会看到)。 第二件事是我们需要实现的是 <st c="4136">perform()</st>函数,这是意图运行时实际执行的代码。
<st c="4224">The</st> <st c="4229">perform()</st> <st c="4266">IntentResult</st> <st c="4370">IntentResult</st> <st c="4325">ProvidesDialog</st>
下一个,让我们使用快捷方式应用来运行我们的意图。
运行意图的快捷方式应用
苹果公司在 2017 年收购了快捷方式应用,该应用最初由一家名为 Workflows 的初创公司开发。
当我们构建和运行我们的应用时,iOS 会扫描符合 <st c="5035">AppIntent</st>

在 *

<st c="5730">perform()</st>
创建应用快捷方式
<st c="6096">AppShortcutsProvider</st>
import AppIntents <st c="6181">struct AppShortcuts: AppShortcutsProvider {</st> @AppShortcutsBuilder
static var appShortcuts: [AppShortcut] { <st c="6287">AppShortcut</st>(intent: GetTasksIntent(),
phrases: ["What is left in \(.applicationName)?", "How many tasks left in \(.applicationName)"], shortTitle: "My tasks", systemImageName: "circle.badge.checkmark")
}
}
<st c="6532">AppShortcuts</st><st c="6588">appShortcuts</st>
<st c="6677">AppShortcut</st>
-
将要执行的意图。 在这里,我们将前面 部分( <st c="6822">GetTasksIntent</st>)中创建的意图放入。 -
用户必须对 Siri 说的确切短语。 在我们的例子中,我们添加了两个短语。 请注意,短语必须包含 应用程序名称。 -
快捷方式的标题和系统图像。
向我们的应用意图添加一个参数
<st c="7361">GetTasksIntent</st> <st c="7497">AppIntents</st>
struct AddTaskIntent: AppIntent {
static var title: LocalizedStringResource { "Create new task" } <st c="7727">@Parameter(title: "Title")</st><st c="7753">var title: String</st> @MainActor
func perform() async throws -> some <st c="7819">ReturnsValue<String></st> {
TaskManager().addTask(Task(title: title))
TaskManager().saveTasks() <st c="7910">return .result(value: title)</st> }
}
<st c="7942">AddTaskIntent</st>
<st c="8033">AddTaskIntent</st> <st c="8068">AppIntent</st> <st c="8099">GetTasksIntent</st><st c="8224">@</st>``<st c="8225">Parameter</st>
<st c="8472">AddTaskIntent</st><st c="8521">任务</st>
<st c="8549">perform()</st> <st c="8595">title</st> <st c="8700">GetTasksIntent</st>
<st c="8763">GetTasksIntent</st><st c="8786">the</st> <st c="8791">ProvidesDialog</st> <st c="8828">ReturnsValue<String></st>

-
第一个是 <st c="9466">AddTaskIntent</st>来自 `我们的应用。 -
第二个是,我们的意图的结果是任务标题和来自 发送消息 动作的消息应用。
<st c="9650">强大的流。</st>

返回自定义视图
struct MiniTasksList: View {
let tasks: [Task]
var body: some View {
VStack {
ForEach(tasks) { task in
TaskView(task: task)
}
}
}
}
<st c="10816">MiniTasksList</st> <st c="10867">数组</st> <st c="10878">TaskView</st>。
-
首先,为什么我们需要创建一个专门的列表视图? 我们不能在我们应用程序中重用已经存在的视图吗? -
其次,为什么我们使用 VStack 而不是列表视图?
struct <st c="11567">GetTasksListIntent</st>: AppIntent {
static var title: LocalizedStringResource { "Get my Tasks's List" }
@MainActor
func perform() async throws -> some <st c="11715">ShowsSnippetView</st> {
let tasks = TaskManager().tasks <st c="11766">return .result(view: MiniTasksList(tasks: tasks))</st> }
}
<st c="11824">GetTasksListIntent</st> <st c="11939">标题</st> <st c="11960">perform()</st>
-
perform()函数的返回类型现在是<st c="12041">ShowsSnippetView</st>。如果我们想将自定义视图作为函数的结果展示,我们使用 <st c="12092">ShowsSnippetView</st>。 `我们的函数。 -
我们返回了一个不同的意图结果,使用了我们创建的 <st c="12221">MiniTasksList</st>视图。

<st c="12567">MiniTasksList</st> <st c="12605">通过查看列表的显示方式,我们可以理解为什么苹果限制了我们可以如何自定义此视图。</st>
<st c="12821">是很好的。</st> <st c="12925">这是否可能?</st>
具有多个结果类型
<st c="13030">如果任务数量超过,比如说,五个,他们希望打开日历来重新安排</st>
struct GetTasksListIntent: AppIntent {
static var title: LocalizedStringResource { "Get my Tasks's List" }
@MainActor
func perform() async throws ->
some <st c="13490">ShowsSnippetView & ReturnsValue<Int></st> {
let tasks = TaskManager().tasks
return <st c="13568">.result(value: tasks.count,</st>
<st c="13595">view: MiniTasksList(tasks: tasks))</st> }
}
<st c="13656">perform()</st> <st c="13697">ShowsSnippetView</st> <st c="13748">ReturnsValue<Int></st>
<st c="13835">IntentResult</st>
return .result(value: tasks.count,
view: MiniTasksList(tasks: tasks))
<st c="14014">ReturnsValue</st>
添加确认和条件
<st c="14424">AppIntents</st>
struct DeleteAllTasksIntent: AppIntent {
static var title: LocalizedStringResource { "Delete all tasks" } <st c="14789">func perform() async throws -> some ProvidesDialog {</st> let taskManager = TaskManager()
if taskManager.tasks.count == 0 {
return .result(dialog: .init("Sorry, there are no tasks to delete"))
} <st c="14979">try await requestConfirmation(actionName: .go,</st>
<st c="15025">dialog: IntentDialog("Are you sure you want to delete all your tasks?"))</st> TaskManager().deleteAllTasks()
return .result(dialog: .init("All of your tasks have been deleted."))
}
}
<st c="15302">perform()</st>
<st c="15575">requestConfirmation()</st>

<st c="15865">requestConfirmation()</st>
<st c="16325">AppEntity</st>
使用应用实体正式化我们的内容
<st c="16705">Task</st>
<st c="16788">Task</st>
<st c="17205">Task</st>
遵循 AppEntity
<st c="17256">AppEntity</st>
<st c="17477">Task</st> <st c="17515">AppEntity</st>
struct Task: Identifiable, Codable, <st c="17563">AppEntity</st> {
static var <st c="17586">typeDisplayRepresentation</st>: TypeDisplayRepresentation { .init(stringLiteral: "Task") }
init(id: UUID = UUID(), title: String,
description: String = "") {
self.id = id
self.title = title
self.description = description
}
var <st c="17809">displayRepresentation</st>: DisplayRepresentation { DisplayRepresentation(stringLiteral: "title: \(title)") }
let id: UUID
@<st c="17929">Property</st>(title: "Title")
var title: String
@<st c="17975">Property</st>(title:"Description")
var description: String
static var defaultQuery = <st c="18057">TaskQuery</st>()
}
<st c="18093">AppEntity</st>
-
<st c="18127">typeDisplayRepresentation</st>: 我们的实体需要在系统中有一个名称,这样我们就可以在快捷方式应用中显示它。 在这种情况下,我们 返回 <st c="18269">Task</st>。 -
<st c="18274">displayRepresentation</st>: 虽然 <st c="18305">typeDisplayRepresentation</st>显示了实体类型名称,但 <st c="18363">displayRepresentation</st>属性返回实体值表示。 在这种情况下,这是标题值(例如, Call my mom )。 -
<st c="18576">@Property</st>属性用于实体的一些 属性,我们定义了实体结构以用于 快捷方式应用。 -
<st c="18692">defaultQuery</st>:仅声明我们的应用程序实体是不够的;我们还需要向系统提供一个检索它们的方法。 我们的下一步将是创建系统将用于检索我们的实体的查询。
<st c="18919">Task</st> <st c="18999">TaskQuery</st>
struct TaskQuery: <st c="19029">EntityQuery</st> {
func entities(for identifiers: [UUID]) async throws -> [Task] {
return TaskManager().tasks.filter {identifiers.contains($0.id)}
}
func suggestedEntities() async throws -> [Task] {
return TaskManager().tasks
}
}
<st c="19296">TaskQuery</st> <st c="19332">EntityQuery</st>
<st c="19390">entities()</st><st c="19502">TaskManager</st>
<st c="19626">suggestedEntities()</st>
<st c="19800">AppEntity</st>
<st c="19950">打开一个</st> <st c="19957">任务</st>
创建一个打开任务意图
<st c="20013">打开一个任务</st> <st c="20152">AppEntity</st>
struct OpenTaskIntent: AppIntent {
static var title: LocalizedStringResource { "Open a task" } <st c="20284">@Parameter(title: "Task")</st>
<st c="20309">var task: Task?</st>
<st c="20325">static let openAppWhenRun: Bool = true</st> @MainActor
func perform() async throws -> some ProvidesDialog{
let taskToOpen: Task
if let task {
taskToOpen = task
} else {
taskToOpen = <st c="20503">try await $task.requestDisambiguation(</st>
<st c="20541">among: TaskManager().tasks,</st>
<st c="20569">dialog: "What task would like to open?")</st>
<st c="20610">}</st> Navigator.shared.path.append(taskToOpen)
return .result(dialog: "Opening your task")
}
}
<st c="20706">打开任务</st>
-
我们向 <st c="20853">Task</st>中添加了 <st c="20839">@Parameter</st>。使用 <st c="20865">@Parameter</st>对我们来说并不新鲜——我们在 在我们的应用意图中添加参数 部分讨论过。 然而,这次,我们通过 <st c="21002">Task</st>结构本身 来实现这一点。我们可以这样做,因为 <st c="21048">Task</st>现在符合 <st c="21069">AppEntity</st>。 -
我们将 <st c="21087">openAppWhenRun</st>属性设置为 <st c="21114">true</st>,这样我们就可以打开应用并显示 <st c="21158">任务详情</st>。 -
如果应用意图没有接收到任务参数,我们可以使用 <st c="21271">requestDisambiguation</st>函数让用户选择一个任务。 此函数向用户显示一个包含给定任务列表的对话框,并要求他们选择 <st c="21398">一个任务</st>。

<st c="22055">打开任务</st>
<st c="22182">openAppWhenRun</st> <st c="22209">true</st>
<st c="22318">标题:<任务标题></st><st c="22374">displayRepresentation</st> <st c="22426">AppEntity</st>
<st c="22735">打开任务</st>
<st c="22735">打开任务</st>
链式连接应用意图
<st c="22839">AddTaskIntent</st><st c="22940">perform()</st>
func perform() async throws -> some <st c="22996">ReturnsValue<String></st> {
TaskManager().addTask(Task(title: title))
TaskManager().saveTasks() <st c="23087">return .result(value: title)</st> }
<st c="23140">perform()</st> <st c="23162">ReturnsValue<String></st><st c="23223">Task</st>
func perform() async throws -> some <st c="23274">ReturnsValue<Task></st> {
let newTask = Task(title: title)
TaskManager().addTask(newTask)
TaskManager().saveTasks()
return .result(value: <st c="23407">newTask</st>)
}
<st c="23430">perform()</st> <st c="23504">ReturnsValue<Task></st>
<st c="23638">AddTaskIntent</st> <st c="23656">OpenTaskIntent</st>

<st c="24041">Task</st>
将我们的意图与其他意图集成
<st c="24214">打开任务</st> <st c="24317">Task</st> <st c="24375">Task</st>
选择其中一个属性
<st c="24542">AppEntity</st>
struct Task: Identifiable, Codable, <st c="24659">AppEntity</st> { <st c="24671">static</st> <st c="24677">var</st> typeDisplayRepresentation: TypeDisplayRepresentation { .init(stringLiteral: "Task") }
@<st c="24769">Property</st>(title: "Title")
var title: String
@<st c="24815">Property</st>(title:"Description")
var description: String
<st c="24875">Task</st> <st c="24915">Task</st><st c="24944">Title</st> <st c="24954">Description</st>
<st c="25163">标题</st> <st c="25184">AppEntity</st>

<st c="25538">AppEntity</st>
传递 <st c="25609">标题</st> <st c="25626">标题</st> <st c="25722">任务</st>
使用 Transferable 协议传递整个实体
<st c="26081">》的框架。</st> <st c="26096">共享数据的思想并不局限于</st>
在执行共享时,主要挑战是找到每个应用都同意的数据类型。
<st c="26513">Transferable</st>
<st c="26603">Transferable 的主要用途是复制粘贴和拖放,但它也非常适合在 <st c="26735">快捷方式应用</st> 中共享应用实体。
<st c="26768">任务</st> <st c="26784">Transferable</st>
extension Task: <st c="26818">Transferable</st> { <st c="26833">static var transferRepresentation: some TransferRepresentation {</st>
<st c="26897">…</st>
<st c="26898">}</st> }
<st c="26918">协议有一个名为</st>
<st c="27118">AppIntent</st> <st c="27175">transferRepresentation</st>
-
<st c="27207">数据表示</st>:我们使用 <st c="27236">数据表示</st>将我们的对象转换为 RTF 或 PNG 图像等数据格式 -
<st c="27321">文件表示</st>:我们使用 <st c="27350">文件表示</st>将我们的实体导出为文件,例如 PDF -
<st c="27414">代理表示</st>:这为其他表示都不合适时提供了一个替代方案
extension Task: Transferable {
static var transferRepresentation: some TransferRepresentation { <st c="27671">DataRepresentation</st>(exportedContentType: .rtf)
{ task in
task.asRTF()! } <st c="27744">ProxyRepresentation</st>(exporting: \.title)
}
}
<st c="27823">数据表示</st> <st c="27846">代理表示</st>
<st c="27943">任务</st> <st c="27960">可传输</st> <st c="28028">代理表示</st>

调整我们的应用意图以与苹果智能技术一起工作
探索助手模式

-
用户以 自由而自然的短语 请求 Siri 执行特定操作。 在这种情况下,用户说,“将这封邮件发送给我的妻子。” -
Siri 和苹果智能使用复杂的机器学习模型将请求转换为系统预定义的其中一个模式。 在这种情况下,苹果智能将用户请求转换为来自 <st c="30775">createDraft</st>模式 的<st c="30803">mail</st>域。 -
现在 Siri 知道所选模式后,它会寻找工具箱中匹配的意图。 我们可以将一些应用程序意图与特定的模式关联起来。 -
Siri 启动并执行上一步(工具箱步骤)中做出的相应应用程序意图。
struct <st c="31625">SendDraftIntent</st>: AppIntent {
static var title: LocalizedStringResource { "Send new email" }
@Parameter(title: "Body")
var body: String? @MainActor
func perform() async throws -> some ReturnsValue
<MailDraftEntity>{
let mailDraftEntity = MailDraftEntity(body: EntityProperty(title: LocalizedStringResource(stringLiteral: body!)))
ComposeDraftManager.shared.isPresentingCompose = true
return .result(value: mailDraftEntity)
}
}
<st c="32087">SendDraftIntent</st> <st c="32127">body</st>
<st c="32344">SendDraftIntent</st>
<st c="32524">AssistantIntent(schema:)</st>
<st c="32550">@AssistantIntent(schema: .mail.createDraft)</st> struct SendDraftIntent: AppIntent {
<st c="32641">AssistantIntent</st> <st c="32678">.mail.createDraft</st>
.mail.createDraft <st c="33099">mail</st><st c="33123">createDraft</st><st c="33170">mail</st> <st c="33191">deleteDraft</st><st c="33204">saveDraft</st><st c="33218">replyMail</st>
<st c="33524">AssistantIntet</st>
<st c="33614">__assistantSchemaIntent</st>
<st c="33754">createDraft</st> schema. It also ensures that our intent conforms to the <st c="33822">AssistantSchemaIntent</st> protocol, which gives it more capabilities.
<st c="33887">Once that happens, it’s time to adjust</st> <st c="33926">our code according to what the</st> <st c="33958">compiler requires:</st>
* <st c="33976">We can remove the</st> `<st c="33995">title</st>` <st c="34000">static variable, as the App Intents frameworks implement it</st> <st c="34061">for us.</st>
* <st c="34068">The same goes for the</st> `<st c="34091">@Parameter</st>` <st c="34101">argument.</st> <st c="34112">The App Intents framework implements that for us, so we can also</st> <st c="34177">remove that.</st>
* <st c="34189">We must add more properties to our app intent that are part of the</st> `<st c="34257">createDraft</st>` <st c="34268">assistant schema –</st> `<st c="34288">account</st>`<st c="34295">,</st> `<st c="34297">attachments</st>`<st c="34308">,</st> `<st c="34310">to</st>`<st c="34312">,</st> `<st c="34314">cc</st>`<st c="34316">,</st> `<st c="34318">bcc</st>`<st c="34321">,</st> <st c="34323">and</st> `<st c="34327">subject</st>`<st c="34334">.</st>
<st c="34335">Our new modified</st> `<st c="34353">SendDraftIntent</st>` <st c="34368">now looks</st> <st c="34379">like this:</st>
func perform() async throws -> some ReturnsValue
<MailDraftEntity>{
let mailDraftEntity = MailDraftEntity(body:
EntityProperty(title: LocalizedStringResource
(stringLiteral: body!)))
ComposeDraftManager.shared.isPresentingCompose =
true
return .result(value: mailDraftEntity)
}
<st c="34929">In this code example, we can see the new modified version of the</st> `<st c="34995">SendDraftIntent</st>` <st c="35010">struct.</st> <st c="35019">The new properties, such as</st> `<st c="35047">attachments</st>`<st c="35058">,</st> `<st c="35060">to</st>`<st c="35062">,</st> `<st c="35064">cc</st>`<st c="35066">, and</st> `<st c="35072">bcc</st>`<st c="35075">, have particular types, such as</st> `<st c="35108">IntentFile</st>` <st c="35118">and</st> `<st c="35123">IntentPerson</st>`<st c="35135">. The</st> `<st c="35141">AppIntent</st>` <st c="35150">framework uses this type to identify people and files and have a clear interface that the system can work with.</st> <st c="35263">Besides adding them to the</st> `<st c="35290">SendDraftIntent</st>` <st c="35305">struct, we don’t need to do anything with them except use them in our</st> `<st c="35376">perform()</st>` <st c="35385">function.</st>
<st c="35395">When we look at the code, one question arises: How do we know what properties to add for each domain</st> <st c="35497">and schema?</st>
<st c="35508">At this time of writing, there is clear documentation of what properties each schema requires.</st> <st c="35604">However, adding the</st> `<st c="35624">AssistantIntent</st>` <st c="35639">Swift macro and building the project creates new errors that provide information about the</st> <st c="35731">missing information.</st>
<st c="35751">One exception, though, is the</st> `<st c="35782">account</st>` <st c="35789">property, which requires</st> <st c="35814">us to declare an</st> `<st c="35832">AssistantEntity</st>`<st c="35847">-based struct.</st> <st c="35863">Let’s</st> <st c="35869">discuss it.</st>
<st c="35880">Creating AssistantEntity</st>
<st c="35905">When we discussed</st> `<st c="35924">SendDraftIntent</st>`<st c="35939">, we reviewed</st> <st c="35952">several properties, such as</st> `<st c="35981">attachments</st>`<st c="35992">,</st> `<st c="35994">to</st>`<st c="35996">, and</st> `<st c="36002">bcc</st>`<st c="36005">. We saw that for each one, the</st> `<st c="36037">AppIntents</st>` <st c="36047">framework provides a dedicated type, such as</st> `<st c="36093">IntentPerson</st>` <st c="36105">and</st> `<st c="36110">IntentFile</st>`<st c="36120">.</st>
<st c="36121">The case of the</st> `<st c="36138">account</st>` <st c="36145">property is a little</st> <st c="36167">bit different:</st>
<st c="36264">AppIntent</st> 的框架的一部分——它是一个我们必须满足 Assistant Schema 要求的自定义类型,类似于我们在 <st c="36376">SendDraftIntent</st> 中所做的那样。让我们看看如何实现它:
@AssistantEntity(schema: .mail.account)
struct MailAccountEntity {
let id = UUID()
var emailAddress: String
var name: String
static var defaultQuery = AccountQuery()
struct AccountQuery:EntityStringQuery {
func entities(matching string: String)
async throws -> [MailAccountEntity] {
[]
}
init() {}
func entities(for identifiers: [MailAccountEntity.ID])
async throws -> [MailAccountEntity] {
[]
}
}
var displayRepresentation: DisplayRepresentation
{ DisplayRepresentation(stringLiteral: name) }
}
在这个例子中,我们可以看到我们的 `<st c="36919">In this example, we can see that our</st>` `<st c="36957">MailAccountEntity</st>` <st c="36974">struct 有一个名为</st> `<st c="37006">@AssistantEntity(schema: .mail.account)</st>`<st c="37045"> 的 Swift 宏。这个宏使得我们的实体符合 `<st c="37086">AssistantSchemaEntity</st>` <st c="37107">,并要求结构体实现重要的属性,例如</st> `<st c="37175">emailAddress</st>` <st c="37187">和</st> `<st c="37192">name</st>`<st c="37196">。</st>
<st c="37197">Swift 宏还要求我们添加一个默认查询,以便在需要时帮助系统检索和定位账户</st> <st c="37299">when needed.</st>
我们需要实现的第二个实体是 `<st c="37311">The second entity</st>` <st c="37329">we need to implement</st> <st c="37351">is</st> `<st c="37354">MailDraftEntity</st>`<st c="37369">:</st>
@AssistantEntity(schema: .mail.draft)
struct MailDraftEntity {
static var defaultQuery = Query()
struct Query: EntityStringQuery {
init() {}
func entities(for identifiers: [MailDraftEntity.ID])
async throws -> [MailDraftEntity] { [] }
func entities(matching string: String)
async throws -> [MailDraftEntity] { [] }
}
var displayRepresentation: DisplayRepresentation
{ DisplayRepresentation(stringLiteral: "\(subject ?? "")") }
let id = UUID()
var to: [IntentPerson]
var cc: [IntentPerson]
var bcc: [IntentPerson]
var subject: String? var body: String? var attachments: [IntentFile]
var account: MailAccountEntity
}
`<st c="37986">MailDraftEntity</st>` <st c="38002">包含类似于</st> `<st c="38040">SendDraftIntent</st>`<st c="38055"> 的属性。这是因为它是 `<st c="38095">SendDraftIntent</st>` `<st c="38110">perform()</st>` <st c="38120">函数的结果,Siri 可以用它将信息链式连接到其工具箱中的其他操作。</st>
<st c="38208">添加 `<st c="38221">MailDraftEntity</st>` <st c="38236">和</st> `<st c="38241">MailAccountEntity</st>` <st c="38258">可能会很烦人——它要求我们调整我们的信息以适应特定的接口。</st> <st c="38343">然而,这样做使得我们的 Siri 集成无懈可击且有效。</st>
<st c="38413">一旦我们设置好一切</st> <st c="38437">set, the user can see a photo and say something like, “Email this photo using MyMailComposer app,” and Siri will launch our app with a</st> <st c="38573">new draft.</st>
<st c="38583">关于本节代码片段的重要免责声明</st>
<st c="38647">苹果智能</st> <st c="38666">在撰写本书时尚未推出。</st> <st c="38732">这意味着代码已成功编译,但尚未测试与苹果智能兼容。</st> <st c="38839">当苹果智能到达您的地区时,您可能需要调整代码,以便它能够按预期与 Siri 协同工作。</st> <st c="38969">。</st>
如苹果公司一位高级经理曾说过,我们应该将我们所有的应用操作视为应用意图。<st c="39088">这种做法为我们</st><st c="39132">提供了与我们的</st> <st c="39158">应用互动的无限可能性。</st>
<st c="39172">总结</st>
<st c="39180">这是一个令人兴奋的章节!</st> <st c="39211">这不仅是因为应用意图是一个非常令人兴奋的话题,而且还因为这是我们第一次真正将我们的代码与苹果最重要的</st> <st c="39369">技术之一集成。</st>
<st c="39394">在本章中,我们讨论了应用意图的概念,创建了一个具有不同用例的简单应用意图,使用应用实体正式化我们的内容,甚至调整它们以与苹果智能协同工作。</st> <st c="39606">现在,我们应该准备好在</st> <st c="39661">短时间内将 Siri 引入我们的应用!</st>
<st c="39669">下一章将从不同的角度审视我们的应用——</st> <st c="39735">质量。</st>
第十五章:14
使用 Swift Testing 提升应用质量
-
理解测试的重要性 -
学习 Xcode 的测试 历史 -
探索 Swift Testing 框架的基本知识 -
了解如何使用套件、测试计划和 方案 来管理测试 -
学习可以帮助我们维护 测试 的技巧
技术要求
<st c="1030">Xcode</st>
理解测试的重要性
func canUserAddTask(to list: List, user: User) -> Bool {
if list.isLocked {
return false
}
if !list.allowedRoles.contains(user.role) {
return false
}
return [.privateList,
.publicList].contains(list.sharingAttribute)
}
学习 Apple 平台上的测试历史
class CanUserAddTaskTests: XCTestCase {
func testCanAddTaskWhenListIsLocked() {
let list = List(id: "1", isLocked: true,
sharingAttribute: .privateList, allowedRoles:
[.admin, .member])
let user = User(role: .admin)
XCTAssertFalse(canUserAddTask(to: list, user:
user), "User should not be able to add a task
when the list is locked")
}
}
-
测试函数是 <st c="4527">CanUserAddTaskTests</st>类的一部分,继承自 <st c="4573">XCTestCase</st>超类。 -
测试函数名称以 <st c="4635">test</st>短语开头。 该 <st c="4652">test</st>短语表示 XCTest 框架,这是一个 测试函数。 -
测试验证表达式是通过一个特定的函数( <st c="4788">XCTAssertFalse</st>)完成的,该函数检查特定的表达式是否 <st c="4853">为假</st>。我们有一系列用于 各种条件的函数。
探索 Swift 测试基础


添加基本测试
import Testing
struct Chapter14Tests {
@Test func testExample() async throws {
// Write your test here and use APIs like
`#expect(...)` to check expected conditions. }
}
-
<st c="7545">Testing</st>,并且我们应该将其导入到我们想要测试的每个文件中。 要测试的每个文件中。 -
<st c="7698">XCTestCase</st>,我们在 Swift Testing 中使用结构体。 结构体 不仅更轻量级且易于使用,而且在尝试并行运行测试时也更有帮助。 记住,结构体是值类型,这意味着每次我们传递一个结构体时,我们都会得到数据的一个副本。 这有助于在测试时检查状态。 -
<st c="8031">@Test</st>``<st c="8081">@Test</st>宏,它帮助 SwiftData 框架管理 其测试。 -
<st c="8159">#expect</st>``<st c="8197">XCTAssert</st>函数,我们使用 <st c="8229">#expect</st>宏,这对于我们想要测试的任何表达式都很有帮助。 要测试的表达式。
<st c="8570">increment</st> <st c="8584">decrement</st> <st c="8610">count</st>
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func increment(by value: Int) { }
func decrement(by value: Int) {}
func reset() {}
}
让我们使用 Swift Testing 测试CounterViewModel的功能。
我们需要做的第一件事是向 Swift Testing 提供对我们的应用目标的访问权限:
@testable import Chapter14
我们将@testable属性添加到import命令中,以启用对内部实体的访问。
现在,让我们编写我们的第一个测试函数:
@Test func testViewModelIncrement() async throws {
// preparation
let viewmodel = CounterViewModel()
viewmodel.count = 5
// execution
viewmodel.increment(by: 1)
// verification
#expect(viewmodel.count == 6)
}
在我们的测试函数中,我们初始化视图模型,调用其增加函数,并验证结果。如果#expect宏函数内的表达式为false,则测试失败。
这三个阶段——准备、执行和验证——是任何测试流程的一部分,无论我们使用 Swift Testing 还是任何其他测试框架。
现在,让我们将包含此测试的结构体(CounterViewModelTests)重命名,并运行我们的测试。
在 Xcode 中,我们可以通过标签页打开左侧面板(或者直接按⌘6),然后我们可以看到我们的测试列表(图 14.3):

图 14.3:Xcode 中列出的测试
在图 14.3中,我们可以看到测试面板上我们的测试结构,这反映在我们创建结构体和测试函数的方式上。
在本章开头,我们通过检查一个简单的代码示例来讨论了 Swift Testing 和 Xcode 之间的区别。其中之一的变化是使用@Test宏。
除了指示一个测试函数,@Test 宏还有额外的功能帮助我们配置测试。
例如,让我们使用@Test宏为我们的测试函数提供一个名称。
为我们的测试函数提供名称
为测试函数提供有表达力和意义的名称至关重要,并且当我们在项目中拥有数百个测试时,这可能会很有价值。
在 XCTest 中,为了做到这一点,我们需要将测试函数重命名为类似以下的内容:
func testViewModelIncremenetFunction_incrementBy1_accept5_expect6
<st c="11542">@Test</st>
@Test("Test the increment function. Accepts 5 and expect 6\. ") func testViewModelIncrement()
<st c="11735">@Test</st>

<st c="12136">@Test</st>
启用和禁用测试
<st c="12547">@Test</st>
<st c="12681">disabled()</st>
@Test("Test the incremenet function. Accepts 5 and expect 6\. ", .disabled()) func testViewModelIncrement()
<st c="12829">disabled()</st> <st c="12867">@Test</st>

<st c="13879">@Test</st>
@Test("Test the decrement function.", .<st c="14110">testTheDecrementFunction</st>. We added a condition to the test function that would run only if we enabled the ability to decrement in the app settings. In this case, the <st c="14276">AppSettings.CanDecrement</st> expression returns <st c="14320">false</st>. Therefore, Swift Testing skips the test function at runtime.
<st c="14388">When using the enabled function, precisely defining the test goal is essential.</st> <st c="14469">For example, when using</st> `<st c="14493">AppSettings</st>`<st c="14504">, we may want to test the results of the decrement function when the feature is turned off.</st> <st c="14596">We need to disable tests according to a Boolean expression only when it’s clear that the function is irrelevant under</st> <st c="14714">specific conditions.</st>
<st c="14734">If we try to run a test when the</st> `<st c="14768">enabled()</st>` <st c="14777">function returns</st> `<st c="14795">false</st>`<st c="14800">, we’ll see something like</st> *<st c="14827">Figure 14</st>**<st c="14836">.6</st>*<st c="14838">:</st>

<st c="15020">Figure 14.6: A skipped test function due to a specific false condition</st>
<st c="15090">In</st> *<st c="15094">Figure 14</st>**<st c="15103">.6</st>*<st c="15105">, we can see that the test function is not grayed out, as in the case of using the</st> `<st c="15188">disabled()</st>` <st c="15198">function.</st> <st c="15209">However, it wasn’t running, and we can also see the skipped icon on</st> <st c="15277">the right.</st>
<st c="15287">We have seen how to provide readable names</st> <st c="15330">to test functions and how to disable or enable tests.</st> <st c="15385">Now, let’s discuss another excellent Swift Testing feature –</st> *<st c="15446">tags</st>*<st c="15450">.</st>
<st c="15451">Tagging our test functions</st>
<st c="15478">Generally, we group tests</st> <st c="15504">according to our project</st> <st c="15529">structure.</st> <st c="15541">For example, we could create a structure of test functions for a specific class or a structure.</st> <st c="15637">Another example would be to create a test structure for a particular feature or service.</st> <st c="15726">However, there are additional ways we can organize our test functions.</st> <st c="15797">We could arrange them according to priority – critical or sanity tests – or according to their system levels, such as UI or business</st> <st c="15930">logic layers.</st>
<st c="15943">Instead of finding workarounds for that organization problem, Swift Testing</st> <st c="16019">provides an organization feature</st> <st c="16053">called</st> **<st c="16060">tags</st>**<st c="16064">.</st>
<st c="16065">We’ll start by defining a new tag in the</st> <st c="16107">test bundle:</st>
extension Tag {
@Tag static let critical: Self
}
<st c="16168">We extended the</st> `<st c="16185">Tag</st>` <st c="16188">structure in this code and added a new static variable,</st> <st c="16245">named</st> `<st c="16251">critical</st>`<st c="16259">.</st>
<st c="16260">We can define and use as many</st> <st c="16290">tags as we want across our bundle.</st> <st c="16326">Therefore, it is a best practice</st> <st c="16358">to manage all our tags in one place and a</st> <st c="16401">separate file.</st>
<st c="16415">Now that we have a new tag, let’s add it to one of</st> <st c="16467">our tests:</st>
@Test("测试重置函数",
<st c="16555">In this code example, another</st> `<st c="16586">@Test</st>` <st c="16591">macro function, called</st> `<st c="16615">tags()</st>`<st c="16621">, provides the new</st> `<st c="16640">critical</st>` <st c="16648">static variable we created in the previous</st> <st c="16692">code example.</st>
<st c="16705">Note that we can provide multiple tags to the same</st> <st c="16757">test function:</st>
.tags(.critical, .calculations, .performance))
<st c="16817">In this example, we marked a specific function with three</st> <st c="16876">different tags.</st>
<st c="16891">The ability to mark a function with multiple tags can be powerful, as it provides flexibility with our</st> <st c="16995">tests’ organization.</st>
<st c="17015">One thing is missing here – even though tagging functions look lovely, we haven’t discussed how to actually use</st> <st c="17128">our tagging.</st>
<st c="17140">Let’s look at</st> *<st c="17155">Figure 14</st>**<st c="17164">.7</st>*<st c="17166">:</st>

<st c="17384">Figure 14.7: The Tags section of the test pane in Xcode</st>
*<st c="17439">Figure 14</st>**<st c="17449">.7</st>* <st c="17451">shows a new section called</st> `<st c="17542">critical</st>` *<st c="17550">tag</st>* <st c="17554">we defined for our</st> `<st c="17574">reset()</st>` <st c="17581">function in the last code example.</st> <st c="17617">Xcode scans our tags’ usage and organizes them for us.</st> <st c="17672">This is how deep the Swift Testing integration with</st> <st c="17724">Xcode is.</st>
<st c="17733">Now that we have all our tags</st> <st c="17763">listed, we can run all our critical</st> <st c="17799">tests (</st>*<st c="17807">Figure 14</st>**<st c="17817">.8</st>*<st c="17819">):</st>

<st c="17828">Figure 14.8: Running all critical tests</st>
<st c="17867">In</st> *<st c="17871">Figure 14</st>**<st c="17880">.8</st>*<st c="17882">, the run button is on the right.</st> <st c="17916">Tapping it will run all our critical</st> <st c="17953">marked tests.</st>
<st c="17966">Now, for the practical usage of tags in testing, working with tags is similar to how tagging works in other places.</st> <st c="18083">When we group tests in files, we usually do that by</st> *<st c="18135">concern</st>* <st c="18142">– a layer, service, module, and so on.</st> <st c="18182">Conversely, tagging helps us group tests by their</st> *<st c="18232">types</st>* <st c="18237">(sanity, smoke or regression, integration, or unit) or a</st> *<st c="18295">property</st>* <st c="18303">(a priority,</st> <st c="18317">for example).</st>
<st c="18330">What are smoke tests?</st>
<st c="18352">We write</st> **<st c="18362">smoke tests</st>** <st c="18373">to check a system’s operations</st> <st c="18404">by testing basic functionality.</st> <st c="18437">While they may sound like a sanity test, they are much lighter and faster than that.</st> <st c="18522">For example, we can try to perform a login and a basic data sync, and the results can indicate</st> <st c="18616">whether we have a severe problem with our app or</st> <st c="18666">the backend.</st>
<st c="18678">Working methodologically with the tagging system can enhance our testing and open</st> <st c="18761">new possibilities.</st>
<st c="18779">Our</st> `<st c="18784">@Test</st>` <st c="18789">macro</st> <st c="18795">features</st> <st c="18804">list doesn’t end with tagging.</st> <st c="18836">Let’s examine a Swift Testing feature that can save us a lot of time –</st> *<st c="18907">arguments</st>*<st c="18916">.</st>
<st c="18917">Working with arguments</st>
<st c="18940">Imagine the following</st> <st c="18962">scenario.</st> <st c="18973">We wrote a Swift function</st> <st c="18998">that performs a very clever calculation – for example, a function that converts meters</st> <st c="19086">to yards:</st>
struct UnitConverter {
static func metersToYards(_ meters: Double) -> Double {
return meters * 1.09361
}
}
<st c="19202">Our function takes a</st> `<st c="19224">meters</st>` <st c="19230">parameter and returns its value in yards.</st> <st c="19273">It looks like a straightforward function, but we must perform several tests to see whether it works</st> <st c="19373">as expected.</st>
<st c="19385">So, let’s write tests for</st> <st c="19412">this function:</st>
struct UnitConverterTests {
@Test func testConvertingMetersToYards_1meter() {
#expect(UnitConverter.metersToYards(1.0) ==
1.09361)
}
@Test func testConvertingMetersToYards_3_5meter() {
#expect(UnitConverter.metersToYards(3.5) ==
3.827635)
}
}
<st c="19669">In this code example, we wrote</st> <st c="19700">two test functions that perform the same test but with different parameters.</st> <st c="19778">They even have very similar names.</st> <st c="19813">Even though this solution works fine, it doesn’t scale up very nicely.</st> <st c="19884">What if we want to test 10 different</st> <st c="19920">variants or parameters?</st> <st c="19945">And what if we need to change the function name we</st> <st c="19996">are testing?</st>
<st c="20008">One option is to perform one test function that contains all of the</st> <st c="20077">different options:</st>
@Test func testConvertingMetersToYards () {
#expect(UnitConverter.metersToYards(1.0) == 1.09361)
#expect(UnitConverter.metersToYards(3.5) == 3.827635)
}
<st c="20249">We created one test function with two</st> `<st c="20288">#expect</st>` <st c="20295">statements in this code example.</st> <st c="20329">That will probably work; however, managing and monitoring them is more challenging now that we have both statements in</st> <st c="20448">one function.</st>
<st c="20461">To solve that, Swift Testing has a feature named</st> **<st c="20511">arguments</st>**<st c="20520">, which allows us to run our tests with different</st> <st c="20570">values repeatedly.</st>
<st c="20588">Let’s see that</st> <st c="20604">in action:</st>
@Test(参数:
func testConvertingMetersToYards(data: <st c="20708">(Double,</st>
#expect(UnitConverter.metersToYards<st c="20764">(data.0) ==</st>
<st c="20786">This code example may look a little cumbersome, but it is straightforward.</st> <st c="20861">We performed three</st> <st c="20880">changes here:</st>
* <st c="20893">We added the</st> `<st c="20907">arguments</st>` *<st c="20916">parameter</st>* <st c="20926">to the</st> `<st c="20934">@Test</st>` <st c="20939">macro, which contains an array of tuples.</st> <st c="20982">Each tuple represents a few meters and its corresponding number of yards.</st> <st c="21056">For example, the</st> `<st c="21073">(1.0, 1.09361)</st>` <st c="21087">tuple represents a conversion between 1 meter and 1.09361 yards.</st> <st c="21153">This array is the list of test variants we are going</st> <st c="21206">to do.</st>
* <st c="21212">We added a</st> *<st c="21224">new tuple parameter called</st>* `<st c="21251">data</st>` *<st c="21255">to our test function</st>*<st c="21276">. With each test run, Swift Testing passes a tuple from the arguments list to the function using this parameter.</st> <st c="21389">The parameter type must be aligned with the</st> <st c="21433">argument type.</st>
* <st c="21447">In the</st> `<st c="21455">#expect</st>` <st c="21462">macro, we now</st> *<st c="21477">compare the two tuple values</st>* <st c="21505">instead of fixed sizes, like in the</st> <st c="21542">previous examples.</st>
<st c="21560">The term</st> *<st c="21570">arguments</st>* <st c="21579">can be misleading.</st> <st c="21599">In the context</st> <st c="21613">of testing, it means</st> <st c="21634">that arguments allow us to run our code in different use cases</st> <st c="21698">and states.</st>
<st c="21709">And if passing all the different use cases within the</st> `<st c="21764">@Test</st>` <st c="21769">macro is cumbersome, we can store them in a</st> <st c="21814">separate variable:</st>
let convertingTests: [(Double, Double)] = [(1.0, 1.09361),
(3.5, 3.827635)]
struct UnitConverterTests {
@Test(arguments: convertingTests)
func testConvertingMetersToYards(data: (Double,
Double)) {
#expect(UnitConverter.metersToYards(data.0) ==
data.1)
}
}
<st c="22088">In this code example, we moved our use cases into a dedicated constant for</st> <st c="22164">better readability.</st>
<st c="22183">If we look at the Xcode testing pane again, we can now see a list of our use cases and their states (</st>*<st c="22285">Figure 14</st>**<st c="22295">.9</st>*<st c="22297">):</st>

<st c="22519">Figure 14.9: Argument testing in the Xcode testing pane</st>
*<st c="22574">Figure 14</st>**<st c="22584">.9</st>* <st c="22586">shows why argument testing in Swift Testing is so powerful.</st> <st c="22647">Instead of having several test functions in the list, we can see one with several</st> <st c="22729">use cases.</st>
<st c="22739">Argument testing adds another</st> <st c="22769">layer to our testing, something</st> <st c="22801">we don’t have</st> <st c="22816">in XCTest.</st>
<st c="22826">Why doesn’t XCTest support parametrized testing?</st>
<st c="22875">Using attributes to perform parametrized</st> <st c="22916">testing is not new in the testing world.</st> <st c="22958">Most testing frameworks support adding arguments to their test functions out of the box.</st> <st c="23047">However, even though it is possible to perform parametrized tests in XCTest, it requires creating several test functions that call a central function that performs the actual test.</st> <st c="23228">This is an ad hoc and unnatural solution.</st> <st c="23270">The reason is that Apple wanted to create a simple testing framework, and locating the test function</st> <st c="23370">in XCTest works according to a simple function signature (functions that start with the phrase</st> `<st c="23466">test</st>`<st c="23470">).</st> <st c="23474">Adding arguments made the locating</st> <st c="23509">process complex.</st>
<st c="23525">Now that we have reviewed the Swift Testing basics, let’s see how to manage</st> <st c="23602">our tests.</st>
<st c="23612">Managing our tests</st>
<st c="23631">Anyone who has previously worked</st> <st c="23664">with tests knows that writing tests is one thing and managing them in the long run</st> <st c="23748">is another.</st>
<st c="23759">If you don’t have testing experience, you might think that simply running all your tests one after the other is sufficient.</st> <st c="23884">But down the road, things become much more complex – different configurations, environments, and even test goals – all translating to a need for a more robust testing</st> <st c="24051">management system.</st>
<st c="24069">Before managing our testing system, let’s review our Xcode</st> <st c="24129">testing structure.</st>
<st c="24147">Going over the testing structure</st>
<st c="24180">So far, we have discussed</st> <st c="24206">how to write testing functions, but besides grouping them in structures, we haven’t discussed anything related to</st> <st c="24321">managing them.</st>
<st c="24335">A whole set of tools can help us manage our test efficiency in Xcode.</st> <st c="24406">Let’s review the different blocks that can help us adapt a flexible system to</st> <st c="24484">our needs:</st>
* **<st c="24494">A testing suite</st>**<st c="24510">: A testing suite can group</st> <st c="24538">several testing functions and</st> <st c="24569">child suites.</st>
* **<st c="24582">A test plan</st>**<st c="24594">: A test plan groups different test</st> <st c="24631">functions and test suites.</st> <st c="24658">It can include or exclude test functions marked with tags.</st> <st c="24717">But it doesn’t stop there – test plans can run multiple times in different configurations with different data and environments.</st> <st c="24845">This is a powerful tool that can help scale up our</st> <st c="24896">testing strategy.</st>
* **<st c="24913">A Scheme</st>**<st c="24922">: Inside each</st> *<st c="24937">Scheme</st>*<st c="24943">, we have several build options.</st> <st c="24976">One is</st> **<st c="24983">Test</st>**<st c="24987">, where we must describe what will happen when testing that specific</st> *<st c="25056">Scheme</st>*<st c="25062">. In the</st> **<st c="25071">Test Build</st>** <st c="25081">option, we can define</st> <st c="25104">precisely what test plans we will run</st> <st c="25141">and on</st> <st c="25149">which target.</st>
<st c="25162">When we look at the different testing building blocks, we can see that the testing structure is complex and requires</st> <st c="25280">some thinking.</st>
<st c="25294">Let’s try to explain the hierarchy by examining</st> *<st c="25343">Figure 14</st>**<st c="25352">.10</st>*<st c="25355">:</st>

<st c="25501">Figure 14.10: Relations between the different building blocks of testing</st>
*<st c="25573">Figure 14</st>**<st c="25583">.10</st>* <st c="25586">shows the relations between the different building blocks of testing.</st> <st c="25657">Next, we will learn how to build them together, starting with</st> <st c="25719">test suites.</st>
<st c="25731">Grouping our test functions into test suites</st>
<st c="25776">The first building block</st> <st c="25801">we are going to discuss is the</st> **<st c="25833">test suite</st>**<st c="25843">. In fact, we have already built a test suite in</st> <st c="25892">this chapter:</st>
@Test func testConvertingMetersToYards_1meter() {
#expect(UnitConverter.metersToYards(1.0) ==
1.09361)
}
}
<st c="26040">Do you remember this code example?</st> <st c="26076">We wrote it in the</st> *<st c="26095">Working with arguments</st>* <st c="26117">section and created a similar test suite for earlier examples.</st> <st c="26181">So, yes, the struct containing our test functions is considered to be a test suite, and Swift Testing recognizes and displays this in the</st> <st c="26319">test pane.</st>
<st c="26329">However, we can annotate a test suite with the</st> `<st c="26377">@Suite</st>` <st c="26383">attribute for better customization.</st> <st c="26420">Let’s add it to our latest</st> <st c="26447">test suite:</st>
@Test func testConvertingMetersToYards_1meter() {
#expect(UnitConverter.metersToYards(1.0) ==
1.09361}
}
<st c="26622">In this code example, we added</st> <st c="26653">the</st> `<st c="26658">@Suite</st>` <st c="26664">swift macro to our</st> `<st c="26684">UnitConverterTests</st>` <st c="26702">structure and, by doing so, gave it a more</st> <st c="26746">readable name.</st>
<st c="26760">Let’s see what our test suite looks like in the test pane in Xcode (</st>*<st c="26829">Figure 14</st>**<st c="26839">.11</st>*<st c="26842">):</st>

<st c="26971">Figure 14.11: The suite in the Xcode test pane</st>
<st c="27017">In</st> *<st c="27021">Figure 14</st>**<st c="27030">.11</st>*<st c="27033">, we can see our new test suite displayed in the</st> <st c="27082">test pane.</st>
<st c="27092">If using the</st> `<st c="27106">@Suite</st>` <st c="27112">macro sounds like how we used the</st> `<st c="27147">@Test</st>` <st c="27152">macro, you are not mistaken; it’s the same idea – providing more information by using</st> <st c="27239">a macro.</st>
<st c="27247">And, just like the</st> `<st c="27267">@Test</st>` <st c="27272">macro, we can also mark test suites</st> <st c="27309">with tags:</st>
@Suite("单位转换器测试", .
struct UnitConverterTests {
}
<st c="27400">In this code example, we marked our new test suite with the critical tag we declared in the</st> *<st c="27493">Tagging our test</st>* *<st c="27510">functions</st>* <st c="27519">section.</st>
<st c="27528">In addition, we can also disable the whole test suite using the same</st> `<st c="27598">disabled()</st>` <st c="27608">function we used in the</st> *<st c="27633">Enabling and disabling</st>* *<st c="27656">tests</st>* <st c="27661">section:</st>
@Suite("单位转换器测试", .
struct UnitConverterTests {
}
<st c="27746">In this code example, we disabled</st> <st c="27780">the</st> `<st c="27785">Unit converter tests</st>` <st c="27805">test suite, and Swift Test will not execute any of its tests in the next</st> <st c="27879">test run.</st>
<st c="27888">Another neat usage for a test suite is its ability to contain nested</st> <st c="27958">test suites:</st>
// 我们的测试函数
}
}
<st c="28118">In this code example, we have a test suite named</st> `<st c="28168">From meters to yards</st>`<st c="28188">, which is part of a bigger test suite named</st> `<st c="28233">Unit</st>` `<st c="28238">converter tests</st>`<st c="28253">.</st>
<st c="28254">Let’s see how this is reflected in the Xcode pane (</st>*<st c="28306">Figure 14</st>**<st c="28316">.12</st>*<st c="28319">):</st>

<st c="28553">Figure 14.12: Nested test suites in the test pane</st>
*<st c="28602">Figure 14</st>**<st c="28612">.12</st>* <st c="28615">shows how our new nested test suite is reflected in the test pane.</st> <st c="28683">We can also customize the nested suites, such as adding tags and</st> <st c="28748">disabling them.</st>
<st c="28763">Now that we know how to define a test suite and tags, it is recommended that we remember each feature’s role.</st> <st c="28874">We use test suites to group different test methods by concern – typically, by writing test functions for a specific class or</st> <st c="28999">a structure.</st>
<st c="29011">Conversely, we use tags to mark our tests</st> <st c="29053">by their type –</st> `<st c="29070">critical</st>`<st c="29078">,</st> `<st c="29080">performance</st>`<st c="29091">,</st> `<st c="29093">integration</st>`<st c="29104">, and so on.</st> <st c="29117">If these are the different roles for tags and test suites, what do we do when we want to manage something such as a sanity or a</st> <st c="29245">regression test?</st>
<st c="29261">That’s what</st> *<st c="29274">test plans</st>* <st c="29284">are for.</st>
<st c="29293">Building test plans</st>
<st c="29313">To better understand</st> <st c="29334">the different testing components, we can think of an app with two layers – the business logic and the UI.</st> <st c="29441">The business logic layer is important, but it doesn’t describe how a user will use our app – the different use cases</st> <st c="29558">and flows.</st>
<st c="29568">We must build the UI layer to complete our app, which handles user stories and flows.</st> <st c="29655">The business logic is analogous to the different testing suites and functions.</st> <st c="29734">These are the building blocks of our testing.</st> <st c="29780">However, testing is always in the context of a specific</st> <st c="29836">development process.</st>
<st c="29856">Let’s try to come up with different</st> <st c="29892">development processes:</st>
* **<st c="29915">Feature development</st>**<st c="29935">: We build new features, often adding new classes, structures,</st> <st c="29999">and entities</st>
* **<st c="30011">Fixing bugs</st>**<st c="30023">: We modify</st> <st c="30036">existing code</st>
* **<st c="30049">Refactoring code</st>**<st c="30066">: We modify existing code for better scalability, maintenance,</st> <st c="30130">or performance</st>
* **<st c="30144">Deployment</st>**<st c="30155">: We prepare an app for deployment</st> <st c="30191">for QA</st> <st c="30198">or production</st>
<st c="30211">This is only a partial list of different development processes, but it demonstrates that we are always in the context of a process when</st> <st c="30348">we develop.</st>
<st c="30359">When we build our testing system, we can describe this process using a test plan.</st> <st c="30442">Let’s add a new test plan to see how</st> <st c="30479">it works.</st>
<st c="30488">Adding a new test plan</st>
**<st c="30511">Test plans</st>** <st c="30522">are a new feature in Xcode, added</st> <st c="30556">to Xcode 11 in 2019\.</st> <st c="30578">They allow us to pick tests or test suites and run them in a specific configuration and environment.</st> <st c="30679">Test plans are our way of expressing how our test functions will</st> <st c="30744">be executed.</st>
<st c="30756">We always run our tests as part of a test plan.</st> <st c="30805">By default, Xcode creates a test plan for us automatically (</st>*<st c="30865">Figure 14</st>**<st c="30875">.13</st>*<st c="30878">):</st>

<st c="31151">Figure 14.13: The autocreated test plan</st>
<st c="31190">In</st> *<st c="31194">Figure 14</st>**<st c="31203">.13</st>*<st c="31206">, we can see that Xcode</st> <st c="31229">created a test plan for us called</st> *<st c="31264">Chapter 14</st>*<st c="31274">. To create a new test plan, we can tap the test plan pop-up menu and select</st> **<st c="31351">New Test Plan</st>**<st c="31364">. After we provide a name for our new test plan, we can see it in our Xcode main pane (</st>*<st c="31451">Figure 14</st>**<st c="31461">.14</st>*<st c="31464">):</st>

<st c="32011">Figure 14.14: The new test plan in Xcode</st>
*<st c="32051">Figure 14</st>**<st c="32061">.14</st>* <st c="32064">shows a new test plan called</st> **<st c="32094">Sanity</st>**<st c="32100">, which has its own</st> <st c="32120">customization screen.</st>
<st c="32141">There are many things we can do to customize</st> <st c="32186">our new</st> <st c="32195">test plan:</st>
* <st c="32205">We can define precisely which test target we want to run.</st> <st c="32264">So far, we have worked on a single test target, but it is possible to have several test targets.</st> <st c="32361">Once we choose the different test targets, we will see the list of test suites and functions and mark what tests we should include</st> <st c="32492">or exclude.</st>
* <st c="32503">We can include or exclude tests marked with specific tags.</st> <st c="32563">For example, we can choose to include only tests marked with the</st> `<st c="32628">critical</st>` <st c="32636">tag for the</st> `<st c="32741">performance</st>` <st c="32752">tag.</st> <st c="32758">This is where the tags become</st> <st c="32788">extremely helpful.</st>
* <st c="32806">If we already have many tests written in XCTest, we can include them in our test plan.</st> <st c="32894">This capability is crucial to preserve</st> <st c="32933">backward compatibility.</st>
<st c="32956">As we can see, the test plan is very flexible in deciding what tests will</st> <st c="33031">be included.</st>
<st c="33043">However, control over the list of test suites and functions is only a fraction of what we can do with test plans.</st> <st c="33158">We can do even more</st> <st c="33178">with configurations.</st>
<st c="33198">Configuring our test plan</st>
<st c="33224">When we started explaining</st> <st c="33251">test plans, we said that part of the idea of creating one is defining the environment in which the test plan runs.</st> <st c="33367">One example of such an environment is localization – language, region, and location can influence our app in certain</st> <st c="33484">use cases.</st>
<st c="33494">Trying to simulate an environment for our test functions can be challenging; therefore, test plans have a feature called</st> **<st c="33616">Configurations</st>** <st c="33630">(</st>*<st c="33632">Figure 14</st>**<st c="33641">.15</st>*<st c="33644">):</st>

<st c="34627">Figure 14.15: The test plan’s Configurations tab</st>
*<st c="34675">Figure 14</st>**<st c="34685">.15</st>* <st c="34688">shows a tab bar at the top of the</st> **<st c="34723">Sanity</st>** <st c="34729">main pane.</st> <st c="34741">The</st> **<st c="34745">Tests</st>** <st c="34750">tab defines the included tests in the test plan, and the</st> **<st c="34808">Configurations</st>** <st c="34822">tab defines the different configurations for the</st> <st c="34872">test plan.</st>
<st c="34882">To add a new configuration, we tap the plus button at the bottom of</st> <st c="34951">the window.</st>
<st c="34962">A test plan can have many configurations.</st> <st c="35005">Each configuration contains a list of settings that can affect our test results.</st> <st c="35086">Let’s examine</st> <st c="35100">them briefly:</st>
* **<st c="35113">Arguments</st>**<st c="35123">: Each app can run with different Launch and environment variables.</st> <st c="35192">We can use them to override our A/B test configuration or define a specific API endpoint.</st> <st c="35282">Arguments are powerful tools that help us adjust our app to</st> <st c="35342">our needs.</st>
* **<st c="35352">Localization</st>**<st c="35365">: Language, region, and location are all part of the localization list of settings that we can define.</st> <st c="35469">These settings can influence available features, texts, measurement units, and</st> <st c="35548">other behavior.</st>
* **<st c="35563">UI testing</st>**<st c="35574">: If our test plan includes UI tests (not supported yet in Swift Testing), we can decide what happens during screen capturing if there is a</st> <st c="35715">test failure.</st>
* **<st c="35728">Distribution</st>**<st c="35741">: Some APIs can behave differently when running on the App Store than on TestFlight – for example, collecting beta testers’ feedback, sandbox issues, and enabling/disabling beta</st> <st c="35920">testing features.</st>
* **<st c="35937">Test Execution</st>**<st c="35952">: Here, we can define the test plan execution behavior, including the execution order, timeouts, and</st> <st c="36054">repetition settings.</st>
* **<st c="36074">Runtime API Checking, Runtime Sanitization</st>**<st c="36117">: Different runtime settings such as memory management, main thread checker,</st> <st c="36195">and sanitization.</st>
<st c="36212">That’s a long list of settings!</st> <st c="36245">I felt that when I looked at</st> *<st c="36274">Figure 14</st>**<st c="36283">.15</st>*<st c="36286">, but now we have confirmation after reviewing almost</st> <st c="36340">each one.</st>
<st c="36349">However, the idea behind configurations</st> <st c="36389">is that we don’t need to redefine all the settings each time we create a new configuration.</st> <st c="36482">If you open your Xcode and create a new test plan, you can see something called</st> **<st c="36562">Shared Settings</st>** <st c="36577">(</st>*<st c="36579">Figure 14</st>**<st c="36588">.16</st>*<st c="36591">):</st>

<st c="36721">Figure 14.16: Shared Settings</st>
*<st c="36750">Figure 14</st>**<st c="36760">.16</st>* <st c="36763">focuses on the list of configurations with</st> **<st c="36807">Shared Settings</st>** <st c="36822">at the top.</st> <st c="36835">The</st> **<st c="36839">Shared Settings</st>** <st c="36854">configuration contains the settings for all configurations unless we explicitly change a specific setting for a</st> <st c="36967">particular configuration.</st>
<st c="36992">Consider a typical use case – we would probably want the same settings for all configurations except for one or two (e.g., a configuration for different locations or distributions).</st> <st c="37175">In this case, we will have the same settings except for the region or the</st> <st c="37249">distribution method.</st>
<st c="37269">Xcode executes all the configurations in a sequence when running a test plan.</st> <st c="37348">However, you can disable a specific configuration by right-clicking on it in the configurations list and</st> <st c="37453">selecting</st> **<st c="37463">Disable</st>**<st c="37470">.</st>
<st c="37471">So, let’s say we created a sanity test plan</st> <st c="37515">and a regression test plan.</st> <st c="37544">What do we do from here?</st> <st c="37569">How can we tell Xcode what to execute when running tests?</st> <st c="37627">This is where the</st> *<st c="37645">Scheme</st>* <st c="37651">comes</st> <st c="37658">into play.</st>
<st c="37668">Setting up a Scheme</st>
<st c="37688">This chapter is about Swift Testing, not the Xcode</st> <st c="37739">build system, but we can’t discuss testing and</st> <st c="37787">ignore</st> **<st c="37794">Schemes</st>**<st c="37801">.</st>
<st c="37802">Schemes are fundamental to managing our project’s build and execution configurations.</st> <st c="37889">A</st> *<st c="37891">Scheme</st>* <st c="37897">defines how our project is built, executed,</st> <st c="37942">and tested.</st>
<st c="37953">We can write dozens of test functions and create as many test plans as we want, but the bottom line is that when we select</st> **<st c="38077">Test</st>** <st c="38081">from the Xcode menu or run tests from our CI/CD environment, the</st> *<st c="38147">Scheme</st>* <st c="38153">defines precisely what</st> <st c="38177">will happen.</st>
<st c="38189">What is CI/CD?</st>
**<st c="38204">CI/CD</st>** <st c="38210">stands for</st> **<st c="38222">Continuous Integrations/Continuous Deployment</st>**<st c="38267">. We use these practices</st> <st c="38291">to automate our app build and deploy process.</st> <st c="38338">A crucial part of this process is testing – before we deploy a build to TestFlight or the App Store, we want to perform testing to ensure we don’t have regressions or other issues.</st> <st c="38519">When we build our CI/CD process, we often choose what Scheme</st> <st c="38580">to execute.</st>
<st c="38591">Looking at the Xcode window, we can locate the</st> *<st c="38639">Scheme</st>* <st c="38645">name next to the project name.</st> <st c="38677">Tapping it will open a list of Schemes where we can change the current</st> *<st c="38748">Scheme</st>* <st c="38754">or edit it (</st>*<st c="38767">Figure 14</st>**<st c="38777">.17</st>*<st c="38780">):</st>

<st c="39710">Figure 14.17: Editing the current Scheme</st>
*<st c="39750">Figure 14</st>**<st c="39760">.17</st>* <st c="39763">shows how to reach</st> <st c="39782">the pop-up</st> *<st c="39794">Scheme</st>* <st c="39800">menu.</st> <st c="39807">Tapping on the</st> **<st c="39822">Edit Scheme…</st>** <st c="39834">option leads us to the</st> **<st c="39858">Edit Scheme</st>** <st c="39869">screen (</st>*<st c="39878">Figure 14</st>**<st c="39888">.18</st>*<st c="39891">):</st>

<st c="40152">Figure 14.18: The Edit Scheme screen</st>
*<st c="40188">Figure 14</st>**<st c="40198">.18</st>* <st c="40201">shows that the</st> *<st c="40217">Scheme</st>* <st c="40223">has six different actions—</st>**<st c="40250">Build</st>**<st c="40256">,</st> **<st c="40258">Run</st>**<st c="40261">,</st> **<st c="40263">Test</st>**<st c="40267">,</st> **<st c="40269">Profile</st>**<st c="40276">,</st> **<st c="40278">Analyze</st>**<st c="40285">, and</st> **<st c="40291">Archive</st>**<st c="40298">. In this screenshot, we will focus on the</st> **<st c="40341">Test</st>** <st c="40345">action.</st>
<st c="40353">Besides choosing the configuration (</st>**<st c="40390">Debug</st>** <st c="40396">or</st> **<st c="40400">Release</st>** <st c="40407">in our case), we can determine what test plans to run.</st> <st c="40463">We can add an existing or new test plan using the plus button at the</st> <st c="40532">bottom left.</st>
<st c="40544">That’s where we decide what happens when executing the</st> **<st c="40600">Test</st>** <st c="40604">action on our</st> *<st c="40619">Scheme</st>*<st c="40625">. Having several</st> *<st c="40642">Schemes</st>* <st c="40649">configured differently for various purposes can be valuable when we connect our project to a</st> <st c="40743">CI/CD system.</st>
<st c="40756">For example, we can run a performance test once a month and sanity every night, just by creating two different Schemes that run different</st> <st c="40895">test plans.</st>
<st c="40906">Now that we know how to create</st> <st c="40937">test functions, suites, test plans, and Schemes, let’s flip to the other side of the equation and see how to write</st> <st c="41053">testable code.</st>
<st c="41067">Tips to write testable code</st>
<st c="41095">One of the biggest challenges</st> <st c="41126">developers face when they try to write tests for code is struggling to write tests for existing functions that could be more testable – for example, functions that contain code that performs network requests or functions that have external dependencies that are difficult to</st> <st c="41401">set up.</st>
<st c="41408">Writing testable code usually goes hand in hand with writing clean and efficient code.</st> <st c="41496">However, we should still follow some writing guidelines if we want our functions to</st> <st c="41580">be testable.</st>
<st c="41592">Let’s explore some of</st> <st c="41615">them now.</st>
<st c="41624">Writing pure functions</st>
**<st c="41647">Pure functions</st>** <st c="41662">are functions that, given the same</st> <st c="41697">input, always return</st> <st c="41718">the same output and don’t rely on external states or have any</st> <st c="41781">side effects.</st>
<st c="41794">For instance, take the</st> <st c="41818">following example:</st>
class NumberFilter {
var numbers: [Int] = []
var filteredNumbers: [Int] = [] <st c="41914">func filterNumbers(predicate: (Int) -> Bool) {</st> self.filteredNumbers =
self.numbers.filter(predicate)
}
}
<st c="42018">This code example contains a</st> `<st c="42048">NumberFilter</st>` <st c="42060">class with a function called</st> `<st c="42090">filterNumbers</st>`<st c="42103">. This class performs a predicate on an instance variable and stores the results in another</st> <st c="42195">instance variable.</st>
<st c="42213">This is a classic example of a non-pure function, since it relies on an external variable and has a side effect.</st> <st c="42327">Now, imagine we want to test this function – it requires us to set up a</st> `<st c="42399">NumberFilter</st>` <st c="42411">instance and set the</st> `<st c="42433">numbers</st>` <st c="42440">variable.</st> <st c="42451">In addition, we need to check the result using the same</st> `<st c="42507">NumberFilter</st>` <st c="42519">instance, with the</st> `<st c="42539">filtersNumbers</st>` <st c="42553">instance.</st>
<st c="42563">The class can change down the road and may require more setup than before, breaking</st> <st c="42648">our test.</st>
<st c="42657">Instead, we can make this function</st> <st c="42692">pure,</st> <st c="42699">like this:</st>
func filterNumbers(_ numbers: [Int], predicate: (Int) -> Bool) -> [Int] {
return numbers.filter(predicate)
}
<st c="42818">In the modified example, our function</st> <st c="42856">receives the input as a parameter and returns the results as part of its output.</st> <st c="42938">This change makes it agnostic to external states and easy</st> <st c="42996">to test.</st>
<st c="43004">Separating your code based on concerns</st>
<st c="43043">As always, a good separation</st> <st c="43073">is crucial for our project maintenance (which we will cover in more detail in</st> *<st c="43151">Chapter 15</st>*<st c="43161">).</st> <st c="43165">However, separation is also essential</st> <st c="43203">for testing.</st>
<st c="43215">The fundamental separation of concerns idea states that each part of our code, whether a variable, function, class, or module, should have one and only</st> <st c="43368">one responsibility.</st>
<st c="43387">Let’s take the following code as</st> <st c="43421">an example:</st>
func processAndSaveData(_ input: String) -> Bool {
// 数据处理
let processedData = // <执行一些数据操作
code>
// 数据保存
return databaseService.saveData(processedData)
}
<st c="43627">The</st> `<st c="43632">processAndSaveData</st>` <st c="43650">function is responsible for two tasks – processing the input data and saving it to the</st> <st c="43738">database service.</st>
<st c="43755">We can see that the string processing code uses the same function that performs data saving.</st> <st c="43849">If we want to test whether the string processing succeeded, we must also ensure that the output has been saved successfully.</st> <st c="43974">These two responsibilities are coupled, which makes the code very difficult</st> <st c="44050">to test.</st>
<st c="44058">To solve that, we can separate the processing code into</st> <st c="44115">another function:</st>
func processAndSaveData(_ input: String) -> Bool {
// 数据处理
let processedData = processData(input)
// 数据保存
return databaseService.saveData(processedData)
}
<st c="44385">In this example, we gave the processing data task its own function, and now it is possible to test it regardless</st> <st c="44497">of the</st> <st c="44505">data-saving part.</st>
<st c="44522">Our last tip also discusses coupling but, in another context –</st> *<st c="44586">protocols</st>*<st c="44595">.</st>
<st c="44596">Performing mocking using protocols</st>
<st c="44631">Sometimes, we don’t have a choice</st> <st c="44665">but to test functions</st> <st c="44687">that reach our network or any external service that can’t really simulate</st> <st c="44762">during tests.</st>
<st c="44775">To overcome that, we can easily create mocks for these services</st> <st c="44840">using</st> **<st c="44846">protocols</st>**<st c="44855">.</st>
<st c="44856">Look at the</st> <st c="44869">following code:</st>
class UserViewModel {
private let networkService<st c="44933">: NetworkServiceProtocol</st> var user: User? init(networkService<st c="44994">: NetworkServiceProtocol)</st> {
self.networkService = networkService
}
func fetchUserDetails(for userId: String, completion:
@escaping () -> Void) {
networkService.fetchUserDetails(for: userId) {
[weak self] user in
self?.user = user
completion()
}
}
}
<st c="45243">This code example contains a</st> `<st c="45273">UserViewModel</st>` <st c="45286">class that fetches user details from the server and stores the results in an instance variable.</st> <st c="45383">Testing the</st> `<st c="45395">fetchUserDetails</st>` <st c="45411">function requires performing a request to the server, which can make our</st> <st c="45485">test unstable.</st>
<st c="45499">To solve that, we can create a mock class that conforms to</st> `<st c="45559">NetworkServiceProtocol</st>` <st c="45581">and simulate the</st> <st c="45599">network service:</st>
class MockNetworkService: NetworkServiceProtocol {
var userToReturn: User? func fetchUserDetails(for userId: String, completion:
@escaping (User?) -> Void) {
completion(userToReturn)
}
}
<st c="45802">This example demonstrates a mock</st> <st c="45835">class that accepts</st> <st c="45854">a user’s return and can easily mock the whole network process.</st> <st c="45918">We achieved that by using a protocol and dependency injection, and we can do the same to store data, authenticate, and</st> <st c="46037">so on.</st>
<st c="46043">Summary</st>
<st c="46051">Testing is crucial to our mission to produce stable, high-quality code.</st> <st c="46124">Remember, writing tests is not just a fundamental part of being a professional iOS developer – it is also part of a culture of doing</st> <st c="46257">things right.</st>
<st c="46270">In this chapter, we’ve learned about the testing history in Xcode, covered the Swift Testing basics by writing simple tests, learned how to manage our tests using suites, test plans, and Schemes, and even discussed some useful tips to make our code more testable.</st> <st c="46535">Now, we should be able to set up a new test plan for</st> <st c="46588">our project!</st>
<st c="46600">Our following and final chapter, on architecture, touches on some of the principles we discussed here and will also help us create a</st> <st c="46734">stable project.</st>
第十六章:15
探索适用于 iOS 的架构
-
理解架构的重要性 。 -
学习架构究竟是什么 -
了解不同的架构,如多层、模块化、和六边形 -
通过分离、测试、和维护来比较不同的架构
技术要求
理解架构的重要性
-
IDE :熟悉 Xcode,其调试工具、配置、模拟器、构建器和代码签名 至关重要。 -
语言 :无论是 Swift、Objective C 还是 C++,语言是 iOS 开发的根本部分。 它是我们每天实现应用逻辑和 设计模式的基础。 -
系统 :理解 iOS 的独特特性、优势和局限性是关键。 最终,我们是在一个具有自己规则 和政策的环境中开发。 -
SDK :SDK 提供了工具集,让我们能够做我们想要做的任何事情。 SwiftUI、UIKit、Foundation、Core Animation 以及许多其他框架都是 SDK 的一部分,有了它们,我们可以创建带有用户输入组件和 持久存储的美丽界面。 -
设计模式 :这些是解决我们日常遇到的常见问题和任务的解决方案。 我们。 -
架构 :我们代码和项目的整体组织结构被称为 其架构。
-
可维护性 :我们的项目可能会变得更大,维护起来更具挑战性。 良好的架构使我们的代码库更容易理解和阅读。 它还使修改 和重构变得更加容易。 -
可扩展性 :在保持我们的项目简单和稳定的同时添加更多功能是应用成功的关键。 糟糕的架构可能需要在添加 新功能时进行重大重构。 -
灵活性 :良好的架构使我们能够根据需求的变化快速更改应用的工作方式。 它还帮助我们添加新功能或替换 第三方框架。
了解建筑究竟是什么 意味着
与此相反,每一层楼都有自己的目标和指定用途。
现在,让我们回到我们的移动应用。我们应该将移动应用的结构想象成一个私人住宅。数据在不同的层级中流动——数据、业务逻辑和
我们知道的更多设计模式,我们就有更多的解决方案。
此外,让我们继续使用房屋的隐喻。在这种情况下,我们可以得出另一个结论——我们关于架构的选择会影响我们用于楼层的设计模式,包括楼层的尺寸和形状,甚至它们是如何连接的。
那么,有哪些不同的架构类型可供选择,我们如何选择一个适合我们需求的架构呢?
概述不同的架构
开发者在选择项目架构时犯有两个常见的错误。首先,他们经常说,“
MVVM 不是一个架构——它是一个旨在解决特定屏幕状态和逻辑管理的模式。它不仅不处理应用结构,甚至也不描述我们通常如何处理我们的屏幕。它只描述了特定的屏幕,例如登录或设置屏幕。
第二个错误是认为我们只能从列表中选择最常见和最受欢迎的架构之一用于我们的项目。实际上,你读到的大多数架构实际上是一套原则,可以帮助我们决定如何构建项目。
一些原则提供了灵活性和解耦,而另一些可能会增加项目开销。我们应该始终考虑权衡;这些在架构设计中变得更加重要。
让我们从最基本的设计理念开始:多层架构。
将我们的项目分层
将项目分层,通常分为三层,是许多项目中的常见架构决策。

*
-
数据 有时也可以称为 服务 。数据层处理数据的持久存储、模型实体、网络处理以及主要处理低级别数据的各种服务,而不考虑 项目的逻辑。 -
业务逻辑 有时也可以称为 领域 。业务逻辑层处理应用的主要逻辑,包括规则和 数据处理。 -
表示层 处理 UI、用户交互 和导航。
控制应用数据流

-
登录 UI 组件直接与安全组件通信,可能为了了解当前的 认证状态 -
线程 UI 组件与网络组件通信,以在 UI 中展示网络状态 的
添加更多层级



将我们的项目划分为模块
了解在模块开发中需要考虑的不同因素
-
功能性和业务领域 :我们已经在上一段中提到过这一点。 将应用程序分解为核心功能可以是我们理解项目不同模块——逻辑、歌曲播放器、提醒、入门等——的一个很好的开始。 -
可重用性 :将我们在应用程序的不同部分使用的功能分组是理解如何创建模块的另一种方式。 例如,如果我们的应用程序执行不同的 HTTP 请求,我们可能会创建一个网络模块来管理所有的 API 调用。 另一个例子是共享组件——如果我们使用相同的按钮在不同的屏幕上,这可能是一个迹象,表明它应该是包含不同可重用组件的 UI 模块的一部分。 -
解耦 :我们的模块应该尽可能地与其他模块解耦。 模块之间的相互依赖程度可以定义是否将其创建为模块是一个优秀的决定。 此外,如果可以为模块创建一个清晰的接口,这也是它可能是一个 好模块的另一个迹象。 -
协作 :想象一下 几个团队正在我们的项目上工作。 他们可以不互相干扰地工作,这表明模块的分离做得很好。 请注意,无论我们是一个人的团队还是由六个开发者组成的五个团队,这条规则的相关性都是一样的。 原则是 最重要的。
组织我们项目中的代码
-
在物理方法中,我们使用专用工具创建我们的模块。 例如,CocoaPods、Swift Packages 和 XCFrameworks 提供了一种将我们的代码物理封装成 代码单元的方法。 -
在功能方法中,我们不使用任何特定工具,而是将代码组织成文件夹。 这种简单的方法非常适合小型项目 或团队。
将多层架构与模块相结合



构建六边形架构

学习端口和适配器的概念
-
计算机上的一个端口,允许我们连接设备;例如,USB 或 HDMI -
安装在设备上且知道如何与计算机所需的端口和接口工作的驱动程序 计算机需要
-
端口 :这是一个 领域内外部的入口或出口点。 在 Swift 中,我们使用协议来描述 端口。 -
适配器 :当一个特定类别想要连接到端口时,它需要实现 端口协议。
理解驱动适配器
理解被驱动适配器

在实践中实现六边形架构
定义不同的端口
protocol LoginUseCaseProtocol {
func login(username: String,
password: String,
completion: @escaping (Result<User, Error>)
-> Void)
}
<st c="32793">LoginUseCaseProtocol</st>
enum NetworkRequestType{
case login
}
protocol NetworkServiceProtocol {
func performRequest(requestType: NetworkRequestType,
params: [String: Any],
completion: @escaping (Result<User,
Error>) -> Void )
}
<st c="33228">NetworkServiceProtocol</st>
创建登录用例
class LoginUseCase: LoginUseCaseProtocol {
let authService: NetworkServiceProtocol
init(authService: NetworkServiceProtocol) {
self.authService = authService
}
func login(username: String, password: String,
completion: @escaping (Result<User, any Error>) ->
Void) {
authService.performRequest(requestType: .login,
params: ["username" : username,
"password" : password],
completion: completion)
}
}
<st c="33898">LoginUseCase</st> <st c="33932">LoginUseCaseProtocol</st> <st c="34019">NetworkServiceProtocol</st>
创建网络服务
class NetworkService { }
extension NetworkService: <st c="34459">NetworkServiceProtocol</st> {
func performRequest(requestType: NetworkRequestType,
params: [String : Any],
completion: @escaping (Result<User, any Error>) -> Void) {
// implementation needed
}
}
<st c="34653">NetworkService</st> <st c="34689">NetworkServiceProtocol</st>
创建登录界面
import SwiftUI
struct LoginView: View {
@State var username: String = ""
@State var password: String = "" <st c="34969">let loginUseCase: LoginUseCaseProtocol</st> var body: some View {
VStack {
TextField("Username", text: $username)
SecureField("Password", text: $password)
Button("Login") {
loginUseCase.login(username: username,
password: password) { result in
// handle result
}
}
}
.padding()
将所有东西连接起来
@main
struct HexagonalAppApp: App {
var body: some Scene {
WindowGroup {
let networkService = NetworkService()
let loginUserCase =
LoginUseCase(networkService: networkService)
LoginView(loginUseCase: loginUserCase)
}
}
}
<st c="35822">NetworkService</st> <st c="35965">LoginView</st>
-
不同的关注点非常清晰。 我们确切地知道应用的核心逻辑是什么,外部服务是什么,以及这些模块的客户是什么。 -
由于它们彼此解耦并且只通过协议进行通信,因此维护每个适配器或核心逻辑案例变得极其容易。 当我们说维护时,我们指的是测试、重构和 错误修复。 -
在我们的应用中更换部分,如服务或用例,变得非常容易。 让我们尝试回忆一下我们曾经工作过的应用或甚至系统。 想象一下替换网络服务、持久存储或甚至 一个屏幕需要付出多少努力。 -
添加更多功能和模块不需要对我们项目进行重大更改。 在添加新屏幕或用例时重用现有代码 变得容易。 重用现有代码
比较不同的架构
-
使用协议来解耦与 外部服务 的通信 -
将领域模型作为 应用 的核心
通过关注点分离
通过测试
通过维护和可扩展性
| 清晰的分层(UI、逻辑、数据);随着依赖性增加可能变得不太灵活 | 独立、强分离,灵活的接口 | 与外部系统有明确的分离;核心逻辑通过端口和适配器进行隔离 | |
| 在层内容易,层间复杂 | 单个模块易于测试,模块内的集成测试也是如此 | 核心逻辑非常易于测试;易于模拟适配器 | |
| 由于紧密耦合可能具有挑战性 | 由于模块化;对模块的影响最小 | 由于与外部变化的隔离而变得简单 | |
| 受限于层交互 | 模块可以独立扩展,因此具有高度可扩展性 | 通过添加新的适配器进行扩展;核心保持稳定 |
总结


浙公网安备 33010602011771号