I get the error "Unexpectedly found nil while implicitly unwrapping an Optional value" when calling a protocol method


When viewersTapped is clicked I want to pass the value of currentTitle from the storeViewController to the liveViewController by using a protocol.
Code Block language
protocol labelMaker {
  func labelMaker(value: String)
}
class storeViewController: UIViewController {
 var labelDelegate: labelMaker!
 @IBAction func viewersTapped(_ sender: UIButton) {
    labelDelegate.labelMaker(value: sender.currentTitle!)
}
}

where it will be displayed within a label in the liveViewController.
Code Block language
class liveViewController: UIViewController {
let storeView = storeViewController()
  
 @IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() {
 super.viewDidLoad()
     
    storeView.labelDelegate = self
}
}
extension liveViewController: labelMaker {
  func labelMaker(value: String) {
    onlineViewers.text = value
}

But as soon as viewersTapped is clicked I am met with the error "Unexpectedly found nil while implicitly unwrapping an Optional value"
Answered by OOPer in 662621022
Thanks for your updated code. Your code is still missing storeButtonIsSelected, but many important things got clarified and I filled the missing part by guess.

So, your view hierarchy (transition structure) is something like this, OK?
Code Block
ViewContoller
|
+-(startLivePressed)---->Segue(id: segueToCamera)-> LiveViewController
| ↑
| No direct segue connection
| ↓
+-(storeButtonPressed)-->Segue(id: segueToStore)--> StoreViewController

(I have renamed all the type names with Capitalized identifiers if not yet. If you have some specific reason that you cannot follow this very common Swift coding rule, please re-interpret the type names.)

In this transition structure, you cannot use delegate pattern.
With using segue, the target view controllers will be instantiated at each transition, so once a view controller is dismissed, it is sort of not alive. But the delegate needs to be alive while the view controller using it is alive.

In other words, while StoreViewController is presented, there is no alive LiveViewController.

If you want to keep this transition structure, you may need to give up using delegate pattern.
Or else, you need to re-design the view hierarchy completely.


When keeping your transition structure, shared object would be a better strategy.

MyModel.swift:

Code Block
import Foundation
class MyModel {
static let shared = MyModel()
var labelTitle: String = ""
}

StoreViewController

Code Block
class StoreViewController: UIViewController {
// var labelDelegate: LabelMaker!
@IBAction func viewersTapped(_ sender: UIButton) {
MyModel.shared.labelTitle = sender.currentTitle ?? ""
// labelDelegate.labelMaker(value: sender.currentTitle!)
}
//...
}

LiveViewController

Code Block
class LiveViewController: UIViewController {
// let storeView = StoreViewController()
@IBOutlet weak var stopButton: UIButton!
@IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// storeView.labelDelegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
onlineViewers.text = MyModel.shared.labelTitle
}
}

If you do not prefer this, you can utilize delegate pattern with making ViewController as a delegate, but it may need a little more complex code. (Please remember ViewController is still alive while StoreViewController is presented.)


By the way, some of your protocols SegueDestination, SourceForB and SourceForC are making your code more complex than it should be. That is making it very hard to read your code.
This line is causing the issue:
Code Block
let storeView = storeViewController()

(In Swift, type names should start with Capital letters.)

You should not create a new instance of a view controller using init().
You need to access the right instance of storeViewController without creating a new one.

To find how to fix, you need to clarify the relationship (transition) between storeViewController and liveViewController.
The instance of StoreViewController that you use in storeView
let storeView = StoreViewController()
is not the one that was loaded from the storyboard when you loaded the controller. It is a new instance.
And so there is no link between the button you tap and storeView.

In addition, your code is really painful to read:
  • class should start with UpperCase

  • you should avoid giving the same name to the protocol and a func of the protocol.

Just to illustrate what I mean:

Code Block
protocol LabelMaker {
func makeLabel(value: String)
}
class StoreViewController: UIViewController {
var labelDelegate: LabelMaker!
@IBAction func viewersTapped(_ sender: UIButton) {
labelDelegate.makeLabel(value: sender.currentTitle!)
}
}

