You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI 6中异步扫描文件夹?Swift 6编译错误求助

Swift 6 文件夹异步扫描编译错误修复方案

问题根源

Swift 6 启用了严格并发检查(Strict Concurrency Checking),原代码存在以下不符合新并发规则的问题:

  • FileManager.enumerator(at:) 返回的 FileEnumerator 并非 Sendable 类型,在 @MainActor 隔离的异步方法中使用会触发并发安全警告/错误。
  • 同步目录枚举逻辑阻塞 MainActor,违背异步编程的非阻塞原则。
  • Task.isCancelled 静态属性在 Swift 6 中推荐改为通过当前任务实例检查状态,避免歧义。

修复后的完整代码

import SwiftUI

@MainActor
final class FileScannerViewModel: ObservableObject {
    @Published var scannedCount: Int = 0
    @Published var isScanning: Bool = false
    private var scanTask: Task<Void, Error>?
    
    func startScan() {
        guard !isScanning else { return }
        scannedCount = 0
        isScanning = true
        
        // 创建后台任务脱离MainActor,避免阻塞主线程
        scanTask = Task.detached { [weak self] in
            guard let self = self else { return }
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            do {
                try await self.scanDirectory(at: documentsURL)
                if !Task.isCancelled {
                    await MainActor.run {
                        self.isScanning = false
                    }
                }
            } catch is CancellationError {
                await MainActor.run {
                    self.isScanning = false
                }
            } catch {
                print("扫描出错:\(error)")
                await MainActor.run {
                    self.isScanning = false
                }
            }
        }
    }
    
    func cancelScan() {
        scanTask?.cancel()
        scanTask = nil
        isScanning = false
    }
    
    // 标记为非隔离方法,在后台执行扫描逻辑
    nonisolated private func scanDirectory(at url: URL) async throws {
        let fileManager = FileManager.default
        guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: nil) else { return }
        
        for case let fileURL as URL in enumerator {
            // 检查当前任务是否被取消,触发取消时抛出错误
            try Task.checkCancellation()
            
            // 回到MainActor更新UI计数
            await MainActor.run {
                self.scannedCount += 1
            }
            
            // 让出线程资源,避免持续阻塞
            await Task.yield()
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = FileScannerViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("已扫描文件数:\(viewModel.scannedCount)")
            if viewModel.isScanning {
                ProgressView()
                    .progressViewStyle(.circular)
            }
            HStack {
                Button("开始扫描") {
                    viewModel.startScan()
                }
                .disabled(viewModel.isScanning)
                
                Button("取消") {
                    viewModel.cancelScan()
                }
                .disabled(!viewModel.isScanning)
            }
        }
        .padding()
    }
}

关键修复点说明

  • 后台任务执行扫描:用 Task.detached 创建脱离MainActor的任务,避免同步枚举阻塞主线程,同时解决FileEnumerator的并发安全问题。
  • 非隔离扫描方法:给scanDirectory添加nonisolated标记,明确该方法不在MainActor隔离环境中运行,符合Swift 6并发规则。
  • 规范取消处理:使用try Task.checkCancellation()替代静态Task.isCancelled,取消时抛出CancellationError,便于统一捕获处理。
  • 完善错误与状态重置:捕获取消错误和扫描异常,确保无论何种情况都能正确重置UI状态。

内容的提问来源于stack exchange,提问作者GT4485

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 13:07:43