0

I'm writing a Mac (Swift) application on Xcode which gets data from a command and asynchronously changes the stringValue of some text in the window. I already figured out the asynchronous part from here, but I can't seem to figure out how to actually change the text, since Xcode seems to require it to be in viewDidAppear. Unfortunately I can't put the function which runs the command in viewDidAppear since it is called by another file and needs to be a public func (as far as I know).

Here are a couple of methods I tried:

1. Call a function inside viewDidAppear which changes the text:

self.viewDidAppear().printText("testing!") // this part is where the "New Output" line is on the attached link above

...

override func viewDidAppear() {
    func printText(_ string: String) {
        textLabel.stringValue = string
    }
}

Result: Value of tuple type '()' has no member 'printText' (on the first line)

2. Change an already-declared variable to the current message, then use Notification Center to tell viewDidAppear to change the text.

var textToPrint = "random text" // directly inside the class
let nc = NotificationCenter.default // directly inside the class

...

self.textToPrint = "testing!" // in place of the "New Output" line in the link above
self.nc.post(name: Notification.Name("printText"), object: nil) // in place of the "New Output" line in the link above

...

@objc func printText2() { // directly inside the class
    textLabel.stringValue = textToPrint // directly inside the class
} // directly inside the class

...

override func viewDidAppear() {
        nc.addObserver(self, selector: #selector(printText2), name: Notification.Name("printText"), object: nil)
}

For this one, I had to put printText2 outside of viewDidAppear because apparently selectors (for Notification Center) only work if you do that.

Result: NSControl.stringValue must be used from main thread only (on textLabel.stringValue line). Also, the text never changes.

So I need to either somehow change the label's text directly from the asynchronous function, or to have viewDidAppear do it (also transmitting the new message).

...................................................................

Extra project code requested by Upholder of Truth

import Cocoa

class VC_image: NSViewController, NSWindowDelegate {

    @IBOutlet var textLabel: NSTextField!

    public func processImage(_ path: String) { // this function is called by another file
        previewImage()
    }

    public func previewImage() {
        if let path = Bundle.main.path(forResource: "bashscript", ofType: "sh") {
            let task3 = Process()

            task3.launchPath = "/bin/sh"
            task3.arguments = [path]

            let pipe3 = Pipe()
            task3.standardOutput = pipe3
            let outHandle = pipe3.fileHandleForReading

            outHandle.readabilityHandler = { pipe3 in
                if let line = String(data: pipe3.availableData, encoding: String.Encoding.utf8) {
                    // Update your view with the new text here
                    let messageToPrint = line.components(separatedBy: " ")
                    if (messageToPrint.count == 6) {
                        DispatchQueue.main.async {
                            self.textLabel.stringValue = "testing!"
                        }
                    }
                } else {
                    print("Error decoding data: \(pipe3.availableData)")
                }
            }

            task3.launch()
        }
    }
}
htmlcat
  • 336
  • 3
  • 10
  • 1
    The problem here is not updating the label outside of viewDidAppear because you can absolutely do that. The problem is that your asynchronous process is being performed on a background thread and you can only update the UI on the main thread. You need to look into using something like `DispatchQueue.main.async` to put the update back onto the main thread. – Upholder Of Truth Jun 11 '20 at 21:44
  • I see. I'm still kind of an amateur w/ Swift, so how would I do that? – htmlcat Jun 11 '20 at 22:07
  • I tried `DispatchQueue.main.async {self.textLabel.stringValue = "testing!"}` but it just gave me `Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value` (basically the exact same thing as without the `DispatchQueue` part). – htmlcat Jun 11 '20 at 23:03
  • If that happens then the textLabel property itself is nil when you go to update it. It's difficult to say why that might be without seeing the full project. – Upholder Of Truth Jun 12 '20 at 09:51
  • Okay, I'll add more of it to the main question. I just tried to call the function `previewImage()` from `viewDidAppear()` instead of from `processImage()` (which is in turn called by the other file) and it actually worked. So that might have something to do w/ why it's not working... – htmlcat Jun 12 '20 at 19:05
  • The call causing the nil-unwrapping-Optional area is probably happening before the view is fully loaded in the reference to textLabel. You don't see error in viewDidAppear because that happens after. Try changing textLabel declaration to optional (use ? instead of ! in the @IBOutlet declaration) and use self.textLabel?.stringValue = (or, if let label=self.textLabel { label.stringValue= ...}. Read up on optionals & the 'guard' keyword, they're helpful! Still need to follow above advice on DispatchQueue.main and also make sure notifications are *posted* on the main thread. – Corbell Jun 12 '20 at 19:51
  • The `processImage` function is called by an `@IBAction` in the AppDelegate (called by a button pressed in the menubar), after `viewDidAppear` loads. I tried your suggestion to use "?" which prevents the error, but it still doesn't successfully change the text. – htmlcat Jun 12 '20 at 21:33
  • So you have an `@IBAction` in your AppDelegate which is not a good thing to do. Is the VC_Image view controller visible when this AppDelegate function is called? – Upholder Of Truth Jun 13 '20 at 08:55
  • Sorry for responding so late, but our time zones are quite different (I live in the U.S.). When I call the function from the AppDelegate, I use `VC_image().processImage(path)`, so I guess that means that the view controller must be visible. Why is putting the `@IBAction` in the AppDelegate not a good thing to do, and what do you suggest doing instead? – htmlcat Jun 13 '20 at 18:54
  • Upholder of Truth, did you see my message? I'm still not sure how to solve the problem... – htmlcat Jun 15 '20 at 19:41
  • I researched some and found [this] (https://forums.developer.apple.com/thread/118193) forum, so I tried to implement what the message from "Jun 20, 2019 8:06 AM" said to do (to add some code in the main VC and then link the menu-item to the first responder). That method works, but the problem is that my main VC is a split-VC, so the sub-VC (`VC_image`) is the one which has the `textLabel` and not the main VC. So at this point, I guess the question is "how do I set the text of a label from outside of its ViewController?" – htmlcat Jun 15 '20 at 23:56

0 Answers0