Search code examples
iosswift

Why is my carefully designed to be "not-optional" return value being treated like an optional?


I'm trying to extract the earliest (and latest, but since the two methods are going to be nearly identical, I'll concentrate on "earliest" for this question) date held in a global DataSource object, theData, and return it as a NON-OPTIONAL value.

DataSource is a "souped up Array" object holding an arbitrary number of DataEntry objects. Here's the bare-bones of the definition of DataEntry:

class DataEntry: Codable, Comparable {
    var theDate: Date = Date() // To avoid using an optional but still silence compiler
                               // complaints about no initializer
    // Lots of other properties and support code irrelevant to the question snipped for brevity.
}

Retrieving the needed date(s) is done as a method of my DataSource class:

class DataSource: Codable {
var theArray: [DataEntry] = Array()

// Several hundred lines of non-relevant support and convenience code chopped

// Return either .theDate from the earliest DataEntry held in theArray, or Date() 
// if theArray.first hands back a nil (indicating theArray is unpopulated).
// Either way, I specifically want the returned value to *NOT* be an optional!
func earliest() -> Date {
    // First, make certain theArray is sorted ascending by .theDate
    theArray.sort {
        $0.theDate < $1.theDate
    }
    // Now that we know theArray is in order, theArray.first?.theDate is the earliest stored date 
    // - If it exists.
    // But since .first hands back an optional, and I specifically DO NOT WANT an optional return from 
    // .earliest(), I nil-coalesce to set firstDate to Date() as needed.
    
    let firstDate: Date = theArray.first?.theDate ?? Date()
    print("firstDate = \(firstDate)")
    return firstDate
}

func latest() -> Date {
    // gutted for brevity - mostly identical to .earliest()
    return lastDate
}
}

Note that I take pains to make sure what gets returned is not an optional. And, in fact, later code blows up if I try to treat it like it could be:

class SelectDateRangeViewController: UIViewController {
@IBOutlet weak var startPicker: UIDatePicker!
@IBOutlet weak var endPicker: UIDatePicker!

// Irrelevant code elided

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let e = theData.earliest()
    // Note: Changing the previous line to 
    // let e = theData.earliest()!   
    // gets a compile-time error: "Cannot force unwrap value of non-optional type 'Date'"                      
    let l = theData.latest()
    // We should now have the earliest and latest dates in theData held in e and l, respectively.
    // Print them to the debugger console to verify they're actually what I expect...
    print ("e = \(e)")
    print ("l = \(l)")
    // ...and carry on
    startPicker.minimumDate = e // <--- Crashes here, claiming that I've tried to implicitly unwrap an optional value! See the console log dump below
    startPicker.maximumDate = l
    startPicker.date = e
    
    endPicker.minimumDate = e
    endPicker.maximumDate = l
    endPicker.date = l
    }
 }

What follows is what I see in the debugger console when I try to run the "actual code" (rather than the gutted-to-save-space version presented here)

---start debugger log

firstDate = 2020-04-16 15:00:31 +0000 <---- This line and the next come from inside the .earliest()/.latest() methods of DataSource

lastDate = 2020-04-27 15:43:23 +0000

e = 2020-04-16 15:00:31 +0000 <---- And these two lines come from the viewWillAppear() method shown above.

l = 2020-04-27 15:43:23 +0000

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file /ActualPathRemoved/SelectDateRangeViewController.swift, line ##

--- end debugger log

Where the ## in that crash line is the actual line number occupied by the "startPicker.minimumDate = e" statement in the "all of the code is there" version.

The values displayed as "firstDate", "lastDate", "e", and "l" are ABSOLUTELY CORRECT for the test dataset I'm using.

But wait a minute! The compiler says that the return from .earliest() is not an optional when I try to force-unwrap it! And since when is a perfectly valid Date object "nil"??? And why is the code trying to "implicitly unwrap" a non-nil, not-optional value?!?!? Somebody make up my mind!

So what am I misunderstanding? Either my code is hosed, or what I understood while trying to write it is. Which is the case, and what's the problem???

Answer to @Paulw11's comment query:

@IBAction func reportRequestButtonAction(_ sender: Any) {
    let theVC = SelectDateRangeViewController()
    self.navigationController?.pushViewController(theVC, animated: true)        
}

This bit of code lives in the viewController that offers the button as part of a scene that offers a menu of "let the user tinker with the dataset in various ways" buttons. When first added, I intended to bring up the SelectRangeScene with a segue, but for some reason, decided that pushing it onto the UINavigationController stack would be a better way of doing it, though I can't remember why it was I thought that now - the reason apparently got shaken loose and lost while I was beating my head against the problem of why trying to set the datepickers was crashing.


Solution

  • You are creating your new view controller instance with

    let theVC = SelectDateRangeViewController()
    

    Since you don't get the instance from the storyboard, none of the outlets are bound; they will be nil. As your outlets are declared as implicitly unwrapped optionals, you get a crash when you refer to them.

    You could instantiate the new view controller from your storyboard but it would probably be simpler to use a segue either directly from your button or by using performSegue in your @IBAction