Complete Concurrency check enabled and how to resolve warnings

下面是fileImporter的实现...执行在.task()ContentView中花费了太多的时间和内存,因为代码正在主线程上运行.问题是如何将它从主线程分离到后台线程,并在操作后安全地返回主线程.

ContentView上使用MainActor来确保我们没有任何数据竞争(在Xcode中完成的构建设置下的并发判断),并在.task(id: selectedFile)内调用importData().importData()extension ContentView

importData()调用readMetaData,然后循环导入每个item

@MainActor
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext

    @State private var disable: Bool = false
    @State private var value: Double = 0
    @State var progress = ProgressItem()
    @State private var importData: Bool = false
    @State private var selectedFile: URL? = nil

    var body: some View {
        VStack {
            Button {
                disable = true
                importData = true
            } label: {
                Text("Press")
            }
            .task(id: selectedFile) {
                guard importData else {return}
                if let selectedFile = selectedFile {
                    let data = try! String(contentsOf: selectedFile, encoding: .utf8)
                    await importData(data: data)
                    importData = false
                }
            }
            .fileImporter(isPresented: $importData, allowedContentTypes: [UTType.plainText], allowsMultipleSelection: false) { result in
                do {
                    guard let selectedFile: URL = try result.get().first else { return }
                    self.selectedFile = selectedFile
                    importData = true
                    //print(selectedFile)
                    progress.message = ""
                    progress.progress = 0.0
                } catch {
                    print(error.localizedDescription)
                }
            }
            Text("\(value)")
            
        }
    }
}

extension ContentView {
    
    func fetchValue() async -> Double {
        //long running task
        try? await Task.sleep(nanoseconds: 60)
        return 2.0
    }
    
    func importData(data: String) async {
        let actor = DataHandler(modelContainer: modelContext.container)
        // read metadata from file and identify number of items
        await actor.readMetadata(data: data)
        
        //loop through number of items from metadata and import each item
        let items = await actor.getItems()
        
        for item in items {
            await actor.importItemsFrom(data: data)
        }
        
        print("... in importFile...")
    }
}

@ModelActor
actor DataHandler {
    private var metadata = [String]()
    private var nItems: Int = 0
    
    func insert<T: PersistentModel>(_ data: T) {
        modelContext.insert(data)
        try? modelContext.save()
    }

    func save() {
        try? modelContext.save()
    }
    
    func getNumItems() -> Int {
        return nItems
    }
    
    func getItems() -> [String] {
        return metadata
    }
    
    func readMetadata(data: String) {
        //reads metadata and saves metadata locally in metadata
        //increment nItems

    }
    
    func importItemsFrom(data: String) {
        // reads lines from file and persists to database using insert and save
    }
}

推荐答案

几件事:

  1. 把这个从主线上取下来.

    ModelActor中有一个bug,正如Swift论坛SwiftData does not work on a background Task even inside a custom ModelActor中所讨论的.正如他们在那里讨论的,我发现你的DataHandler将运行在主线程上,即使它是自己的演员.(!!!)虽然这是该论坛上的一个老讨论,但我仍然经历了Xcode 15.3和Swift 5.10的行为.

    正如在另一个线程中所讨论的,你可以手动分离任务(但之后你可能会添加取消处理程序):

    func importData(data: String) async throws {
        let container = modelContext.container
    
        let task = Task.detached {
            let modelActor = DataHandler(modelContainer: container)
    
            …
        }
    
        try await withTaskCancellationHandler {
            try await task.value
        } onCancel: {
            task.cancel()
        }
    }
    

    更简单的是,你可以使它不孤立:

    nonisolated func importData(data: String, into container: ModelContainer) async throws {
        let actor = DataHandler(modelContainer: container)
    
        …
    }
    

    注意,我正在通过ModelContainer.因为modelContext不是Sendable.这显然需要在呼叫点进行更新.

    try await importData(data: …, into: modelContext.container)
    

    还有其他方法可以把它从主线上go 掉,但这是基本的 idea .确保,尽管表面上使用一个演员,它确实是运行在后台线程.您可以在代码中添加断点,并查看它使用的线程.

  2. 在我的实验中,修复第一个点解决了一个重要的性能问题,使速度提高了3倍.它还消除了我看到的从Xcode调试器和从Instruments运行它之间的差异.

  3. 您还没有共享完整的MRE,但共享了一个函数insert<T: PersistentModel>(),该函数表明您在每次插入后都要保存.您可以考虑使用transaction或以其他方式删除您的插件.在我的例子中(10,000次插入,每1,000次只保存一次),这使它快了19倍.

  4. 小意见,但我不会打String(contentsOf:encoding:)从主要演员.如果您的文件很小,这可能不是一个问题,但如果您在主要参与者上这样做,那么大的文件可能会导致UI中出现明显的故障.

    就个人而言,我甚至可能避免将整个文件一次加载到内存中.例如,这是一种异步读取单个行的非阻塞方式,从而减少了内存占用:

    @ModelActor
    actor DataHandler {
        func importItems(from fileURL: URL) async throws {
            var count = 0
    
            for try await line in fileURL.lines {
                count += 1
                if count % 1000 == 0 {
                    print(count)
                    try modelContext.save()
                }
                let item = Item(…)
                modelContext.insert(item)
            }
    
            try modelContext.save()
        }
    }
    

    您没有共享您的文件格式,所以我甚至不知道这是否适用于您的场景,但请考虑是否可以在使用文件时处理文件,而不是一次将其全部加载到内存中(特别是如果文件可能很大).

Swift相关问答推荐

SwiftUI轨迹绘制怪异

SwiftUI同心圆,通过逐渐出现来显示进度

从任务中打印

Observable.create 捕获行为

Swift UI中视图上的值未更新

如何制作一个在 SceneKit 中投射阴影的透明物体?

在 Swift 的异步上下文中,如何指示我想要函数的同步版本?

如何在不提交到应用store 的情况下在本地运行我的应用

如何制作具有 2 个视图的 3D 旋转动画?

XCUITest 在 TearDown 期间随机失败-无法终止 com.bundle.id

如何在 SwiftUI 中获取视图的位置

有什么方法可以快速为泛型参数分配默认值?

在 Select 器上显示alert Select SwiftUI

Xcode 6 中的嵌入式内容包含 Swift 代码构建设置有什么作用?

如何实现 Swift 的 Comparable 协议?

Swift 自定义字体 Xcode

Swift 中惰性 var 的优势是什么

如何在 Swift 中遍历 struct 属性?

在 Swift 中为 UIPickerView 设置默认值?

UISearchbar TextField 的高度可以修改吗?