Search code examples
swiftcocoansscrollviewnsviewcontrollernspopover

Append text to NSScrollView - Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value


I am doing a Mac application, and I have a problem appending text to a NSScrollView when I call a function from a different class.

I have this function on my ViewController class:

import Cocoa

class PopoverVC1: NSViewController {

let popover1 = NSPopover()

class func loadView() ->PopoverVC1 {
    let vc = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), 
bundle: nil).instantiateController(withIdentifier: 
NSStoryboard.SceneIdentifier(rawValue: "Popover1")) as! PopoverVC1
    vc.popover1.contentViewController = vc
    return vc
}

override func viewDidLoad() {
    super.viewDidLoad()

    popover1.behavior = .transient
    popover1.contentViewController = self
}

func showPopover (view: NSView){
popover1.show(relativeTo: view.bounds, of: view, preferredEdge: .maxY)

}

@IBOutlet weak var radioOption1: NSButton!
@IBOutlet weak var radioOption2: NSButton!
@IBOutlet weak var radioOption3: NSButton!


@IBAction func clickOption(_ sender: NSButton) {
    switch sender {
    case radioOption1: popover1.performClose(sender)

    case radioOption2: let vc = ViewController()
        vc.myPrint(string: "This is a test")

    default: print ("hello")

    }
}
}

Than I have a PopoverVC1 class, which is a class to a popover I am using:

import Cocoa

class ViewController: NSViewController {


@IBOutlet weak var oneYes: NSButton!
@IBOutlet weak var oneNo: NSButton!
@IBOutlet weak var notesArea: NSScrollView!


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

}

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

func myPrint (string: String){
    let mystring = string
    let myNotes = notesArea.documentView as? NSTextView
    let text = myNotes?.textStorage!
    let attr = NSAttributedString(string: mystring)
    text?.append(attr)

}

let popover1 = NSPopover()
@IBAction func oneClicked(_ sender: NSButton) {


    switch sender {
    case oneYes: let vc = PopoverVC1.loadView()
        vc.showPopover(view: sender)

    case oneNo:
    let myNotes = notesArea.documentView as? NSTextView
    let text = myNotes?.textStorage!
    let attr = NSAttributedString(string: "test")
    text?.append(attr)

    default: print ("")
    }
}
}

However, I got an error when I press the radio button "oneNo" that should call the function "myPrint" and pass the argument.

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

I did some tests and when I call this same function "myPrint" from within the ViewCotroller class it works fine.

Any ideas?


Solution

  • Your issue is in clickOption when you are calling:

    let vc = ViewController()
    vc.myPrint(string: "This is a test")
    

    When you call this method from code and the ViewController's UIViews are set up in a storyboard, the connection from the storyboard is not made. That is why the notesArea is nil when you call the function myPrint. In this case you are creating a new copy of ViewController and it will not be the same one that created the popover.

    There are a few ways you can solve the problem that you are trying to accomplish. One of them is known as a delegate. This is a way for you to to call the ViewController's methods like your popover inherited them. You can check out a tutorial here. The idea is that we want to have a reference to the ViewController in your popover so that you can call the functions in the protocol. Then the ViewController that conforms to the protocol will be responsible for handling the method call.

    So let's create a protocol called PrintableDelegate and have your ViewController class conform to it. Then in your popover, you will be able to have a reference to the ViewController as a weak var called delegate (you can use what ever name you want but delegate is standard). Then we can call the methods described in the protocol PrintableDelegate, by simply writing delegate?.myPrint(string: "Test"). I have removed some of your irrelevant code from my example.

    protocol PrintableDelegate {
        func myPrint(string: String)
    }
    
    class ViewController : UIViewController, PrintableDelegate {
    
        func myPrint (string: String){
            let mystring = string
            let myNotes = notesArea.documentView as? NSTextView
            let text = myNotes?.textStorage!
            let attr = NSAttributedString(string: mystring)
            text?.append(attr)
        }
    
        @IBAction func oneClicked(_ sender: NSButton) {
            let vc = PopoverVC1.loadView()
            // Set the delegate of the popover to this ViewController
            vc.delegate = self
            vc.showPopover(view: sender)
        }
    }
    
    class PopoverVC1: NSViewController {
    
        // Delegates should be weak to avoid a retain cycle
        weak var delegate: PrintableDelegate?
    
        @IBAction func clickOption(_ sender: NSButton) {
            // Use the delegate that was set by the ViewController
            // Note that it is optional so if it was not set, then this will do nothing
            delegate?.myPrint(string: "This is a test")
        }
    }