Swift 并发机制优化 App 用户体验

Viewed 0

通过更新一个现有的示例 App,我们将向你介绍如何通过 Swift 并发机制来优化 App 的用户体验。我们将从一个主 Actor App 入手,然后根据需要逐步引入异步代码。我们将使用任务来优化主 Actor 上运行的代码,并探索如何通过将工作转移到后台来实现代码并行运行。我们将探讨数据争用安全机制提供的功能,并讲解如何解读和修复数据争用安全错误。最后,我们将展示如何在 App 情境中充分利用结构化并发机制。

首先,我们异步从照片库加载所选照片。以下是 loadPhoto 函数的实现:

func loadPhoto(_ item: SelectedPhoto) async {
    var data: Data? = try? await item.loadTransferable(type: Data.self)

    if let cachedData = getCachedData(for: item.id) { data = cachedData }

    guard let data else { return }
    processedPhotos[item.id] = Image(data: data)

    cacheData(item.id, data)
}

在 SwiftUI View 中,我们使用 .task 修饰符在视图出现时调用这个异步函数:

StickerPlaceholder()
      .task {
          await viewModel.loadPhoto(selectedPhoto)
      }

接下来,我们同步从照片中提取贴纸和颜色。更新 loadPhoto 函数以使用 PhotoProcessor

func loadPhoto(_ item: SelectedPhoto) async {
      var data: Data? = try? await item.loadTransferable(type: Data.self)

      if let cachedData = getCachedData(for: item.id) { data = cachedData }

      guard let data else { return }
      processedPhotos[item.id] = PhotoProcessor().process(data: data)

      cacheData(item.id, data)
}

存储处理后的照片在一个字典中:

var processedPhotos = [SelectedPhoto.ID: ProcessedPhoto]()

在轮播视图中显示带渐变背景的贴纸,代码如下:

import SwiftUI
import PhotosUI

struct StickerCarousel: View {
      @State var viewModel: StickerViewModel
      @State private var sheetPresented: Bool = false

      var body: some View {
          ScrollView(.horizontal) {
              LazyHStack(spacing: 16) {
                  ForEach(viewModel.selection) { selectedPhoto in
                      VStack {
                          if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] {
                              GradientSticker(processedPhoto: processedPhoto)
                          } else if viewModel.invalidPhotos.contains(selectedPhoto.id) {
                              InvalidStickerPlaceholder()
                          } else {
                              StickerPlaceholder()
                                  .task {
                                      await viewModel.loadPhoto(selectedPhoto)
                                  }
                          }
                      }
                      .containerRelativeFrame(.horizontal)
                  }
              }
          }
          .configureCarousel(
              viewModel,
              sheetPresented: $sheetPresented
          )
          .sheet(isPresented: $sheetPresented) {
              StickerGrid(viewModel: viewModel)
          }
      }
}

为了允许照片处理在后台线程运行,我们定义 PhotoProcessornonisolated 并使用 @concurrent

nonisolated struct PhotoProcessor {

      let colorExtractor = ColorExtractor()

      @concurrent
      func process(data: Data) async -> ProcessedPhoto? {
          let sticker = extractSticker(from: data)
          let colors = extractColors(from: data)

          guard let sticker = sticker, let colors = colors else { return nil }

          return ProcessedPhoto(sticker: sticker, colorScheme: colors)
      }

      private func extractColors(from data: Data) -> PhotoColorScheme? {
          // ...
      }

      private func extractSticker(from data: Data) -> Image? {
          // ...
      }
}

然后,在 loadPhoto 函数中异步调用 process 方法:

func loadPhoto(_ item: SelectedPhoto) async {
      var data: Data? = try? await item.loadTransferable(type: Data.self)

      if let cachedData = getCachedData(for: item.id) { data = cachedData }

      guard let data else { return }
      processedPhotos[item.id] = await PhotoProcessor().process(data: data)

      cacheData(item.id, data)
}

为了并行执行贴纸和颜色提取,我们使用 async let

