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

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().async runs the heavy copy task off the main thread, so your GUI stays responsive.
  • Main Thread UI Updates: All calls to modify logView are wrapped in DispatchQueue.main.async, which complies with AppKit's rules for UI manipulation.
  • Decoupled Logic: The closure callback in processImages keeps your image handling code separate from the UI layer, making it easier to maintain.
  • Safer Optional Handling: We replaced force-unwraps with guard statements to prevent crashes if time components can't be fetched.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:19:01