View in English

  • 打开菜单 关闭菜单
  • Apple Developer
搜索
关闭搜索
  • Apple Developer
  • 新闻
  • 探索
  • 设计
  • 开发
  • 分发
  • 支持
  • 账户
在“”范围内搜索。

快捷链接

5 快捷链接

视频

打开菜单 关闭菜单
  • 专题
  • 相关主题
  • 所有视频
  • 关于

返回 WWDC25

大多数浏览器和
Developer App 均支持流媒体播放。

  • 简介
  • 转写文稿
  • 代码
  • 探索 SwiftUI 中的并发机制

    探究 SwiftUI 如何利用 Swift 并发机制来构建安全且响应灵敏的 App。探索 SwiftUI 如何默认使用主 Actor,并将工作转移给其他 Actor。了解如何解释并发注解并使用 SwiftUI 的事件循环来管理异步任务,以实现流畅的动画和 UI 更新。你还将了解如何避免数据争用并放心地编写代码。

    章节

    • 0:00 - 简介
    • 2:13 - 主 Actor 牧场
    • 7:17 - 并发山峰
    • 16:53 - 代码营地
    • 23:47 - 后续步骤

    资源

    • Concurrency
    • Mutex
    • The Swift Programming Language: Concurrency
    • Updating an App to Use Swift Concurrency
      • 高清视频
      • 标清视频

    相关视频

    WWDC25

    • 跟着视频学编程:使用 Swift 并发机制提升 App 性能
    • 采用 Swift 并发

    WWDC23

    • 探索 SwiftUI 动画
  • 搜索此视频…

    Hi folks, welcome aboard. I’m your tour guide, Daniel, from the SwiftUI team. Together, we’ll explore the landscape of concurrency, and SwiftUI app development.

    You’re here because you’ve heard the stories about these dangerous creatures called data-race bugs. You might have run into some yourself in the past. I'm talking about unexpected app states, glitchy animations, and even permanent data losses. But don't worry, this tour is 100% safe. Because with Swift and SwiftUI, we’re leaving those data-race animals in the rear-view mirror. SwiftUI runs your code concurrently in various ways. In this tour, you’ll learn how to identify them via the concurrency annotations from SwiftUI APIs. In the end, I hope you come out more confident, and fearless in your own SwiftUI app adventures.

    Swift 6.2 introduces a new language mode, which marks all types in a module with the @MainActor annotation implicitly. Everything we’ll see in this tour applies with or without this new mode. This tour features three attractions. We’ll start from the beautiful Meadows at the Main Actor, and appreciate how SwiftUI treats the main actor as the compile time and runtime default for applications.

    Then we’ll visit Concurrency Cliffs and explore how SwiftUI helps apps avoid UI hitches by offloading work from the main thread, and, at the same time, protects us from data-race bugs in the wild.

    Finally, we’ll arrive at Camp, situate ourselves, and contemplate on the relationship between your concurrent code, and SwiftUI APIs.

    Let’s go to our very first stop, Main Actor Meadows. During our tour, I want to collect some nature-inspired color schemes, so I built an app for it. After taking a photo, I can pick how many colors I want, and press the Extract button. The app will pick out complimentary colors from the photo, and show them on screen.

    I can scroll down to see all the color schemes I’ve extracted, and choose my favorite to export.

    For the extraction UI, I made a struct ColorExtractorView. It conforms to SwiftUI's view protocol, which declares @MainActor isolation.

    Swift uses data isolation to understand and verify the safety of all mutable states. Throughout the tour, we’ll encounter many concurrency concepts like that. If you’re new to Swift Concurrency or just need a refresher, watch the session “Embracing Swift Concurrency”. In SwiftUI, View is isolated on the @MainActor, and I conform my struct to View. Therefore, the ColorExtractorView becomes @MainActor isolated. This dotted line indicates inferred isolation, meaning, this annotation is implied at compile time, but it’s not actually part of the code I wrote. The overall type being isolated on the @MainActor means all of its members are implicitly isolated as well.

    This includes the body property that implements the requirement from View, as well as other members I declare, such as this @State variable.

    Closing up on the body of the view, I'm referring to other member properties, such as model’s scheme, or a binding to model’s colorCount. This is allowed by the compiler because the shared @MainActor isolation guarantees that these accesses are safe. This also feels intuitive.

    @MainActor is SwiftUI’s compile-time default. This means most of the time, I can just focus on building my app features, and I don’t have to think much about concurrency. I don’t need to annotate the code for concurrency purposes. It's safe automatically.

    To make some room for more code, I’m just gonna to hide these inferred isolations.

    This compile time default with @MainActor, extends beyond the synchronous code in my view.

    My data model’s types don’t need any @MainActor annotations.

    Because I instantiate the model inside the view’s declaration, Swift will make sure that the model instance is properly isolated.

    This SchemeContentView has a tap gesture that kicks off the work for color extraction. The color extraction function is asynchronous, so I’m using a Task to switch to an async context, in order to call it.

    Because the view body is @MainActor isolated, it makes the closure I gave to this task also run on the main thread, which is really convenient. @MainActor isolation is SwiftUI’s compile time default. It makes writing views convenient, and approachable. But, there’s another very practical reason for it. APIs from AppKit and UIKit, are exclusively @MainActor isolated. SwiftUI seamlessly interoperates with these frameworks. For example, the protocol UIViewRepresentable refines the View protocol. Similar to a struct, this isolates UIViewRepresentable on @MainActor.

    So a type that conforms to UIViewRepresentable is also a View. Therefore, it's @MainActor isolated. UILabel’s initializer requires @MainActor isolation. And that works in my makeUIView, because makeUIView is a member of my @MainActor isolated representable type.

    There’s no need to annotate it with @MainActor. SwiftUI annotates its APIs with @MainActor, because that reflects the default runtime behavior it implements.

    These annotations are downstream of the framework’s intended semantics at runtime. SwiftUI’s concurrency annotations express its runtime semantics. This may seem like a subtle distinction from the compile time conveniences we saw earlier, but it is fundamental. We’ll see another example that reinforces this idea coming right up.

    Allright folks, this next stop is gonna be exciting. Make sure your seat belt is snug, and your electronic devices are secured.

    As you introduce more app features during app development, if the main thread has too much work to do, the app may start to have frame drops or hitches. You can use tasks and structured concurrency to offload your compute from the main thread. Our session, “Elevate an app with Swift Concurrency,” provides a series of practical techniques for improving your app's performance. Make sure you catch that one.

    The focus of this tour is how SwiftUI leverages Swift concurrency, to give your apps better performance.

    In the past, the SwiftUI team has revealed that built-in animations use a background thread to calculate their intermediary states.

    Let’s review that by investigating this circle inside my SchemeContentView.

    As the color extraction job begins and ends, the circle grows larger, and shrinks back down to its original size with animation.

    For that, I’m using a scaleEffect that reacts to the property isLoading.

    Every frame of this animation requires a different scale value between 1 and 1.5.

    Animated values such as this scale involve complex maths. Calculating a lot of these, frame by frame can be expensive. Therefore, SwiftUI performs this calculation on a background thread, so that the main thread has more capacity for other stuff.

    This optimization applies to APIs you implement as well.

    That's right. Sometimes, SwiftUI runs your code off the main thread. But don’t worry, it’s not that complicated. SwiftUI is declarative. Unlike an UIView, the struct that conforms to the View protocol, is not an object that has to occupy a fixed location in memory.

    At runtime, SwiftUI creates a separate representation for the View.

    This representation provides opportunities for many types of optimizations. An important one is to evaluate parts of the view representation on a background thread.

    SwiftUI reserves this technique for occasions where a lot of compute is done on your behalf. For example, most of the time, it involves some high-frequency geometry calculations. The Shape protocol is an example of that.

    The Shape protocol requires a method that returns a path. I made a custom wedge shape to represent an extracted color in my wheel. It implements that path method.

    Each wedge has a distinct orientation. While this wedge shape is animating, the path method I wrote gets calls from a background thread.

    Another kind of custom logic SwiftUI runs on your behalf is a closure argument.

    In the middle of the circle are these blurred texts. To implement that, I’m using a visualEffect on a SwiftUI Text.

    It alters the blur radius between two values as the pulse value flips between true and false. The view modifier visualEffect, takes in a closure for defining effects on the subject view, aka the text. Visual effects can get fancy, and expensive to render. So SwiftUI can choose to call this closure from a background thread.

    So that’s two APIs that could call your code from a background thread. Let's quickly visit a few more.

    The Layout protocol may call its requirement methods off the main thread. And similar to visualEffect, the first argument of onGeometryChange is a closure that may get called from the background thread as well.

    This runtime optimization with a background thread has been part of SwiftUI for a long time. SwiftUI can express this runtime behavior, or semantics, to the compiler, and you, with the Sendable annotation. Here again, SwiftUI’s concurrency annotations express its runtime semantics.

    Running your code on a separate thread frees up the main thread, so that your app is more responsive. And the Sendable keyword is here to remind you about potential data-race conditions when you need to share data from the @MainActor.

    Think of Sendable like a warning sign on a cliffside trail that reads “Danger! Don’t race here!” Hmm, that description is maybe a little too dramatic. In practice, Swift will reliably find any potential race conditions in code, and remind you of them with compiler errors. The best strategy to avoid data-race conditions, is to not share data between concurrent tasks at all.

    When a SwiftUI API requires you to write a sendable function, the framework will provide most of the variables you need as function arguments. Here's a quick example.

    Earlier, there’s a detail in ColorExtactorView that I didn’t show. The color wheel and the slider have the same width, thanks to this EqualWidthVStack type.

    EqualWidthVStack is a custom layout. How it does the layout isn't our focus. The point here is, I’m able to do all these sophisticated calculations with the argument SwiftUI passes in, without touching any external variables.

    But, what if I really need to access some variables external to a sendable function? In SchemeContentView, I need the state pulse in this visualEffect. But, Swift says there’s a potential data-race condition.

    Let’s take out our binoculars, and zoom in on what the compiler error is telling us.

    The pulse variable is short for self.pulse. This is a common scenario when sharing a @MainActor isolated variable in sendable closures.

    Self is a view. It’s isolated on the main actor. This is our starting point. From there, our end goal is to access the pulse variable in a sendable closure. To achieve that, two things must happen. First, the value self must cross the boundary from main actor to the background threads code region.

    In Swift, we refer to this as sending the variable self into the background thread. This requires the type of self to be Sendable.

    Now that self appears in the right place, we want to read its property pulse in this nonisolated region. The compiler will not allow that unless the property pulse is not isolated to any actor.

    Looking at the code again, because self is a View, it’s protected by the @MainActor.

    So the compiler considers it Sendable.

    Because of that, Swift is fine with the fact that this reference to self crosses from its @MainActor isolation into the Sendable closure.

    So really, Swift is warning us about the attempt to access the pulse property. Of course, we know that as a member of the View, pulse is @MainActor isolated.

    So the compiler is telling me, even though I can send self in here, accessing is @MainActor isolated property pulse is unsafe.

    To fix this compile error, I can avoid reading the property through a reference to the View. The visual effect I’m writing, doesn’t need the whole value of this view. It just wants to know if pulse is true or false. I can make a copy of the pulse variable in the closure’s capture list, and refer to the copy instead. This way, I’m no longer sending self into this closure.

    I’m sending a copy of pulse, which is sendable because Bool is a simple value type.

    This copy exists only within the scope of this function, so accessing it here does not cause any data-race problems.

    In that example, we couldn’t access that pulse variable in a sendable closure, because it's protected by a global actor. Another strategy to make this work is to make everything we’re reading nonisolated.

    All right, folks, you’ve made it to Camp. Let’s sit down and talk about organizing your concurrent code.

    Experienced SwiftUI developers might have noticed that most SwiftUI's APIs, such as button’s action callback, are synchronous. To call your concurrent code, you first need to switch to an async context with a Task.

    But why doesn’t Button accept an async closure instead? Synchronous updates are important for a good user experience. It’s extra important if your app has long-running tasks, and people have to wait for the results.

    Before kicking off a long-running task with an async function, it’s important to update your UI to indicate the task is in progress. This update should be synchronous, especially if it needs to trigger some time-sensitive animation.

    Imagine if I ask a language model to help me extract the colors. That extraction process will take a while. So in my app, I'm using withAnimation to synchronously trigger various loading states. When the task is done, I then reverse these loading states, by another synchronous state change.

    SwiftUI’s action callbacks accept synchronous closures, which are necessary to set up UI updates, like my loading states. Async functions, on the other hand, require extra consideration, especially if you’re working with animations. Let's explore that now.

    In my app, I can scroll up to reveal a history of the color schemes from earlier. As each scheme appears on screen, I want its colors to reveal themselves with some animation. The view modifier onScrollVisibilityChange gives me the event when the color scheme appears on screen. As soon as this happens, I’m setting a state variable to true to trigger the animation, which causes each color’s Y offset to update with animation.

    As an UI framework, in order to create buttery smooth interactions every frame, SwiftUI needs to confront the reality that devices demand a certain screen refresh rate.

    That’s some important context when I want my code to react to a continuous gesture like scrolling. Let's put this code on the timeline.

    I’m going to use this green triangle to mark the moment SwiftUI calls onScrollVisibilityChange. And the blue circle marks the moment I trigger my animation with a state mutation.

    With this setup, whether such mutation occurs on the same frame with the gesture callback can make a big difference visually.

    Suppose I want to add some async work prior to my animated mutation. I’ll mark the moment the async work starts with an orange line and await on it. In Swift, awaiting on an async function creates a suspension point.

    A Task accepts an async function as an argument.

    When the compiler sees an await, it splits the async function into two parts.

    After executing part one, the Swift runtime can pause this function and do some other work on the CPU. This can go on for an arbitrary amount of time. Then the runtime resumes on the original async function and execute its second half.

    This process can repeat for each occurrence of await in the function.

    Going back to our timeline, this suspension could mean my task closure doesn’t resume until much later, passing the refresh deadline dictated by the device.

    To the user, that means my animation looks laggy and out of step. So a mutation in an async function may not help achieving your goal.

    SwiftUI provides synchronous callbacks by default. This helps avoid unintentional suspension of async code. Updating UI within synchronous action closures is easy to do correctly. You always have the option to use a Task to opt in into an asynchronous context.

    Time-sensitive logic like animation requires SwiftUI’s input and output to be synchronous. Synchronous mutations of observable properties, and synchronous callbacks, are the most natural types of interaction with the framework. A great user experience doesn’t have to involve a lot of custom concurrent logic. Synchronous code is a great starting point and endpoint for lots of apps.

    On the other hand, if your app does a lot of concurrent work, try and find the boundaries between your UI code and non-UI code. It’s best to separate the logic for async work from your view logic.

    You can use a piece of state as a bridge. The state decouples the UI code from the async code.

    It can initiate the async tasks.

    As some async work finishes up, perform a synchronous mutation on the state, so that your UI can update as reactions to this change. This way, the UI logic is mostly synchronous.

    As a bonus, you’ll find it easier to write tests for your async code, because it’s now independent from the UI logic.

    Your view can still use a Task to switch to an async context.

    But try to keep the code in this async context simple. It’s there to inform the model about a UI event. Finding the boundaries between UI code that requires a lot of time-sensitive changes, and long-running async logic is a great way to improve the structure of an app. It can help you keep the views synchronous and responsive. It’s also important to organize the non-UI code well. You’ll have greater freedom to do so with the tips I showed you in this basecamp.

    Swift 6.2 comes with a great default actor isolation setting. If you have an existing app, try it out. You’ll be able to delete most of your @MainActor annotations.

    Mutex is an important tool for making a class sendable. Check out its official documentation to learn how.

    Challenge yourself to write some unit tests for the async code in your app. See if you can do it without importing SwiftUI.

    Alright, folks. So that’s how SwiftUI leverages Swift concurrency to help you build fast and data-race free apps. As we wrap up this tour, I hope you’ve gained a solid mental model for concurrency in SwiftUI.

    Thanks for touring, I wish you many epic adventures.

    • 2:45 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 5:55 - AppKit and UIKit require @MainActor: an example

      // AppKit and UIKit require @MainActor
      // Example: UIViewRepresentable
      
      struct FancyUILabel: UIViewRepresentable {
          func makeUIView(context: Context) -> UILabel {
              let label = UILabel()
              // customize the label...
              return label
          }
      }
    • 6:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorScheme: Identifiable, Hashable {
          var id = UUID()
          let imageName: String
          var colors: [Color]
      }
      
      @Observable
      final class ColorExtractor {
          var imageName: String
          var scheme: ColorScheme?
          var isExtracting: Bool = false
          var colorCount: Float = 5
      
          func extractColorScheme() async {}
      }
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractorModel()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack(spacing: 30) {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 8:26 - Animated circle, part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 13:10 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 13:47 - Part of color scheme view

      // Part of color scheme view
      
      struct SchemeContentView: View {
          let isLoading: Bool
          @State private var pulse: Bool = false
      
          var body: some View {
              ZStack {
                  // Color wheel …
      
                  Circle()
                      .scaleEffect(isLoading ? 1.5 : 1)
      
                  VStack {
                      Text(isLoading ? "Please wait" : "Extract")
      
                      if !isLoading {
                          Text("^[\(extractCount) color](inflect: true)")
                      }
                  }
                  .visualEffect { [pulse] content, _ in
                      content
                          .blur(radius: pulse ? 2 : 0)
                  }
                  .onChange(of: isLoading) { _, newValue in
                      withAnimation(newValue ? kPulseAnimation : nil) {
                          pulse = newValue
                      }
                  }
              }
          }
      }
    • 17:42 - UI for extracting colors

      // UI for extracting colors
      
      struct ColorExtractorView: View {
          @State private var model = ColorExtractor()
      
          var body: some View {
                  ImageView(
                      imageName: model.imageName,
                      isLoading: model.isExtracting
                  )
                  EqualWidthVStack {
                      ColorSchemeView(
                          isLoading: model.isExtracting,
                          colorScheme: model.scheme,
                          extractCount: Int(model.colorCount)
                      )
                      .onTapGesture {
                          guard !model.isExtracting else { return }
                          withAnimation { model.isExtracting = true }
                          Task {
                              await model.extractColorScheme()
                              withAnimation { model.isExtracting = false }
                          }
                      }
                      Slider(value: $model.colorCount, in: 3...10, step: 1)
                          .disabled(model.isExtracting)
                  }
              }
          }
      }
    • 18:55 - Animate colors as they appear by scrolling

      // Animate colors as they appear by scrolling
      
      struct SchemeHistoryItemView: View {
          let scheme: ColorScheme
          @State private var isShown: Bool = false
      
          var body: some View {
              HStack(spacing: 0) {
                  ForEach(scheme.colors) { color in
                      color
                          .offset(x: 0, y: isShown ? 0 : 60)
                  }
              }
              .onScrollVisibilityChange(threshold: 0.9) {
                  guard !isShown else { return }
                  withAnimation {
                      isShown = $0
                  }
              }
          }
      }

Developer Footer

  • 视频
  • WWDC25
  • 探索 SwiftUI 中的并发机制
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载 (英文)
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    获取 Apple Developer App。
    版权所有 © 2025 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则