2025-02-08 16:48:53
,某些文章具有时效性,若有错误或已失效,请在下方留言。SwiftUI provides a task()
modifier that starts a new task as soon as a view appears, and automatically cancels the task when the view disappears. This is sort of the equivalent of starting a task in onAppear()
then cancelling it onDisappear()
, although task()
has an extra ability to track an identifier and restart its task when the identifier changes.
SwiftUI 提供了一个 task()
修饰符,该修饰符可在视图出现时立即开始新任务,并在视图消失时自动取消任务。这有点等同于 在 onAppear()
中启动任务,然后取消它 onDisappear(),
尽管 task()
具有跟踪标识符并在标识符更改时重新启动其任务的额外功能。
In the simplest scenario – and probably the one you’re going to use the most – task()
is the best way to load your view’s initial data, which might be loaded from local storage or by fetching and decoding a remote URL.
在最简单的场景中 – 也可能是你最常使用的场景 – task()
是加载视图初始数据的最佳方式,这些数据可以从本地存储加载,也可以通过获取和解码远程 URL 来加载。
Important: Because all SwiftUI views are automatically run on the main actor, the tasks they launch will also automatically run on the main actor until you move them elsewhere.
重要: 由于所有 SwiftUI 视图都会在主角色上自动运行,因此它们启动的任务也将自动在主角色上运行,直到您将它们移动到其他位置。
For example, this downloads data from a server and decodes it into an array for display in a list:
例如,这会从服务器下载数据并将其解码为数组,以便在列表中显示:
struct Message: Decodable, Identifiable {
let id: Int
let user: String
let text: String
}
struct ContentView: View {
@State private var messages = [Message]()
var body: some View {
NavigationStack {
List(messages) { message in
VStack(alignment: .leading) {
Text(message.user)
.font(.headline)
Text(message.text)
}
}
.navigationTitle("Inbox")
.task {
await fetchData()
}
}
}
func fetchData() async {
do {
let url = URL(string: "https://hws.dev/inbox.json")!
let (data, _) = try await URLSession.shared.data(from: url)
messages = try JSONDecoder().decode([Message].self, from: data)
} catch {
messages = [
Message(id: 0, user: "Failed to load inbox.", text: "Please try again later.")
]
}
}
}
Important: The task()
modifier is a great place to load the data for your SwiftUI views. Remember, they can be recreated many times over the lifetime of your app, so you should avoid putting this kind of work into their initializers if possible.
重要: task()
修饰符是加载 SwiftUI 视图数据的好地方。请记住,它们可以在应用程序的生命周期内多次重新创建,因此应尽可能避免将此类工作放入其初始值设定项中。
A more advanced usage of task()
is to attach some kind of Equatable
identifying value – when that value changes SwiftUI will automatically cancel the previous task and create a new task with the new value. This might be some shared app state, such as whether the user is logged in or not, or some local state, such as what kind of filter to apply to some data.task()
的更高级用法是附加某种 Equatable
标识值 – 当该值发生变化时,SwiftUI 将自动取消上一个任务,并使用新值创建一个新任务。这可能是一些共享应用程序状态,例如用户是否已登录,也可能是某些本地状态,例如要对某些数据应用哪种筛选器。
As an example, we could upgrade our messaging view to support both an Inbox and a Sent box, both fetched and decoded using the same task()
modifier. By setting the message box type as the identifier for the task with .task(id: selectedBox)
, SwiftUI will automatically update its message list every time the selection changes.
例如,我们可以升级我们的消息传递视图以支持 Inbox 和 Sent 框,它们都使用相同的 task()
修饰符进行获取和解码。通过使用 .task(id: selectedBox)
将消息框类型设置为任务的标识符,SwiftUI 将在每次选择更改时自动更新其消息列表。
Here’s how that looks in code:
下面是它在代码中的样子:
struct Message: Decodable, Identifiable {
let id: Int
let user: String
let text: String
}
// Our content view is able to handle two kinds of message box now.
struct ContentView: View {
@State private var messages = [Message]()
@State private var selectedBox = "Inbox"
let messageBoxes = ["Inbox", "Sent"]
var body: some View {
NavigationStack {
List(messages) { message in
VStack(alignment: .leading) {
Text(message.user)
.font(.headline)
Text(message.text)
}
}
.navigationTitle(selectedBox)
// Our task modifier will recreate its fetchData() task whenever selectedBox changes
.task(id: selectedBox) {
await fetchData()
}
.toolbar {
// Switch between our two message boxes
Picker("Select a message box", selection: $selectedBox) {
ForEach(messageBoxes, id: \.self, content: Text.init)
}
.pickerStyle(.segmented)
}
}
}
// This is almost the same as before, but now loads the selectedBox JSON file rather than always loading the inbox.
func fetchData() async {
do {
let url = URL(string: "https://hws.dev/\(selectedBox.lowercased()).json")!
let (data, _) = try await URLSession.shared.data(from: url)
messages = try JSONDecoder().decode([Message].self, from: data)
} catch {
messages = [
Message(id: 0, user: "Failed to load message box.", text: "Please try again later.")
]
}
}
}
Tip: That example uses the shared URLSession
, which means it will cache its responses and so load the two inboxes only once. If that’s what you want you’re all set, but if you want it to always fetch the files make sure you create your own session configuration and disable caching.
提示: 该示例使用共享 URLSession
,这意味着它将缓存其响应,因此仅加载两个收件箱一次。如果这是您想要的,那么您已经准备好了,但是如果您希望它始终获取文件,请确保创建自己的会话配置并禁用缓存。
One particularly interesting use case for task()
is with AsyncSequence
collections that continuously generate values. This might be a server that maintains an open connection while sending fresh content, it might be the URLWatcher
example we looked at previously, or perhaps just a local value.task()
的一个特别有趣的用例是持续生成值的 AsyncSequence
集合。这可能是在发送新内容时保持开放连接的服务器,可能是我们之前看到的 URLWatcher
示例,或者可能只是一个本地值。
For example, we could write a simple random number generator that regularly emits new random numbers – with the task()
modifier we can constantly watch that for changes, and stream the results into a SwiftUI view.
例如,我们可以编写一个简单的随机数生成器,它会定期发出新的随机数——使用 task()
修饰符,我们可以持续观察它的变化,并将结果流式传输到 SwiftUI 视图中。
To bring this example to life, we’re going to add one more thing: the random number generator will print a message every time a number is generated, and the resulting number list will be shown inside a detail view. Both of these are done so you can see how task()
automatically cancels its work: the numbers will automatically start streaming when the detail view is shown, and stop streaming when the view is dismissed.
为了让这个例子栩栩如生,我们将再添加一件事:随机数生成器将在每次生成数字时打印一条消息,结果数字列表将显示在详细信息视图中。完成这两项作是为了让你看到 task()
是如何自动取消其工作的:当 detail 视图显示时,数字将自动开始流式传输,当视图被关闭时,数字将停止流式传输。
Here’s the code: 这是代码:
// A simple random number generator sequence
struct NumberGenerator: AsyncSequence, AsyncIteratorProtocol {
let range: ClosedRange<Int>
let delay: Double = 1
mutating func next() async -> Int? {
// Make sure we stop emitting numbers when our task is cancelled
while Task.isCancelled == false {
try? await Task.sleep(for: .seconds(delay))
print("Generating number")
return Int.random(in: range)
}
return nil
}
func makeAsyncIterator() -> NumberGenerator {
self
}
}
// This exists solely to show DetailView when requested.
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("Start Generating Numbers") {
DetailView()
}
}
}
}
// This generates and displays all the random numbers we've generated.
struct DetailView: View {
@State private var numbers = [String]()
let generator = NumberGenerator(range: 1...1000)
var body: some View {
List(numbers, id: \.self, rowContent: Text.init)
.task {
await generateNumbers()
}
}
func generateNumbers() async {
for await number in generator {
numbers.insert("\(numbers.count + 1). \(number)", at: 0)
}
}
}
Notice how the generateNumbers()
method at the end doesn’t actually have any way of exiting? That’s because it will exit automatically when generator
stops returning values, which will happen when the task is cancelled, and that will happen when DetailView
is dismissed – it takes no special work from us.
注意到最后的 generateNumbers()
方法实际上没有任何退出方式吗?这是因为当generator
停止返回值时,它会自动退出,当任务被取消时,也会在 DetailView
被关闭时发生——我们不需要做任何特殊工作。
Tip: The task()
modifier accepts a priority
parameter if you want fine-grained control over your task’s priority. For example, use .task(priority: .low)
to create a low-priority task.
提示: task()
修饰符接受 priority
参数,以便对任务的优先级进行精细控制。例如,使用 .task(priority: .low)
创建低优先级任务。