This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 4 years ago.
I am totally new at Swift so what follows may be a very basic question. Plus it's my first one at stackoverflow.com!
I am getting the "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value" while setting text properties of UILabels in an instantiated view controller.
I am making a simple app to show a list of medicines i have at home and their expiry dates. A table view controller shows a list of the medicines, from an array called medArray, and when each one is tapped I want it to load a detail screen with some further details.
I set up a DetailView controller using IB and created the outlet connections of 3 UILabels:
// DetailView.swift
// HomeMed
//
// Created by Joao Boavida on 10/12/2018.
// Copyright © 2018 Joao Boavida. All rights reserved.
//
import UIKit
class DetailView: UIViewController {
#IBOutlet var nameLbl: UILabel!
#IBOutlet var subtextLbl: UILabel!
#IBOutlet var expdateLbl: UILabel!
}
I think the connections are successfully made as the dots on the left gutter are solid. I do get the error when tapping on a table view cell, which calls the following code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let medicine = medArray[indexPath.row]
if let vc = storyboard?.instantiateViewController(withIdentifier: "DetailView") as? DetailView {
vc.nameLbl.text = medicine.name
if let subtext = medicine.subtext {
vc.subtextLbl.text = subtext
} else {
vc.subtextLbl.text = ""
}
let ISOdateFormatter = ISO8601DateFormatter()
ISOdateFormatter.formatOptions = [.withMonth, .withYear, .withDashSeparatorInDate]
vc.expdateLbl.text = "Exp: \(ISOdateFormatter.string(from: medicine.expiry))"
navigationController?.pushViewController(vc, animated: true)
}
}
I get the error as soon as I try to access vc.nameLbl.text, which is nil for reasons I don't know. vc.expdateLbl and vc.subtextLbl are also nil.
The "vc" view controller seems to be instantiated correctly because if I omit the configuration code and push it just after creating it it displays fine, albeit with the initial text in the labels that I set in IB. It is only when I try to change those labels that it crashes.
I have tried making and remaking the outlet connections with no success. Can anyone help troubleshooting this?
Thank you very much!
You are setting your VC's outlets here, right?
vc.nameLbl.text = medicine.name <---
if let subtext = medicine.subtext {
vc.subtextLbl.text = subtext <---
} else {
vc.subtextLbl.text = "" <---
}
...
vc.expdateLbl.text = "Exp: \(ISOdateFormatter.string(from: medicine.expiry))" <---
The outlets nameLbl, subtextLbl and expdateLbl are all nil when the above code is run. They are not set at that point in time yet.
The outlets are already set when viewDidLoad is called. Therefore, what you can do is to add three properties in your DetailView:
var name: String!
var subtext: String!
var expdate: String!
Instead of setting the outlets, you set the above three properties:
vc.name = medicine.name <---
if let subtext = medicine.subtext {
vc.subtext = subtext <---
} else {
vc.subtext = "" <---
}
...
vc.expdat = "Exp: \(ISOdateFormatter.string(from: medicine.expiry))" <---
Then, in the DetailView, assign the properties to the text of the labels in viewDidLoad:
override func viewDidLoad() {
nameLbl.text = name
subtextLbl.text = subtext
expdateLbl.text = expdate
}
It crashes because your are trying to access a UILabel before it is loaded. So, you should call vc.loadViewIfNeeded(). This will load all the UIViewController views.
Related
I've created a new window controller that hosts a tabViewController inside my app. I've added classes to the window controller, and have the same class across all the view controllers in the tab view.
I can connect buttons and give them an action and it works perfectly, however, I try to connect an outlet and attempt to change something via the outlet, it returns nil and crashes the program. For example, this exact code works in the pre-made viewController of my app, but returns:
Unexpectedly found nil while implicitly unwrapping an Optional value
when running it through the new tab view controller I created.
What's weird to me is I can use a regular view controller and the outlets connect fine, but if I want to use a tab view controller, these nil errors are happening.
I made sure that the nil was not related to grabbing the inputs for the button by printing the audio devices, and the audio devices are there and able to be printed. It seems as if the button is not there even though it is connected.
I have also tried to simply change an NSTextField color after connecting it to an outlet and this returns the same nil error.
Any idea what I might be doing wrong here? Thanks so much for the help.
class FirstLaunchViewController: NSViewController {
var FLWindow: FirstLaunchWindowController?
var selectedAudioDevice = [String]()
#IBOutlet weak var deviceListPopUpButton: NSPopUpButton!
#IBOutlet weak var visualizeButton: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = NSMakeSize(self.view.frame.size.width, self.view.frame.size.height)
populateDeviceList()
}
#IBAction func CloseStartupGuide(_ sender: Any) {
self.view.window?.windowController?.close()
}
#IBAction func popUpDidChange(_ sender: Any) {
print("changed")
}
//grab inputs for button
fileprivate func populateDeviceList() {
deviceListPopUpButton.removeAllItems()
for device in AudioDevice.allInputDevices() {
var teststring = ""
teststring = device.uid!
print(device.name)
deviceListPopUpButton.addItem(withTitle: device.name)
deviceListPopUpButton.lastItem?.tag = Int(device.id)
selectedAudioDevice.append(device.uid!)
}
}
}
}
I have a segue named "hydrogenSegue" from a "hydrogenBoxButton" to a "Hydrogen" view controller. However, I also wanted to implement a table view so I could search for an element. I tried to make the code so when the cell is clicked it will segue over to the element's view. I used hydrogen as an example here.
In my main ViewController.swift file, I have this to transfer the data:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//If identifier equals the hydrogen element go to the elements Swift file
if segue.identifier == "hydrogenSegue" {
let hydrogenAtomicNumberPassing = segue.destination as! hydrogenViewController
hydrogenAtomicNumberPassing.hydrogenAtomicNumberPassed = hydrogenAtomicNumber
let hydrogenAtomicMassPassing = segue.destination as! hydrogenViewController
hydrogenAtomicMassPassing.hydrogenAtomicMassPassed = hydrogenAtomicMass
}
}
In the hydrogenViewController.swift file I have this:
import UIKit
class hydrogenViewController: UIViewController {
var hydrogenAtomicNumberPassed: Int!
var hydrogenAtomicMassPassed: Float!
#IBOutlet weak var hydrogenInformationLabel: UILabel!
#IBOutlet weak var hydrogenAtomicNumberLabel: UILabel!
#IBOutlet weak var hydrogenAtomicMassLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//Setting the background color
self.view.backgroundColor = UIColor.gray
//Converting hydrogen's atomic number from an Int to a String
let hydrogenAtomicNumberString = String("\(hydrogenAtomicNumberPassed!)")
hydrogenAtomicNumberLabel.text = "Atomic Number: \(hydrogenAtomicNumberString)"
//Converting hydrogen's atomic mass from a Float to a String
let hydrogenAtomicMassString = String("\(hydrogenAtomicMassPassed!)")
hydrogenAtomicMassLabel.text = "Atomic Mass: \(hydrogenAtomicMassString)"
}
}
I am getting the error at:
let hydrogenAtomicNumberString = String("\(hydrogenAtomicNumberPassed!)")
I'm assuming it would happen to this line also if I fix only that line:
let hydrogenAtomicMassString = String("\(hydrogenAtomicMassPassed!)")
I have this code in my "searchViewController" (the .swift file used for the table view):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("row selected : \(indexPath.row)")
if indexPath.row == 0 {
let hydrogenSearchSegue = UIStoryboard(name:"Main",
bundle:nil).instantiateViewController(withIdentifier: "hydrogenView") as!
hydrogenViewController
self.navigationController?.pushViewController(hydrogenSearchSegue,
animated:true)
}
}
When I click on the "Hydrogen" cell in the table view it crashes to this error:
Hydrogen cell
The crash
When I click on the "H" button in this image it will take me to the hydrogen view controller:
Image of the Hydrogen Button in the simulator (Top Left)
Image of the Hydrogen View Controller
I want the hydrogen cell to segue over to the hydrogen view controller just like the button can.
When this same issue came up earlier I just had an issue with the name of the segue in the storyboard. However, because there is no visible segue from the table view, I don't know how to fix the issue.
I've tried this:
performSegue(withIdentifier: "hydrogenSegue", sender: nil)
I was thinking that I could just reuse the "hydrogenSegue" from the button to the view controller but I get a SIGABRT error. It just says that there is no segue with the name "hydrogenSegue." It would be best if I could just reuse that segue in a way because everything is already connected but I now found out that the "searchViewController" can't recognize the segue. Any help is appreciated and my main goal is to just get the cell that is clicked on to move over to the element's designated view. I tried to provide as much information as possible without making it to long and if there is any more information needed, I should be able to provide it.
well. first answer
in your hydrogenViewController try with this lines.
var hydrogenAtomicNumberPassed: Int?
var hydrogenAtomicMassPassed: Float?
override func viewDidLoad(){
super.viewDidLoad()
self.viewBackgroundColor = .gray
}
override func viewWillAppear(){
super.viewWillAppear()
if let number = hydrogenAtomicNumberPassed
{
hydrogenAtomicNumberLabel.text = "Atomic Number: \(number)"
}
if let mass = hydrogenAtomicMassPassed
{
hydrogenAtomicMassLabel.text = "Atomic Mass: \(mass)"
}
}
Now, the segues only "lives" between a couple viewControllers, if you have a third view controller, the last will not recognize him.
other thing, you are using segues and navigation controller, from my point of view, it's a bad idea mixed both, I mean, there are specific apps that can use both ways to present views, only is a advice.
if you want to pass data with pushviewcontroller only use this line
if indexPath.row == 0 {
let hydrogenSearchSegue = UIStoryboard(name:"Main",bundle:nil).instantiateViewController(withIdentifier: "hydrogenView") as! hydrogenViewController
hydrogenSearchSegue.VAR_hydrogenViewController = YOURVAR_INYOURFILE
self.navigationController?.pushViewController(hydrogenSearchSegue, animated:true)
}
tell me if you have doubts, and I will try to help you.
Full code for this branch here
View controller "MovieDetailsVC" is presented to the navigation controller when a cell is selected.
The presenting view controller, "ViewController", stores the row of the tableView to display in NSUserDefaults as an Int.
"MovieDetailsVC" reads the row ok. It then pulls the whole array of custom class info from CoreData and stores the array row in a property.
The data is displayed ok at first. The IBOutlet connections are setup ok. I've disconnected and reconnected twice all outlets on MovieDetailsVC, so that should be ok.
"viewDidLoad" is called a successive time. Not sure from where. When it is called, the coredata entity and row number are pulled ok.
The issue is at line "lblTitle.text = self.movieRecord.title". I'm assuming any of the IBOutlets would cause the same issue.
The error thrown is what you would see if the outlets were not connected:
fatal error: unexpectedly fond nil while unwrapping Optional value.
code for the MovieDetailsVC is below. Any ideas why this outlet link would break after working ok would be greatly appreciated.
import UIKit
import CoreData
class MovieDetailsVC: UIViewController {
#IBOutlet var lblTitle: UILabel!
#IBOutlet var lblDescr: UILabel!
#IBOutlet var lblLink: UILabel!
#IBOutlet var imgMovie: UIImageView!
var movieRecord:FavMovie!
var favMovies = [FavMovie]()
override func viewDidLoad() {
super.viewDidLoad()
fetchAndSetResult()
}
func fetchAndSetResult() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "FavMovie")
do {
let results = try context.executeFetchRequest(fetchRequest)
self.favMovies = results as! [FavMovie]
} catch let err as NSError {
print(err.description)
}
if let row = NSUserDefaults.standardUserDefaults().objectForKey("movieRow") as? Int {
self.movieRecord = self.favMovies[row]
configureCellDescr()
}
}
func configureCellDescr() {
lblTitle.text = self.movieRecord.title
lblDescr.text = self.movieRecord.descrWhyGood
lblLink.text = self.movieRecord.linkImdb
imgMovie.image = self.movieRecord.getImg()
}
}
I just have a look at your source code in github and find the problem. There are two issues and I will explain it following.
it does that the second time that the app overrides viewdidload
The reason that your code would call the viewDidLoad method twice is because you have a segue in your storyboard that connect the tableViewCell to movieDetailVC. This will present the movieDetailVC once you click the cell.
And in your code for didSelectCell method, you create another movieDetailVC object and present it.
So actually movieDetailVC would be presented twice when you click the cell. This cause the issue.
Any ideas why this outlet link would break after working ok would be greatly appreciated
The reason why the IBOutlet is nil is because of the way you present movieDetailVC in your code. You create the movieDetailVC object using: let movieDetailsVC = MovieDetailsVC(). Doing it this way, the IBOutlets will not be connected correctly, because ios dont know about the storyboard information.
The correct way to create a MovieDetailVC object is to instantiate from storyboard. See this post for difference.
Solution
There is a very simple solution for your code design:
just remove let movieDetailsVC = MovieDetailsVC() and self.navigationController?.presentViewController(movieDetailsVC, animated: true, completion: nil) from your ViewController class. Since you save the selection data in NSUserDefault, the cell segue will present movieDetailVC and your movieDetailVC can also get the selection data from userDefault.
I'm working on my first iOS app using swift. I'm trying to load the a value from one viewController into another. I am using a protocol, but I can't get it to execute properly. I have searched around both stack overflow and elsewhere but haven't been able to find an answer that works in my situation.
Here's the VC I'm trying to pull the value from:
protocol AddHelperVCDelegate {
func didFinishAddingHelper(controller: AddHelperViewController)
}
class AddHelperViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
#IBOutlet weak var tableView: UITableView!
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var fetchedResultsController:NSFetchedResultsController = NSFetchedResultsController()
var delegate:AddHelperVCDelegate! = nil
var helperBonus:NSNumber = 0
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController = getFetchResultsController()
fetchedResultsController.delegate = self
fetchedResultsController.performFetch(nil)
// Do any additional setup after loading the view.
}
}
And here is where I am trying to move the value (and the view) back to a proceeding VC.
// UITableViewDelegate
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var indexPathForRow = tableView.indexPathForSelectedRow()
println("indexPath for selected row is: \(indexPathForRow)")
let thisUser = fetchedResultsController.objectAtIndexPath(indexPath) as UserModel
var cell:UserCell = tableView.dequeueReusableCellWithIdentifier("helperCell") as UserCell
helperBonus = thisUser.effectiveCombat
helperBonus = Int(helperBonus)
println("helperBonus is: \(helperBonus)")
delegate.didFinishAddingHelper(self)
}
If I make the delegate an optional (delegate?.didFinishAddingHelper(self)) then nothing happens. If I do not, I get a crash with the error message:
indexPath for selected row is: Optional(<NSIndexPath: 0xc000000000018016> {length = 2, path = 0 - 3})
helperBonus is: 0
fatal error: unexpectedly found nil while unwrapping an Optional value
Now, I know I'm declaring delegate as nil, but that's the limit to my understanding of what's going on. I need to add the value in the proceeding VC in this function:
func didFinishAddingHelper(controller: AddHelperViewController) {
self.effectiveCombat = Int(controller.helperBonus)
controller.navigationController?.popViewControllerAnimated(true)
}
The crash is happening because AddHelperViewController's delegate property is nil. This is because you aren't setting it.
Wherever you create the AddHelperViewController, set its delegate on the next line:
let addHelperVC = AddHelperViewController()
addHelperVC.delegate = self
Then when you call the delegate property, it will point back to the view controller that created it.
If your AddHelperViewController is created using a storyboard, set delegate in the prepareForSegue(_:sender:) of the method that is about to show the new controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let vc = segue.destinationViewController as? AddHelperViewController {
// If we got here, vc is an AddHelperViewController
vc.delegate = self
}
}
var delegate:AddHelperVCDelegate! = nil
Should be
var delegate:AddHelperVCDelegate?
It is failing because you are using !, which means that you guarantee that, while it can't be initialized during the classes init, it will not be nil by the time you want to use it.
Aaron Brager is absolutely correct when he says you need to set the delegate property.
With your delegate declared as optional (with ?), you can call it only if the object is not nil (ignored otherwise):
delegate?.didFinishAddingHelper(self)
On a side note, you might also consider making the delegate property weak in order to help prevent retain cycles.
I'm getting a Can't unwrap Optional.None error when running a slightly modified version of the Master-Detail App in Swift.
All I did was add a second UILabel to the DetailViewController, right under the pre-existing detailDescriptionLabel, and when I navigate to the DetailViewController from the MasterViewController I crash on the statement which sets my new Label:
secondLabel.text = "This is the Second Label"
I declare this label is as followed:
#IBOutlet var secondLabel : UILabel
What's really interesting is that the pre-existing code for setting the detailDescriptionLabel includes the new optional let syntax:
if let label = self.detailDescriptionLabel {
label.text = "This is the Detail View"
}
So why is it that we need a let statement here for detailDescriptionLabel? It was never declared as an Optional Label, it was declared like any regular Label IBOutlet property, like so:
#IBOutlet var detailDescriptionLabel: UILabel
so why is it being treated as an Optional?
And does this mean that from now on any object I add as an IBOutlet will also have to go through this sort of let statement if I want to set it through code?
EDIT:
I'm crashing in the following method, on the line anotherLabel.text = "Second Label":
func configureView() {
// Update the user interface for the detail item.
if let theCar: CarObject = self.detailItem as? CarObject {
if let label = self.detailDescriptionLabel {
label.text = theCar.make
}
anotherLabel.text = "Second Label"
}
}
but when I treat anotherLabel with the whole if let business, as follows, it works perfectly well:
if let label2 = self.anotherLabel {
label2.text = "Second Label"
}
Properties declared with #IBOutlet are always implicitly unwrapped optional variables. Apple's documentation explains it this way:
When you declare an outlet in Swift, the compiler automatically
converts the type to a weak implicitly unwrapped optional and assigns
it an initial value of nil. In effect, the compiler replaces
#IBOutlet var name: Type with #IBOutlet weak var name: Type! = nil.
The compiler
converts the type to an implicitly unwrapped optional so that you
aren’t required to assign a value in an initializer. It is implicitly
unwrapped because after your class is initialized from a storyboard or
xib file, you can assume that the outlet has been connected. Outlets
are weak by default because the outlets you create usually have weak
relationships.
Since it's implicitly unwrapped, you don't have to go through if let label = something every time, just know that if your label is nil and you try to work with it, you'll end up with a runtime error. I'm guessing your second label isn't hooked up in Interface Builder -- is that the case? [OP: Nope!]
Okay, what's happening in this specific case is that the configureView() method can get called from the master view controller's prepareForSegue(), since the detailItem property on the detail view controller has a didSet handler. When that happens, the detail view controller hasn't been loaded yet, so no labels have been created. Since the labels will get set up at the same time, you could put both initializations under that one if statement (and make it a bit more clear, even):
func configureView() {
// Update the user interface for the detail item.
if let theCar: CarObject = self.detailItem as? CarObject {
if self.detailDescriptionLabel != nil {
self.detailDescriptionLabel.text = theCar.make
self.secondLabel.text = "Second Label"
}
}
}