Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift optionals: language issue, or doing something wrong?

Tags:

ios

swift

I am doing what I believe to be a very simple task. I'm trying to get a value out of a dictionary if the key exists. I am doing this for a couple keys in the dictionary and then creating an object if they all exist (basically decoding a JSON object). I am new to the language but this seems to me like it should work, yet doesn't:

class func fromDict(d: [String : AnyObject]!) -> Todo? {
    let title = d["title"]? as? String
    // etc...
}

It gives me the error: Operand of postfix ? should have optional type; type is (String, AnyObject)

HOWEVER, if I do this, it works:

class func fromDict(d: [String : AnyObject]!) -> Todo? {
    let maybeTitle = d["title"]?
    let title = maybeTitle as? String
    // etc...
}

It appears to be basic substitution but I may be missing some nuance of the language. Could anyone shed some light on this?

like image 419
jcomo Avatar asked Sep 02 '14 06:09

jcomo


People also ask

What is the purpose of optionals in Swift?

Optionals are in the core of Swift and exist since the first version of Swift. An optional value allows us to write clean code with at the same time taking care of possible nil values. If you're new to Swift you might need to get used to the syntax of adding a question mark to properties.

How do I make a return optional in Swift?

Swift lets you override its safety by using the exclamation mark character: ! . If you know that an optional definitely has a value, you can force unwrap it by placing this exclamation mark after it.

What is optional binding in Swift?

Optional binding is a mechanism built into Swift to safely unwrap optionals. Since an optional may or may not contain a value, optional binding always has to be conditional. To enable this, conditional statements in Swift support optional binding, which checks if a wrapped value actually exists.


2 Answers

The recommended pattern is

if let maybeTitle = d["title"] as? String {
    // do something with maybeTitle
}
else {
    // abort object creation
}

It is possibly really a question of nuance. The form array[subscript]? is ambiguous because it could mean that the whole dictionary (<String:AnyObject>) is optional while you probably mean the result (String). In the above pattern, you leverage the fact that Dictionary is designed to assume that accessing some key results in an optional type.

After experimenting, and noticing that the ? after as is just as ambiguous, more, here is my solution:

var dictionary = ["one":"1", "two":"2"]
// or var dictionary = ["one":1, "two":2]
var message = ""
if let three =  dictionary["three"] as Any? {
    message = "\(three)"
}
else {
    message = "No three available."
}
message // "No three available."

This would work with all non-object Swift objects, including Swift Strings, numbers etc. Thanks to Viktor for reminding me that String is not an object in Swift. +

If you know the type of the values you can substitute Any? with the appropriate optional type, like String?

like image 146
Mundi Avatar answered Oct 14 '22 10:10

Mundi


There are a few of things going on here.

1) The ? in d["title"]? is not correct usage. If you're trying to unwrap d["title"] then use a ! but be careful because this will crash if title is not a valid key in your dictionary. (The ? is used for optional chaining like if you were trying to call a method on an optional variable or access a property. In that case, the access would just do nothing if the optional were nil). It doesn't appear that you're trying to unwrap d["title"] so leave off the ?. A dictionary access always returns an optional value because the key might not exist.

2) If you were to fix that:

let maybeTitle = d["title"] as? String

The error message changes to: error: '(String, AnyObject)' is not convertible to 'String'

The problem here is that a String is not an object. You need to cast to NSString.

let maybeTitle = d["title"] as? NSString

This will result in maybeTitle being an NSString?. If d["title"] doesn't exist or if the type is really NSNumber instead of NSString, then the optional will have a value of nil but the app won't crash.

3) Your statement:

let title = maybeTitle as? String

does not unwrap the optional variable as you would like. The correct form is:

if let title = maybeTitle as? String {
    // title is unwrapped and now has type String
}

So putting that all together:

if let title = d["title"] as? NSString {
    // If we get here we know "title" is a valid key in the dictionary, and
    // we got the type right.  title has now been unwrapped and is ready to use
}

title will have the type NSString which is what is stored in the dictionary since it holds objects. You can do most everything with NSString that you can do with String, but if you need title to be a String you can do this:

if var title:String = d["title"] as? NSString {
    title += " by Poe"
}

and if your dictionary has NSNumbers as well:

if var age:Int = d["age"] as? NSNumber {
    age += 1
}
like image 21
vacawama Avatar answered Oct 14 '22 10:10

vacawama