Swift 1.2
Swift, true to its name, is moving fast. This week marks the beta release of Swift 1.2, a major update to the language. The Swift team has responded to so many of the community’s requests in one fell swoop, we’re overflowing with new and exciting features. Every line-item in this announcement is a big advancement: incremental builds, improved error messages and stability in Xcode, static class properties, support for C unions and bitfields, bridging of Swift enum
s to Objective-C, safer type casting, improvements to single-line closures, and more.
In what follows, let’s look at two major aspects of the release that will significantly improve the experience of working in Swift: first, big changes in if let
optional binding (“finally”), and second, new access to nullability annotations in Objective-C.
Improved Optional Binding
Swift 1.2 allows multiple simultaneous optional bindings, providing an escape from the trap of needing deeply nested if let
statements to unwrap multiple values. Multiple optional bindings are separated by commas and can be paired with a where
clause that acts like the expression in a traditional if
statement. As such, the byzantine pyramid of doom has been renovated into a mid-century modern ranch:
Old:
let a = "10".to Int()
let b = "5".to Int()
let c = "3".to Int()
if let a = a {
if let b = b {
if let c = c {
if c != 0 {
println("(a + b) / c = \((a + b) / c)")
}
}
}
}
New:
if let a = a, b = b, c = c where c != 0 {
println("(a + b) / c = \((a + b) / c)") // (a + b) / c = 5
}
The order of execution in these two examples is identical. Using the new syntax, each binding is evaluated in turn, stopping if any of the attempted bindings is
nil
. Only after all the optional bindings are successful is thewhere
clause checked.An
if
statement can actually have more than onelet
binding separated by commas. Since eachlet
binding can bind multiple optionals and include awhere
clause, some truly sophisticated logic is possible with this construct. (Thanks to Stephen Celis for helping clarify this point.)
What’s more, later binding expressions can reference earlier bindings. This means you can delve into Dictionary
instances or cast an Any
value to a specific type, then use it in another expression, all in a single if let
statement.
To revisit the canonical example, this is what parsing a big block of JSON can look like in Swift 1.2. The example uses one if let
block to handle the optionals that come with using NSBundle
, NSURL
and NSData
, then another to map several Any
instances from the interpreted JSON to specific types:
var users: [User] = []
// load and parse the JSON into an array
if let
path = NSBundle.main Bundle().path For Resource("users", of Type: "json"),
url = NSURL(file URLWith Path: path),
data = NSData(contents Of URL: url),
user List = NSJSONSerialization.JSONObject With Data(data, options: nil, error: nil) as? [[String: Any Object]]
{
// extract individual users
for user Dict in user List {
if let
id = user Dict["id"] as? Int,
name = user Dict["name"] as? String,
email = user Dict["email"] as? String,
address = user Dict["address"] as? [String: Any Object]
{
users.append(User(id: id, name: name, ...))
}
}
}
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "[email protected]",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "[email protected]",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
...
]
I see many commas in our future.
Nullability Annotations
When Swift was first released, every call to a Cocoa API method took and returned implicitly unwrapped optionals (i.e., Any
). Because they crash a program on access, implicitly unwrapped return values are inherently unsafe if there isn’t clear documentation of whether or not a method will return a null value. All those exclamation marks were a sign of bad form. Sure, Swift bridged to the Cocoa APIs, but it always looked grumpy about it.
As the beta releases rolled on through the summer and fall, internal audits gradually removed the offending punctuation, replacing implicitly unwrapped optionals with either true optionals or non-optional, never-gonna-be-nil
values. This vastly improved the experience of working with Cocoa, but there was no mechanism to mark up third-party code in the same way, leaving part of the problem in place.
But no longer—Swift 1.2 ships alongside a new version of Clang. New property attributes and pointer annotations allow you to indicate whether a pointer, be it an Objective-C property, method parameter, or return value, can or won’t ever be nil
.
nonnull
: Indicates that the pointer should/will never benil
. Pointers annotated withnonnull
are imported into Swift as their non-optional base value (i.e.,NSData
).nullable
: Indicates that the pointer can benil
in general practice. Imported into Swift as an optional value (NSURL?
).null_unspecified
: Continues the current functionality of importing into Swift as an implicitly unwrapped optional, ideally to be used during this annotation process only.null_resettable
: Indicates that while a property will always have a value, it can be reset by assigningnil
. Properties with a non-nil
default value can be annotated this way, liketint
. Imported into Swift as a (relatively safe) implicitly unwrapped optional. Document accordingly!Color
The first three annotations can also be used with C pointers and block pointers, using the doubly-underscored __nonnull
, __nullable
, and __null_unspecified
. The last annotation, null_resettable
, is only valid as an Objective-C property attribute.
Nullability in Action
As an example to show the benefit of these annotations, let’s take a look at a data controller used to handle a list of locations, each with a possible attached photo:
@interface Location Data Controller : NSObject
@property (nonatomic, readonly) NSArray *locations;
@property (nonatomic, readonly) Location *latest Location;
- (void)add Photo:(Photo *)photo for Location:(Location *)location;
- (Photo *)photo For Location:(Location *)location;
@end
Without any nullability annotations, each pointer in my Location
class is imported to Swift as an implicitly unwrapped optional:
class Location Data Controller : NSObject {
var locations: [Any Object]! { get }
var latest Location: Location! { get }
func add Photo(photo: Photo!, for Location location: Location!)
func photo For Location(location: Location!) -> Photo!
}
Enough! With! The shouting! Here’s how I can now annotate the Objective-C interface:
@interface Location Data Controller : NSObject
@property (nonnull, nonatomic, readonly) NSArray *locations;
@property (nullable, nonatomic, readonly) Location *latest Location;
- (void)add Photo:(nonnull Photo *)photo for Location:(nonnull Location *)location;
- (nullable Photo *)photo For Location:(nonnull Location *)location;
@end
First, the properties—my locations
list is nonnull
, since at worst it will be an empty array, but latest
can be nil
if there are no locations in the list yet. Likewise, the parameters to my two methods should always have a value, yet because not all locations have a photo, that second method returns a nullable
photo. Back in Swift, the results are much better—that is, clearer about how to safely use the data controller and no more grumpy !
s:
class Location Data Controller : NSObject {
var locations: [Any Object] { get }
var latest Location: Location? { get }
func add Photo(photo: Photo, for Location location: Location)
func photo For Location(location: Location) -> Photo?
}
NS_ASSUME_NONNULL_BEGIN/END
Annotating any pointer in an Objective-C header file causes the compiler to expect annotations for the entire file, bringing on a cascade of warnings. Given that most annotations will be nonnull
, a new macro can help streamline the process of annotating existing classes. Simply mark the beginning and end of a section of your header with NS_ASSUME_NONNULL_BEGIN
and ..._END
, then mark the exceptions.
Another revision of our data controller interface from above results in a more readable version with the exact same Swift profile:
@interface Location Data Controller : NSObject
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSArray *locations;
@property (nullable, nonatomic, readonly) Location *latest Location;
- (void)add Photo:(Photo *)photo for Location:(Location *)location;
- (nullable Photo *)photo For Location:(Location *)location;
NS_ASSUME_NONNULL_END
@end
Not Just for Swift
The new Objective-C nullability annotations have huge benefits for code on the Swift side of the fence, but there’s a substantial gain here even without writing a line of Swift. Pointers marked as nonnull
will now give a hint during autocomplete and yield a warning if sent nil
instead of a proper pointer:
// Can I remove a photo by sending nil?
[data Controller add Photo:nil for Location:current Location];
// Nope -- Warning: Null passed to a callee that requires a non-null argument
Excitingly, all that is just half the story. In addition to the changes in Swift syntax and compiler savoir faire, the standard library has also seen a major revision, including a proper Set
class (so long, dear friend). Okaaay, so none of our code works anymore, and Stack Overflow has 21,000 out-of-date Swift questions? It’s still fun to be along for the ride.