Code Block
class LiveViewController: UIViewController {
let storeView = StoreViewController() // To be changed as OOPer explained
@IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
storeView.labelDelegate = self
}
}
extension LiveViewController: LabelMaker {
func makeLabel(value: String) {
onlineViewers.text = value
}



To find how to fix, you need to clarify the relationship (transition) between storeViewController and liveViewController.

These view controllers are not directly connected by segues. So the protocol was in place to communicate the information.

These view controllers are not directly connected by segues.

Whether you use segue or not does not matter. One thing sure is that storeViewController() is not the right way to instantiate a view controller.

These view controllers are not directly connected by segues.

Could you explain the sequence between StoreViewController and LiveViewController ?
How do you transition to StoreViewController ?

Normally, you should set the delegate in destination in the initiating controller.

The user will click viewersTapped which will store the value of the senders current title into the protocol and then they will click the navigation bar back button which is viewWillDisappear to go to the base "ViewController".
Code Block language
protocol SourceForB {
  func prepare(for segue: UIStoryboardSegue, from sender: Any?, toB vc: testStore)
}
protocol labelMaker {  
func labelMaker(value: String)
}
class storeViewController: UIViewController { 
var labelDelegate: labelMaker! 
@IBAction func viewersTapped(_ sender: UIButton) {   
 labelDelegate.labelMaker(value: sender.currentTitle!)
}
override func viewWillDisappear(_ animated: Bool)
  {
     
       
    }
    super.viewWillDisappear(animated)
    self.navigationController?.isNavigationBarHidden = false
  }
}
extension testStore : SegueDestination{
   
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?){
     
    let sourceForB = segue.source as! SourceForB
    sourceForB.prepare(for: segue, from: sender, toB: self)
  }
}

Once they are in the base "ViewController" they will click startLivePressed which will segue them to the LiveViewController
Code Block language
class ViewController: UIViewController, SourceForB, SourceForC {
  
 @IBOutlet weak var startLiveButton: UIButton!
   var startLiveButtonIsSelected = false
@IBAction func startLivePressed(_ sender: UIButton) {
     
    
startLiveButtonIsSelected = !startLiveButtonIsSelected
      
performSegue(withIdentifier: "segueToCamera", sender: self)
  }
 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
     segue.prepare(sender:sender)
   }
 func prepare(for segue: UIStoryboardSegue, from sender: Any?, toC vc: liveViewController) {
    
    
   }
}


Where the value that was stored in the protocol should be displayed within the onlineViewers label
Code Block language
protocol SourceForC{
  func prepare(for segue: UIStoryboardSegue, from sender: Any?, toC vc: liveViewController)
}
class liveViewController: UIViewController {
let storeView = storeViewController()  
 @IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() { 
super.viewDidLoad()       
storeView.labelDelegate = self
}
}
extension liveViewController: labelMaker { 
 func labelMaker(value: String) {    
onlineViewers.text = value
}
extension liveViewController : SegueDestination {
  
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?){
     
    let sourceForC = segue.source as! SourceForC
    sourceForC.prepare(for: segue, from: sender, toC: self)
  }
}



Thanks for additional info, but still, the most important info is missing:
How StoreViewController is shown?


One more, please show buildable code. In your newly shown code, parentheses are not matching and some identifiers are not defined. Hard to guess what you want to do.
This cannot work:

Code Block
override func viewWillDisappear(_ animated: Bool)
{
}
super.viewWillDisappear(animated)
self.navigationController?.isNavigationBarHidden = false
}
}

Closing curly bracket line 5 is an extra.

So, did you copy the REAL code ? That's absolutely needed.
My mistake, heres how everything works.


