Search code examples
swiftxcodensviewcontroller

I get "Unexpectedly found nil while implicitly unwrapping an Optional value" when trying to change the value of an NSTextField


I have prior experience in sequential programming in C, but it was in the mid 80's. I am new to OOP, Swift and multithread coding in general. I am writing a small program to better understand all 3. I was able to build a functional program that starts two threads that each count to 200 and then reset to 1 and restart counting in an endless loop. The value of each counter is printed to the console and I have a Start and stop button for each thread that allow me to control them separately. Everything works fine although I would admit that my code is far from perfect (I don't fully respect encapsulation, I have a few global variables that should be made local etc... My main problem is trying to output each thread counter value to a label instead of printing them to the console. When I try to change the content of any of my labels, I get "Unexpectedly found nil while implicitly unwrapping an Optional value"

When I use a similar line of code inside of a pushbutton function it works perfectly.

This is the content of my ViewController.swift file:

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        //Call Async Task
        startProgram()
    }

    override var representedObject: Any? {
        didSet {
           // Update the view, if already loaded.
        }
    }

    @IBOutlet var threadAValueLabel: NSTextField!
    @IBOutlet var threadBValueLabel: NSTextField!



    @IBAction func threadAStartButton(_ sender: NSButtonCell) {
    threadAGoNoGo = 1
        self.threadAValueLabel.stringValue = "Start"
    }

    @IBAction func threadAStopButton(_ sender: NSButton) {
        threadAGoNoGo = 0
        self.threadAValueLabel.stringValue = "Stop"
    }

    @IBAction func threadBStartButton(_ sender: NSButton) {
        threadBGoNoGo = 1
        self.threadBValueLabel.stringValue = "Start"
    }

    @IBAction func threadBStopButton(_ sender: NSButton) {
        threadBGoNoGo = 0
        self.threadBValueLabel.stringValue = "Stop"
    }

    func changethreadALabel(_ message: String) {
        self.threadAValueLabel.stringValue = message
    }

    func changethreadBLabel(_ message: String) {
        self.threadBValueLabel.stringValue = message
    }

The code creating the error is located in the last 2 methods:

        self.threadAValueLabel.stringValue = message

and

        self.threadBValueLabel.stringValue = message

While the following code inside of a pushbutton function, Works perfectly.

self.threadBValueLabel.stringValue = "Stop"

The code that creates the two threads is the following:

import Foundation
func startProgram(){
    let myViewController: ViewController = ViewController (nibName:nil, bundle:nil)

    // Start counting through 200 when Thread A start button is pressed and stop when Thread A Stop button is pressed. When reaching 200, go back to 0 and loop forever

    DispatchQueue(label: "Start Thread A").async {
        while true {                                    // Loop Forever
            var stepA:Int = 1
            while stepA < 200{
                for _ in 1...10000000{}                 // Delay loop
                if threadAGoNoGo == 1{
                print("Thread A \(stepA)")
                myViewController.changethreadALabel("South \(stepA)") // Update Thread A value display label
                stepA += 1
                }
            }
        stepA = 1
        }
    }

    // Start counting through 200 when Thread B start button is pressed and stop when Thread B Stop button is pressed. When reaching 200, go back to 0 and loop forever
    DispatchQueue(label: "Start Thread B").async {
        while true {                                    // Loop Forever
            var stepB:Int = 1
            while stepB < 200{
                for _ in 1...10000000{}                 // Delay loop
                if threadBGoNoGo == 1{
                    print("Tread B \(stepB)")
                    myViewController.changethreadBLabel("South \(stepB)") // Update Thread B value display label
                    stepB += 1
                }
            }
        stepB = 1
        }
    }
}

This is probably very simple to most of you, but I have spent four evenings trying to figure out by myself and searching through this forum, with no success.

New edit:

Thanks to Rob's answer, I was able to progress, but I am hitting another snag. if I move my code to the ViewController class, I seem to be able to access my stringValue variables self.threadAValueLabel.stringValue and self.threadBValueLabel.stringValue, but the below lines generate a "NSControl.stringValue must be used from main thread only" error message:

