In the first article of this introductory series on Swift, we talked about Swift's philosophy, took a first look at its syntax, and highlighted a few key differences with Objective-C. In this article, we continue our exploration of Swift's syntax. You'll also learn about optionals and see how memory management works in Swift.
1. Conditionals and Loops
If
If statements are identical in Swift and Objective-C with the exception of two subtle differences:
- parentheses around the condition variable are optional
- curly braces are required
These are about the only differences with if statements in Objective-C.
Ranges
As we saw in the first article, Swift includes two range operators ..<
and ...
to specify a range of values. These two operators are the half-closed range operator and the closed range operator.
A half-closed range, such as 1..<5
, represents the values 1, 2, 3, and 4, excluding 5. A closed range, such as 1...5
, represents the values 1, 2, 3, 4, and includes 5.
Ranges can be used in for
loops, array
subscript, and even in switch
statements. Take a look at the following examples.
// for loop example for i in 1..<10 { } // iterates from 1 to 9
// array subscript example let someArray = ["apple","pair","peach","watermelon","strawberry"] for fruit in someArray[2..<4] { println(fruit) } // outputs: peach and watermelon
// switch example switch someInt { case 0: // do something with 0 case 1..<5: // do something with 1,2,3,4 case 5...10: // do something with 5,6,7,8,9,10 default: // everything else }
Switch
Switch statements are more powerful in Swift than they are in Objective-C. In Objective-C, the result of the expression of a switch statement needs to be of type integer and the values of each case statement should be a constant or a constant expression. This isn't true in Swift. In Swift, the case statements can be of any type, including ranges.
In Swift, switch has no break
statements and it doesn't automatically fall through from one case to another. When writing a switch statement, care must be taken that all conditions are being handled by its case statements, failing to do so will result in a compiler error. A sure way to cover all conditions is to include a default
case statement.
Here's an example of a switch
statement with String
cases:
let vegetable = "red pepper" switch vegetable { case "celery": let vegetableComment = "Add some raisins and make ants on a log." case "cucumber", "watercress": let vegetableComment = "That would make a good tea sandwich." default: let vegetableComment = "Everything tastes good in soup." }
In Swift, case statements don't fall through by default. This was a deliberate design decision to avoid common errors. If a specific case needs to fall through, you can use the fallthrough
keyword to indicate this to the compiler.
switch someInt { case 0: // do something with 0 case 1: // do something with 1 case 2: // do something with 2 fallthrough default: //do something for everything else } // case 2 will fall through to default case
It doesn't stop here. Swift adds two other features to switch, value bindings and the where clause. Value binding is used with the case let
keywords to bind a constant with the matching case. The where clause adds an extra condition to the case statement using the where
keyword.
These two concepts are better explained with examples. The following code block shows how value binding works.
let somePoint = (xaxis:2, yaxis:0) switch somePoint { case (let x, 0): println("on the x-axis with an x value of \(x)") case (0, let y): println("on the y-axis with a y value of \(y)") case let (x, y): println("somewhere else at (\(x), \(y))") }
The first case statement, case (let x, 0)
, will match the values where yaxis
is equal to 0
and any value for xaxis
, and we bind xaxis
to the constant x
to be used inside the case statement.
Here's an example of the where clause in action.
let vegetable = "red pepper" switch vegetable { case "celery": println("Add some raisins and make ants on a log.") case let x where x.hasSuffix("pepper"): println("I'm allergic to \(x)") default: println( "Everything tastes good in soup.") } // outputs: I'm allergic to red pepper
2. Functions and Closures
Functions
Defining functions and closures is easy in Swift. Objective-C developers know them better as functions and blocks.
In Swift, function parameters can have default values, which is reminiscent of scripting languages, such as PHP and Ruby.
The syntax for functions is as follows:
func functionName(parameterName: Type = DefaultValue) -> returnType { [...] return returnType; }
To write a sayHello
function that takes a parameter name
of type String
and returns a Bool
when successful, we write the following:
func sayHello(name: String) -> Bool { println("hello \(name)"); return true; } sayHello("World") // output // hello World
To pass a default value for the name
parameter, the function's implementation would look as follows:
func sayHello(name: String = "World") -> Bool { println("hello \(name)"); return true; } sayHello() // output // hello World sayHello("mike") // output // hello mike
A feature that is completely absent in Objective-C are tuples. In Swift, functions can return multiple values in the form of a tuple. Tuples are treated as a single variable, which means that you can pass it around just like a variable.
Tuples are very easy to use. In fact, we've already worked with tuples in the previous article when we enumerated a dictionary. In the next code snippet, the key/value pair is a tuple.
for (key,value) in someDictionary { println("Key \(key) has value \(value)" }
How are tuples used and how do you benefit from them? Let's take a look at another example. Let's modify the above sayHello
function to return a boolean when successful as well as the resulting message. We do this by returning a tuple, (Bool, String)
. The updated sayHello
function looks like this:
func sayHello(name: String = "World") -> (Bool, String) { let greeting = "hello \(name)" return (true, greeting); } let (success, greeting) = sayHello() println("sayHello returned success:\(success) with greeting: \(greeting)"); // output // sayHello returned success:1 with greeting: hello World
Tuples have been on the wish list of many Objective-C programmers for a long time.
Another cool feature of tuples is that we can name the returned variables. If we revisit the previous example and give names to the variables of the tuple, we get the following:
func sayHello(name: String = "World") -> (success: Bool, greeting: String) { let greeting = "hello \(name)" return (true, greeting); } let status = sayHello() println("sayHello returned success:\(status.success) with greeting: \(status.greeting)"); // output // sayHello returned success:1 with greeting: hello World
This means that instead of defining a separate constant for each return element of a tuple, we can access the returned tuple elements using dot notation as shown in the above example, status.success
and status.greeting
.
Closures
Closures in Swift are the same as blocks in Objective-C. They can be defined inline, passed as a parameter, or returned by functions. We use them exactly as we would use blocks in Objective-C.
Defining closures is also easy. Actually, a function is a special case of closures. So it's no wonder that defining a closure looks a lot like defining a function.
Closures are a first-class type, which means that they can be passed and returned by functions just like any other type, such as Int
, String
, Bool
, etc. Closures are essentially code blocks that can be called later and have access to the scope in which they were defined.
Creating an nameless closure is as simple as wrapping a block of code in curly braces. The parameters and return type of the closure are separated from the closure's body with the in
keyword.
Let's say we want to define a closure that returns true
if a number is even, then that closure could look something like:
let isEven = { (number: Int) -> Bool in let mod = number % 2 return (mod==0) }
The isEven
closure takes an Int
as its single parameter and returns a Bool
. The type of this closure is (number: Int) -> Bool
, or (Int -> Bool)
for short. We can call isEven
anywhere in our code just like we would invoke a code block in Objective-C.
To pass a closure of this type as a parameter of a function, we use the closure's type in the function's definition:
let isEven = { (number: Int) -> Bool in let mod = number % 2; return (mod==0); } func verifyIfEven(number: Int, verifier:(Int->Bool)) ->Bool { return verifier(number); } verifyIfEven(12, isEven); // returns true verifyIfEven(19, isEven); // returns false
In the above example, the verifier
parameter of the verifyIfEven
function is a closure that we pass to the function.
3. Classes & Structures
Classes
It's time to talk about the cornerstone of object-oriented programming, classes. Classes, as mentioned before, are defined in a single implementation file with a .swift extension. Property declarations and methods are all defined in that file.
We create a class with the class
keyword followed by the name of the class. The class's implementation is wrapped in a pair of curly braces. As in Objective-C, the naming convention for classes is to use upper camel case for class names.
class Hotel { //properties //functions }
To create an instance of the Hotel
class we write:
let h = Hotel()
In Swift, there's no need to call init
on objects as init
is called automatically for us.
Class inheritance follows the same pattern as in Objective-C, a colon separates the class name and that of its superclass. In the following example, Hotel
inherits from the BigHotel
class.
class BigHotel: Hotel { }
As in Objective-C, we use dot notation to access an object's properties. However, Swift also uses the dot notation to invoke class and instance methods as you can see below.
// Objective-C UIView* view = [[UIView alloc] init]; [self.view addSubview:view]; // Swift let view = UIView() self.view.addSubview(view)
Properties
Another difference with Objective-C is that Swift doesn't distinguish between instance variables (ivars) and properties. An instance variable is a property.
Declaring a property is just like defining a variable or a constant, using the var
and let
keywords. The only difference is the context in which they are defined, that is, the context of a class.
class Hotel { let rooms = 10 var fullRooms = 0 }
In the above example, rooms
is an immutable value, a constant, set to 10
and fullRooms
is a variable with an initial value of 0
, which we can change later. The rule is that properties need to be initialized when they're declared. The only exception to this rule are optionals, which we'll discuss in a moment.
Computed Properties
The Swift language also defines computed properties. Computed properties are nothing more than fancy getters and setters that don't store a value. As their name indicates, they are computed or evaluated on the fly.
Below is an example of a computed property. I have changed the rooms
property to a var
for the rest of these examples. You'll find out why later.
class Hotel { var rooms = 10 var fullRooms = 0 var description: String { get { return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)" } } }
Because the description
property is read-only and only has a return
statement, we can omit the get
keyword and curly braces, and only keep the return
statement. This is shorthand and that's what I'll be using in the rest of this tutorial.
class Hotel { var rooms = 10 var fullRooms = 0 var description: String { return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)" } }
We can also define read-write computed properties. In our class Hotel
, we want an emptyRooms
property that gets the number of empty rooms in the hotel, but we also want to update fullRooms
when we set the emptyRooms
computed property. We can do this by using the set
keyword as shown below.
class Hotel { var rooms = 10 var fullRooms = 0 var description: String { return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)" } var emptyRooms :Int { get { return rooms - fullRooms } set { // newValue constant is available here // containing the passed value if(newValue < rooms) { fullRooms = rooms - newValue } else { fullRooms = rooms } } } } let h = Hotel() h.emptyRooms = 3 h.description // Size of Hotel: 10 rooms capacity:7/10
In the emptyRooms
setter, the newValue
constant is handed to us and represents the value passed to the setter. It's also important to note that computed properties are always declared as variables, using the var
keyword, because their computed value can change.
Methods
We've already covered functions earlier in this article. Methods are nothing more than functions that are tied to a type, such as a class. In the following example we implement an instance method, bookNumberOfRooms
, in the Hotel
class we created earlier.
class Hotel { var rooms = 10 var fullRooms = 0 var description: String { return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)" } var emptyRooms :Int { get { return rooms - fullRooms } set { // newValue constant is available here // containing the passed value if(newValue < rooms) { fullRooms = rooms - newValue } else { fullRooms = rooms } } } func bookNumberOfRooms(room:Int = 1) -> Bool { if(self.emptyRooms>room) { self.fullRooms++; return true } else { return false } } } let h = Hotel() h.emptyRooms = 7 h.description //Size of Hotel: 10 rooms capacity:3/10 h.bookNumberOfRooms(room: 2) // returns true h.description //Size of Hotel: 10 rooms capacity:5/10 h.bookNumberOfRoom() // returns true h.description //Size of Hotel: 10 rooms capacity:6/10
Initializers
The default initializer for classes is init
. In the init
function, we set the initial values of the instance that is created.
For example, if we would need a Hotel
subclass with 100 rooms, then we would need an initializer to set the rooms
property to 100
. Remember that I previously changed rooms
from being a constant to being a variable in the Hotel
class. The reason is that we cannot change inherited constants in a subclass, only inherited variables can be changed.
class BigHotel: Hotel { init() { super.init() rooms = 100 } } let bh = BigHotel() println(bh.description); //Size of Hotel: 100 rooms capacity:0/100
Initializers can also take parameters. The following example shows you how this works.
class CustomHotel: Hotel { init(size:Int) { super.init() rooms = size } } let c = CustomHotel(size:20) c.description //Size of Hotel: 20 rooms capacity:0/20
Overriding Methods and Computed Properties
This is one of the coolest things in Swift. In Swift, a subclass can override both methods and computed properties. To do so, we use the override
keyword. Let's override the description
computed property in the CustomHotel
class:
class CustomHotel: Hotel { init(size:Int) { super.init() rooms = size } override var description:String { return super.description + " Howdy!" } } let c = CustomHotel(size:20) c.description // Size of Hotel: 20 rooms capacity:0/20 Howdy!
The result is that description
returns the result of the superclass's description
method with the string "Howdy!"
appended to it.
What's cool about overriding methods and computed properties is the override
keyword. When the compiler sees the override
keyword, it checks wether the class's superclass implements the method that's being overridden. The compiler also checks if the properties and methods of a class are in conflict with properties or methods higher up the inheritance tree.
I don't know how many times a typo in an overridden method in Objective-C made me curse, because the code wasn't working. In Swift, the compiler will tell you exactly what's wrong in these situations.
Structures
Structures, defined with the struct
keyword, are more powerful in Swift than they are in C and Objective-C. In C, structs define only values and pointers. Swift structs are just like C structs, but they also support computed properties and methods.
Anything you can do with a class, you can do with a structure, with two important differences:
- structures don't support inheritance like classes do
- structures are passed around by value while classes are passed by reference
Here are a few examples of structures in Swift:
struct Rect { var origin: Point var size: Size var area: Double { return size.width * size.height } func isBiggerThanRect(r:Rect) -> Bool { return (self.area > r.area) } } struct Point { var x = 0 var y = 0 } struct Size { var width = 0 var height = 0 }
4. Optionals
Solution To a Problem
Optionals are a new concept if you're coming from Objective-C. They solve a problem we all face as programmers. When we access a variable whose value we're not sure about, we usually return an indicator, known as a sentinel, to indicate that the returned value is a no-value. Let me illustrate this with an example from Objective-C:
NSString* someString = @"ABCDEF"; NSInteger pos = [someString rangeOfString:@"B"].location; // pos = 1
In the above example, we are trying to find the position of @"B"
in someString
. If @"B"
is found, its location or position is stored in pos
. But what happens if @"B"
isn't found in someString
?
The documentation states that rangeOfString:
returns an NSRange
with location
set to the NSNotFound
constant. In the case of rangeOfString:
, the sentinel is NSNotFound
. Sentinels are used to indicate that the returned value is not valid.
In Cocoa, there are many uses of this concept, but the sentinel value differs from context to context, 0
, -1
, NULL
, NSIntegerMax
, INT_MAX
, Nil
, etc. The problem for the programmer is that she must remember which sentinel is used in which context. If the programmer isn't careful, she can mistake a valid value for a sentinel and vice versa. Swift solves this problem with optionals. To quote Brian Lanier "Optionals are the one sentinel to rule them all."
Optionals have two states, a nil
state, which means the optional contains no value, and a second state, which means it holds a valid value. Think of optionals as a package with an indicator to tell you if the package's contents is valid or not.
Usage
All types in Swift can become an optional. We define an optional by adding a ?
after the type declaration like so:
let someInt: Int? // someInt == nil
We assign a value to an optional's package just like we do with constants and variables.
someInt = 10 // someInt! == 10
Remember that optionals are like packages. When we declared let someInt: Int?
, we defined an empty box with a value of nil
. By assigning the value 10
to the optional, the box contains an integer that is equal to 10
and its indicator or state becomes not nil.
To get to the contents of an optional we use the !
operator. We must be sure that the optional has a valid value before unwrapping it. Failing to do so will cause a runtime error. This is how we access the value stored in an optional:
if ( someInt != nil) { println("someInt: \(someInt!)") } else { println("someInt has no value") } // someInt: 10
The above pattern is so common in Swift that we can simplify the above code block by using optional binding with the if let
keywords. Take a look at the updated code block below.
if let value = someInt { println("someInt: \(value)") } else { println("someInt has no value") }
Optionals are the only type that can take a nil
value. Constants and variables cannot be initialized or set to nil
. This is part of Swift's safety policy, all non-optional variables and constants must have a value.
5. Memory Management
If you remember, back when ARC was introduced we were using the strong
and weak
keywords to define object ownership. Swift also has a strong
and weak
ownership model, but it also introduces a new one, unowned
. Let's take a look at each object ownership model in Swift.
strong
Strong references are the default in Swift. Most of the time, we own the object that we're referencing and we are the ones responsible for keeping the referenced object alive.
Since strong references are the default, there is no need to explicitly keep a strong reference to an object, any reference is a strong reference.
weak
A weak reference in Swift indicates that the reference points to an object that we're not responsible for keeping alive. It's mainly used between two objects that don't need the other to be around in order for the object to continue its life cycle.
There is one but, however. In Swift, weak references must always be variables with an optional type, because they are set to nil
when the referenced object is deallocated. The weak
keyword is used to declare a variable as weak:
weak var view: UIView?
unowned
Unowned references are new for Objective-C programmers. An unowned reference means that we're not responsible for keeping the referenced object alive, just like weak references.
The difference with a weak reference is that an unowned reference is not set to nil
when the object it references is deallocated. Another important difference with weak references is that unowned references are defined as a non-optional type.
Unowned references can be constants. An unowned object doesn't exist without its owner and therefore the unowned reference is never nil
. Unowned references need the unowned
keyword before the definition of the variable or constant.
unowned var view: UIView
Conclusion
Swift is an amazing language that has a lot of depth and potential. It's fun to write programs with and it removes a lot of the boilerplate code we write in Objective-C to make sure our code is safe.
I highly recommend The Swift Programming Language, which is available for free in Apple's iBooks Store.
Comments