にクロージャーを渡して、切り離されたタスクのステータスを更新しようとしたときに問題が発生しましたMainActor.run
。
この問題を説明するために、フォルダーとそのサブディレクトリ内のファイル数をカウントする関数を考えてみましょう。Task.detached
メインスレッドをブロックしたくないので、クロージャーで実行されます。10,000 番目のファイルごとに、クロージャを に渡すことによって、Published
プロパティを更新します。fileCount
MainThread.run
ただし、UI の更新に失敗し、回転するビーチ ボールが表示されることさえあります。これを止める唯一の方法は、await Task.sleep(1_000_000_000)
への呼び出しの前に挿入することMainThread.run
です。コードは次のとおりです。
final class NewFileCounter: ObservableObject {
@Published var fileCount = 0
func findImagesInFolder(_ folderURL: URL) {
let fileManager = FileManager.default
Task.detached {
var foundFileCount = 0
let options = FileManager.DirectoryEnumerationOptions(arrayLiteral: [.skipsHiddenFiles, .skipsPackageDescendants])
if let enumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: [], options: options) {
while let _ = enumerator.nextObject() as? URL {
foundFileCount += 1
if foundFileCount % 10_000 == 0 {
let fileCount = foundFileCount
await Task.sleep(1_000_000_000) // <-- Only works with this in...comment out to see failure
await MainActor.run { self.fileCount = fileCount }
}
}
let fileCount = foundFileCount
await MainActor.run { self.fileCount = fileCount }
}
}
}
}
これを達成する古い方法に戻ると、コードは機能します。
final class OldFileCounter: ObservableObject {
@Published var fileCount = 0
func findImagesInFolder(_ folderURL: URL) {
let fileManager = FileManager.default
DispatchQueue.global(qos: .userInitiated).async {
let options = FileManager.DirectoryEnumerationOptions(arrayLiteral: [.skipsHiddenFiles, .skipsPackageDescendants])
var foundFileCount = 0
if let enumerator = fileManager.enumerator(at: folderURL, includingPropertiesForKeys: [], options: options) {
while let _ = enumerator.nextObject() as? URL {
foundFileCount += 1
if foundFileCount % 10_000 == 0 {
let fileCount = foundFileCount
DispatchQueue.main.async { self.fileCount = fileCount }
}
}
let fileCount = foundFileCount
DispatchQueue.main.async { self.fileCount = fileCount }
}
}
}
}
私は何を間違っていますか?
ところで、このコードを試してみたい場合は、ここにテスト ハーネスがあります。たくさんのファイルが含まれるフォルダーとそのサブフォルダーを必ず選択してください。
import SwiftUI
@main
struct TestFileCounterApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State private var showPickerOld = false
@StateObject private var fileListerOld = OldFileCounter()
@State private var showPickerNew = false
@StateObject private var fileListerNew = NewFileCounter()
var body: some View {
VStack {
Button("Select folder to count files using DispatchQueue...") { showPickerOld = true }
Text("\(fileListerOld.fileCount)").foregroundColor(.green)
.fileImporter(isPresented: $showPickerOld, allowedContentTypes: [.folder], onCompletion: processOldSelectedURL )
Divider()
Button("Select folder to count files using Swift 5.5 concurrency...") { showPickerNew = true }
Text("\(fileListerNew.fileCount)").foregroundColor(.green)
.fileImporter(isPresented: $showPickerNew, allowedContentTypes: [.folder], onCompletion: processNewSelectedURL )
}
.frame(width: 400, height: 130)
}
private func processOldSelectedURL(_ result: Result<URL, Error>) {
switch result {
case .success(let url): fileListerOld.findImagesInFolder(url)
case .failure: return
}
}
private func processNewSelectedURL(_ result: Result<URL, Error>) {
switch result {
case .success(let url): fileListerNew.findImagesInFolder(url)
case .failure: return
}
}
}