The user will launch the app and will be met with the base "ViewController" From there they can either click startLivePressed or storeButtonPressed. If they click startLivePressed without ever going to the store there will not be a value in the onlineViewers label within the liveViewController. So they will click storeButtonPressed first.
Code Block language
class ViewController: UIViewController, SourceForB, SourceForC {  
 @IBOutlet weak var startLiveButton: UIButton!  
 
var startLiveButtonIsSelected = false
@IBAction func startLivePressed(_ sender: UIButton) {   
     
 startLiveButtonIsSelected = !startLiveButtonIsSelected     
 performSegue(withIdentifier: "segueToCamera", sender: self)
 
 } 
 
@IBAction func storeButtonPressed(_ sender: UIButton) {
   
 storeButtonIsSelected = !storeButtonIsSelected
    performSegue(withIdentifier: "segueToStore", sender: self)
  }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {    
 segue.prepare(sender:sender)  
 } 
func prepare(for segue: UIStoryboardSegue, from sender: Any?, toC vc: liveViewController) {    
       }
 func prepare(for segue: UIStoryboardSegue, from sender: Any?, toB vc: storeViewController) {
     
    if (segue.identifier == "segueToStore") {
      navigationController?.setNavigationBarHidden(navigationController?.isNavigationBarHidden == false, animated: true)
        
}
   }
@IBAction func unwindToViewController(unwindSegue: UIStoryboardSegue){
    print("unwind")
  }
}


Once the user is in the storeViewController they will click viewersTapped which should store the value of senders current title. After they click that they will click the navigation bar back button which is viewWillDisappear to go back to the viewController.
Code Block language
protocol SourceForB { 
 
func prepare(for segue: UIStoryboardSegue, from sender: Any?, toB vc: storeViewController)
}
protocol labelMaker {  
func labelMaker(value: String)
}
class storeViewController: UIViewController { 
var labelDelegate: labelMaker! 
@IBAction func viewersTapped(_ sender: UIButton) {  
  labelDelegate.labelMaker(value: sender.currentTitle!)
}
override func viewWillDisappear(_ animated: Bool)  {               
 super.viewWillDisappear(animated)   
self.navigationController?.isNavigationBarHidden = false  
}
}
extension storeViewController : SegueDestination{
   
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?){
     
    let sourceForB = segue.source as! SourceForB
    sourceForB.prepare(for: segue, from: sender, toB: self)
  }
}

Once they are back in the viewController they will now click startLivePressed
Code Block language
class ViewController: UIViewController, SourceForB, SourceForC {  
 @IBOutlet weak var stopButton: UIButton!
 @IBOutlet weak var startLiveButton: UIButton!  
 
var startLiveButtonIsSelected = false
@IBAction func startLivePressed(_ sender: UIButton) {   
     
 startLiveButtonIsSelected = !startLiveButtonIsSelected     
 performSegue(withIdentifier: "segueToCamera", sender: self)
 
 } 
 
@IBAction func storeButtonPressed(_ sender: UIButton) {
   
 storeButtonIsSelected = !storeButtonIsSelected
    performSegue(withIdentifier: "segueToStore", sender: self)
  }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {    
 segue.prepare(sender:sender)  
 } 
func prepare(for segue: UIStoryboardSegue, from sender: Any?, toC vc: liveViewController) {    
       }
 func prepare(for segue: UIStoryboardSegue, from sender: Any?, toB vc: storeViewController) {
     
    if (segue.identifier == "segueToStore") {
      navigationController?.setNavigationBarHidden(navigationController?.isNavigationBarHidden == false, animated: true)
        
}
   }
 
@IBAction func unwindToViewController(unwindSegue: UIStoryboardSegue){
    print("unwind")
  }
}

After they clicked startLivePressed they will be met with the LiveViewController where the value that was stored in viewersTapped should be showing within the onlineViewers label. And if they were to leave the app and go back in and click startLivePressed the value should still be showing within the label. stopButton is a unwind button.
Code Block language
protocol SourceForC {  
func prepare(for segue: UIStoryboardSegue, from sender: Any?, toC vc: liveViewController)
}
class liveViewController: UIViewController {
let storeView = storeViewController()   
 @IBOutlet weak var stopButton: UIButton!
@IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() { 
super.viewDidLoad()       
storeView.labelDelegate = self
}
}
extension liveViewController: labelMaker { 
 func labelMaker(value: String) {    
onlineViewers.text = value
}
}
extension liveViewController : SegueDestination {
  
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?){
     
    let sourceForC = segue.source as! SourceForC
    sourceForC.prepare(for: segue, from: sender, toC: self)
  }
}

