I have a custom object called Field. I basically use it to define a single field in a form.
class Field {
var name: String
var value: Any?
// initializers here...
}
When the user submits the form, I validate each of the Field
objects to make sure they contain valid values. Some fields aren't required so I sometimes deliberately set nil
to the value
property like this:
field.value = nil
This seems to pose a problem when I use an if-let to determine whether a field is nil or not.
if let value = field.value {
// The field has a value, ignore it...
} else {
// Add field.name to the missing fields array. Later, show the
// missing fields in a dialog.
}
I set breakpoints in the above if-else and when field.value
has been deliberately set to nil, it goes through the if-let block, not the else. However, for the fields whose field.value
I left uninitialized and unassigned, the program goes to the else block.
I tried printing out field.value
and value
inside the if-let block:
if let value = field.value {
NSLog("field.value: \(field.value), value: \(value)")
}
And this is what I get:
field.value: Optional(nil), value: nil
So I thought that maybe with optionals, it's one thing to be uninitialized and another to have the value of nil. But even adding another if inside the if-let won't make the compiler happy:
if let value = field.value {
if value == nil { // Cannot invoke '==' with an argument list of type '(Any, NilLiteralConvertible)'
}
}
How do I get around this? I just want to check if the field.value
is nil
.
I believe this is because Any?
allows any value and Optional.None
is being interpreted as just another value, since Optional
is an enum!
AnyObject?
should be unable to do this since it only can contain Optional.Some([any class object])
, which does not allow for the case Optional.Some(Optional)
with the value Optional.None
.
This is deeply confusing to even talk about. The point is: try AnyObject?
instead of Any?
and see if that works.
More to the point, one of Matt's comment mentions that the reason he wants to use Any
is for a selection that could be either a field for text input or a field intended to select a Core Data object.
The Swifty thing to do in this case is to use an enum with associated values, basically the same thing as a tagged/discriminated union. Here's how to declare, assign and use such an enum:
enum DataSelection {
case CoreDataObject(NSManagedObject)
case StringField(String)
}
var selection : DataSelection?
selection = .CoreDataObject(someManagedObject)
if let sel = selection { // if there's a selection at all
switch sel {
case .CoreDataObject(let coreDataObj):
// do what you will with coreDataObj
case .StringField(let string):
// do what you will with string
}
}
Using an enum like this, there's no need to worry about which things could be hiding inside that Any?
. There are two cases and they are documented. And of course, the selection variable can be an optional without any worries.
There's a tip to replace my Any?
type with an enum but I couldn't get this error out of my head. Changing my approach doesn't change the fact that something is wrong with my current one and I had to figure out how I arrived at an Optional(nil)
output.
I was able to reproduce the error by writing the following view controller in a new single-view project. Notice the init
signature.
import UIKit
class Field {
var name: String = "Name"
var value: Any?
init(_ name: String, _ value: Any) {
self.name = name
self.value = value
}
}
class AppState {
var currentValue: Field?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let f = Field("Amount", AppState().currentValue)
NSLog("\(f.value)")
}
}
In short, I was passing a nil value (AppState().currentValue
) to an initializer that accepts Any
, and assigns it to a property whose type is Any?
. The funny thing here is if I directly passed nil
instead, the compiler will complain:
let f = Field("Amount", nil) // Type 'Any' does not conform to protocol 'NilLiteralConvertible'
It seems that somewhere along the way, Swift wraps the nil
value of AppState().currentValue
in an optional, hence Optional(nil)
.