Swift macOS:如何从其他类/线程更新NSTextView?
Hey there! Let's tackle this UI blocking issue you're facing—it's a super common pitfall when working with long-running tasks in macOS apps, especially if you're transitioning from procedural languages. Let's break down what's going wrong and fix it step by step.
The Root of the Problem
Your processImages() function runs on the main thread (the same thread that handles UI updates), which blocks all GUI interactions until the task finishes. Even if you moved it to a background thread, your writeToLog() function is directly modifying the NSTextView from whatever thread calls it—and AppKit strictly requires all UI updates to happen on the main thread. Plus, your current way of fetching the ViewController via NSApplication.shared.keyWindow is risky and can lead to unexpected behavior across threads.
Step-by-Step Fixes
Let's adjust your code to safely run the heavy task in the background while updating the log in real time on the main thread.
1. Move the Heavy Task to a Background Queue
Update your button action to offload processImages() to a background GCD queue (this is the standard way to handle async tasks in Swift):
@IBAction func ExecuteButtonPressed(_ sender: NSButtonCell){ Utilities.writeToLog(lineOfText: "Copy process starting\n", logView: logView) // Run the heavy task on a background queue DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let inputPath = self.sourceFolder.stringValue let outputPath = self.targetFolder.stringValue // Execute the image copy process self.processImages(inputPath, outputPath) { logMessage in // Pass log updates back to the main thread DispatchQueue.main.async { Utilities.writeToLog(lineOfText: logMessage, logView: self.logView) } } // Final log update once the task finishes DispatchQueue.main.async { Utilities.writeToLog(lineOfText: "Copy process ended\n", logView: self.logView) } } }
2. Refactor writeToLog for Thread Safety
Rewrite your Utilities class to ensure all UI updates happen on the main thread, and avoid risky force-unwraps:
class Utilities { // Follow Swift naming conventions: uppercase class names static func writeToLog(lineOfText: String, logView: NSTextView){ // Force all UI updates to run on the main thread DispatchQueue.main.async { let calendar = Calendar.current let timeComponents = calendar.dateComponents([.hour, .minute, .second], from: Date()) // Safely handle optional time values to avoid crashes guard let hour = timeComponents.hour, let minute = timeComponents.minute, let second = timeComponents.second else { let fallbackLine = "Unknown time - \(lineOfText)" logView.textStorage?.append(NSAttributedString(string: fallbackLine)) logView.scrollToEndOfDocument(nil) return } let timestampedLine = "\(hour):\(minute):\(second) - \(lineOfText)" logView.textStorage?.append(NSAttributedString(string: timestampedLine)) logView.scrollToEndOfDocument(nil) } } }
3. Add a Callback to processImages for Real-Time Logs
Modify your processImages function to accept a closure that sends log updates back to the view controller. This keeps your image copy logic decoupled from the UI:
func processImages(_ inputFolder: String, _ outputFolder: String, updateLog: (String) -> Void) { // Example workflow (replace with your actual copy logic) updateLog("Scanning input folder: \(inputFolder)\n") // Iterate over image files if let imageFiles = try? FileManager.default.contentsOfDirectory(atPath: inputFolder) { for file in imageFiles where file.hasSuffix(".png") || file.hasSuffix(".jpg") { let sourcePath = URL(fileURLWithPath: inputFolder).appendingPathComponent(file).path let targetPath = URL(fileURLWithPath: outputFolder).appendingPathComponent(file).path do { try FileManager.default.copyItem(atPath: sourcePath, toPath: targetPath) updateLog("Copied: \(file)\n") } catch { updateLog("Failed to copy \(file): \(error.localizedDescription)\n") } } } else { updateLog("Error: Could not access input folder\n") } }
Key Improvements Explained
- Background Thread Safety:
DispatchQueue.global().asyncruns the heavy copy task off the main thread, so your GUI stays responsive. - Main Thread UI Updates: All calls to modify
logVieware wrapped inDispatchQueue.main.async, which complies with AppKit's rules for UI manipulation. - Decoupled Logic: The closure callback in
processImageskeeps your image handling code separate from the UI layer, making it easier to maintain. - Safer Optional Handling: We replaced force-unwraps with
guardstatements to prevent crashes if time components can't be fetched.
内容的提问来源于stack exchange,提问作者Gazing South