Segue code in separate folder.
Code Block language
extension UIStoryboardSegue {
 func prepare(sender : Any?) {
    guard let destination = self.destination as? SegueDestination else{ fatalError("Destination \(self.destination) does not conform to SegueDestination protocol")}
    destination.prepareAsDestination(segue: self, sender: sender)
  }
}
extension UINavigationController : SegueDestination
{
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?){
    guard let destination = self.topViewController as? SegueDestination
      else{ fatalError("Top view contoroller must conform to protocol SegueDestination")}
     
    destination.prepareAsDestination(segue: segue, sender: sender)
  }
}
protocol SegueDestination
{
  func prepareAsDestination( segue: UIStoryboardSegue, sender: Any?)
}





Accepted Answer
Thanks for your updated code. Your code is still missing storeButtonIsSelected, but many important things got clarified and I filled the missing part by guess.

So, your view hierarchy (transition structure) is something like this, OK?
Code Block
ViewContoller
|
+-(startLivePressed)---->Segue(id: segueToCamera)-> LiveViewController
| ↑
| No direct segue connection
| ↓
+-(storeButtonPressed)-->Segue(id: segueToStore)--> StoreViewController

(I have renamed all the type names with Capitalized identifiers if not yet. If you have some specific reason that you cannot follow this very common Swift coding rule, please re-interpret the type names.)

In this transition structure, you cannot use delegate pattern.
With using segue, the target view controllers will be instantiated at each transition, so once a view controller is dismissed, it is sort of not alive. But the delegate needs to be alive while the view controller using it is alive.

In other words, while StoreViewController is presented, there is no alive LiveViewController.

If you want to keep this transition structure, you may need to give up using delegate pattern.
Or else, you need to re-design the view hierarchy completely.


When keeping your transition structure, shared object would be a better strategy.

MyModel.swift:

Code Block
import Foundation
class MyModel {
static let shared = MyModel()
var labelTitle: String = ""
}

StoreViewController

Code Block
class StoreViewController: UIViewController {
// var labelDelegate: LabelMaker!
@IBAction func viewersTapped(_ sender: UIButton) {
MyModel.shared.labelTitle = sender.currentTitle ?? ""
// labelDelegate.labelMaker(value: sender.currentTitle!)
}
//...
}

LiveViewController

Code Block
class LiveViewController: UIViewController {
// let storeView = StoreViewController()
@IBOutlet weak var stopButton: UIButton!
@IBOutlet var onlineViewers: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// storeView.labelDelegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
onlineViewers.text = MyModel.shared.labelTitle
}
}

If you do not prefer this, you can utilize delegate pattern with making ViewController as a delegate, but it may need a little more complex code. (Please remember ViewController is still alive while StoreViewController is presented.)


By the way, some of your protocols SegueDestination, SourceForB and SourceForC are making your code more complex than it should be. That is making it very hard to read your code.
Awesome, it worked almost exactly how I wanted it to. Just one last question, how do I make the value in onlineViewers stay when the user leaves the app and comes back?

how do I make the value in onlineViewers stay when the user leaves the app and comes back? 

That's a little bit too far from your original question of this thread.
For a small amount of app state info like labelTitle, UserDefaults would be a good place.

Also you should better learn about App State Restoration features of iOS:
Preserving Your Apps UI Across Launches.
(There may be some other documents. Non-Apple articles might be more easy to read, as usual.)

If you cannot make it work with your code, you should better start a new thread focused on it.

I get the error "Unexpectedly found nil while implicitly unwrapping an Optional value" when calling a protocol method
 
 
Q