nonisolated struct PhotoProcessor {

      @concurrent
      func process(data: Data) async -> ProcessedPhoto? {
          async let sticker = extractSticker(from: data)
          async let colors = extractColors(from: data)

          guard let sticker = await sticker, let colors = await colors else { return nil }

          return ProcessedPhoto(sticker: sticker, colorScheme: colors)
      }

      private func extractColors(from data: Data) -> PhotoColorScheme? {
          let colorExtractor = ColorExtractor()
          return colorExtractor.extractColors(from: data)
      }

      private func extractSticker(from data: Data) -> Image? {
          // ...
      }
}

在轮播中,我们对每个贴纸应用视觉效果:

import SwiftUI
import PhotosUI

struct StickerCarousel: View {
      @State var viewModel: StickerViewModel
      @State private var sheetPresented: Bool = false

      var body: some View {
          ScrollView(.horizontal) {
              LazyHStack(spacing: 16) {
                  ForEach(viewModel.selection) { selectedPhoto in
                      VStack {
                          if let processedPhoto = viewModel.processedPhotos[selectedPhoto.id] {
                              GradientSticker(processedPhoto: processedPhoto)
                          } else if viewModel.invalidPhotos.contains(selectedPhoto.id) {
                              InvalidStickerPlaceholder()
                          } else {
                              StickerPlaceholder()
                                  .task {
                                      await viewModel.loadPhoto(selectedPhoto)
                                  }
                          }
                      }
                      .containerRelativeFrame(.horizontal)
                      .visualEffect { [selection = viewModel.selection] content, proxy in
                          let frame = proxy.frame(in: .scrollView(axis: .horizontal))
                          let distance = min(0, frame.minX)
                          let isLast = selectedPhoto.id == selection.last?.id

                          return content
                              .hueRotation(.degrees(frame.origin.x / 10))
                              .scaleEffect(1 + distance / 700)
                              .offset(x: isLast ? 0 : -distance / 1.25)
                              .brightness(-distance / 400)
                              .blur(radius: isLast ? 0 : -distance / 50)
                              .opacity(isLast ? 1.0 : min(1.0, 1.0 - (-distance / 400)))
                      }
                  }
              }
          }
          .configureCarousel(
              viewModel,
              sheetPresented: $sheetPresented
          )
          .sheet(isPresented: $sheetPresented) {
              StickerGrid(viewModel: viewModel)
          }
      }
}

当从并发任务访问引用类型时,使用 @concurrent

Task { @concurrent in
      await viewModel.loadPhoto(selectedPhoto)
}

使用任务组一次处理所有照片:

func processAllPhotos() async {
      await withTaskGroup { group in
          for item in selection {
              guard processedPhotos[item.id] == nil else { continue }
              group.addTask {
                  let data = await self.getData(for: item)
                  let photo = await PhotoProcessor().process(data: data)
                  return photo.map { ProcessedPhotoResult(id: item.id, processedPhoto: $0) }
              }
          }

          for await result in group {
              if let result {
                  processedPhotos[result.id] = result.processedPhoto
              }
          }
      }
}

在贴纸网格视图中,启动照片处理并配置分享链接:

import SwiftUI

struct StickerGrid: View {
      let viewModel: StickerViewModel
      @State private var finishedLoading: Bool = false

      var body: some View {
          NavigationStack {
              VStack {
                  if finishedLoading {
                      GridContent(viewModel: viewModel)
                  } else {
                      ProgressView()
                          .frame(maxWidth: .infinity, maxHeight: .infinity)
                          .padding()
                  }
              }
              .task {
                  await viewModel.processAllPhotos()
                  finishedLoading = true
              }
              .toolbar {
                  ToolbarItem(placement: .topBarTrailing) {
                      if finishedLoading {
                          ShareLink("Share", items: viewModel.selection.compactMap {
                              viewModel.processedPhotos[$0.id]?.sticker
                          }) { sticker in
                              SharePreview(
                                  "Sticker Preview",
                                  image: sticker,
                                  icon: Image(systemName: "photo")
                              )
                          }
                      }
                  }
              }
              .configureStickerGrid()
          }
      }
}
0 Answers