self.threadAValueLabel.stringValue = message
self.threadBValueLabel.stringValue = message

Although I understand what the error message means, I have tried to find a workaround to this problem for a few hours now and nothing seems to work.

Here is the full code

import Foundation
import Cocoa
public var threadAGoNoGo:Int = 0
public var threadBGoNoGo:Int = 0



class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        //Call Async Task

        // Start counting through 200 when Thread A start button is pressed and stop when Thread A Stop button is pressed. When reaching 200, go back to 0 and loop forever
        DispatchQueue(label: "Start Thread A").async {
            while true {                                    // Loop Forever
                var stepA:Int = 1
                while stepA < 200{
                    for _ in 1...10000000{}                 // Delay loop
                    if threadAGoNoGo == 1{
                        print("Thread A \(stepA)")
                        self.changethreadALabel("Thread A \(stepA)")
                        stepA += 1
                    }
                }
                stepA = 1
            }
        }

        // Start counting through 200 when Thread B start button is pressed and stop when Thread B Stop button is pressed. When reaching 200, go back to 0 and loop forever
        DispatchQueue(label: "Start Thread B").async {
            while true {                                    // Loop Forever
                var stepB:Int = 1
                while stepB < 200{
                    for _ in 1...10000000{}                 // Delay loop
                    if threadBGoNoGo == 1{
                        print("Tread B \(stepB)")
                        self.changethreadBLabel("Thread B \(stepB)")
                        stepB += 1
                    }
                }
                stepB = 1
            }
        }
    }

    override var representedObject: Any? {
        didSet {
           // Update the view, if already loaded.
        }
    }

    @IBOutlet var threadAValueLabel: NSTextField!
    @IBOutlet var threadBValueLabel: NSTextField!

    @IBAction func threadAStartButton(_ sender: NSButtonCell) {
    threadAGoNoGo = 1
        self.threadAValueLabel.stringValue = "Start"
    }

    @IBAction func threadAStopButton(_ sender: NSButton) {
        threadAGoNoGo = 0
        self.threadAValueLabel.stringValue = "Stop"
    }

    @IBAction func threadBStartButton(_ sender: NSButton) {
        threadBGoNoGo = 1
        self.threadBValueLabel.stringValue = "Start"
    }

    @IBAction func threadBStopButton(_ sender: NSButton) {
        threadBGoNoGo = 0
        self.threadBValueLabel.stringValue = "Stop"
    }

    func changethreadALabel(_ message:String) {
        self.threadAValueLabel.stringValue = message
    }

    func changethreadBLabel(_ message: String) {
        self.threadBValueLabel.stringValue = message
    }

}


Solution

  • Your startProgram is instantiating a new, duplicative instance of the view controller which does not have any outlets hooked up. Hence, the outlets will be nil and attempts to reference them will generate the error you describe.

    If you really wanted to make it a global function, then pass the view controller reference, e.g.:

    func startProgram(on viewController: ViewController) {
        // var myViewController = ...
    
        // now use the `viewController` parameter rather than the `myViewController` local var
    
        ...
    }
    

    And then the view controller could pass reference to itself so that the global startProgram knew what instance of the view controller to update:

    startProgram(on: self)
    

    Or, better, (a) you shouldn’t have global methods at all; and (b) even if you did, nothing outside the view controller should be updating the view controller’s outlets, anyway. Here the simple solution is to, instead, make startProgram a method of the ViewController class, and then you can refer to the outlets directly.


    You have edited your question to put startProgram in the view controller class, as advised above. You then encountered a second, different problem, where the debugger reported:

    NSControl.stringValue must be used from main thread only

    Yes, if you’re initiated a UI update from a background thread, you must dispatch that update back to the main thread, e.g.:

    DispatchQueue.main.async {
        self.changethreadALabel("Thread A \(stepA)")
    }
    

    See the Main Thread Checker for more information.