-
Notifications
You must be signed in to change notification settings - Fork 472
Swift Intermediate
This guide covers the more elegant features of the language. It is recommended for after competency in the basic rules and syntax.
This guide is designed to be concise. For in-depth details, check out the official documentation:
AnyObject
is an ambiguous type that can represent any object. An even more ambiguous type, Any
, covers non-class objects such as Structs and Enums.
var myArray: [AnyObject] = Array()
myArray.append(Dog()) // Legal
myArray.append(Homework.inProgress) // Illegal. An enum is not a class instance
While strong typing is preferred, sometimes it's unavoidable. For instance, a JSON dictionary can have mixed types of values.
To check an object's type, use is
if userDictionary["bio"] is String {
print("The bio is present")
} else if userDictionary["bio"] is NSNull {
print("The bio is not available.")
}
Use as
to downcast:
let bio = userDictionary["bio"] as String
It's safer to downcast with as?
, which returns an optional, in case the downcast fails.
if let bio = userDictionary["bio"] as? String {
print("Bio: \(bio)")
} else {
print("Bio is unavailable.")
}
To write functions that accept multiple types without losing type information, use Generics, which are covered in Advanced Swift.
If you have a chain of properties, rather than unwrap each individually, use the ?
operator.
var myValue: Int? = nil
if object != nil && object!.childObject != nil {
myValue = object!.childObject!.method()
}
// Equivalent
var myValue: Int? = object?.childObject?.method()
If any part of the chain is nil
, it stops evaluating and returns nil
, ignoring everything to the right.
You can declare an optional that automatically unwraps itself, using the !
postfix.
var i: Int? = 1
if i != nil {
i = i! + 1
}
// Equivalent
var j: Int! = 1
if j != nil {
j = j + 1
}
This is more dangerous than explicit unwrapping, but useful when dealing with values that will only briefly be nil
.
A frequent pattern is, "If the optional is nil, use this other value." For brevity, use the ??
operator. The following two lines are equivalent:
a != nil ? a! : b
a ?? b
Normally, you put initialization code in your init()
override. However, some classes have designated initializers, which are the official way to instance a class. For instance, on UIView, you should override init(frame:)
to perform your initialization.
Classes can have multiple designated initializers. For instance, init(coder:)
is a designated initializer used when an object is unarchived.
Designated initializers are allowed to call "up", to a super initializer. Convenience initializers are only allowed to call "across" their class, to a designated initializer.
class Dog: Animal {
var name: String = ""
convenience init(randomName: Bool){
self.init()
if randomName {
self.name = generateRandomName()
}
}
}
Swift tries to only inherit initializers when it's safe to do so, which can be confusing. For instance, you commonly override UIViewController's designated initializer to perform setup logic. If you just added this code, you would get a compilation error:
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.edgesForExtendedLayout = [] // This used to be UIRectEdge.None
}
This is because by overriding this initializer, you have lost the method init(coder:)
, which is the other designated initializer, and used when the view controller is loaded from a Storyboard.
The rules of initializer inheritance are:
- If you don't add a designated initializer, you inherit all superclass designated initializers.
- If you do add a designated initializer, you must provide all designated initializers to inherit the convenience initializers.
Lazy properties are instanced the first time they're used.
class MailAccount {
lazy var inbox = LoadInboxFromDisk()
}
If an enum, class, or struct is only used within a certain context, such as one particular class, it can be convenient to nest their declaration to namespace it.
class Homework {
var status: State = .inProgress
enum State {
case inProgress
case submitted
}
}
array.sort({(a: String, b: String) -> Bool in
return a < b
})
The types can be removed thanks to type inference:
array.sort({ a, b in
return a < b
})
This pattern is so common, one line closures have an implied return
statement:
array.sort({ a, b in a < b })
This can be edited further with implicit argument names:
array.sort({ $0 < $1 })
Finally, If the last parameter to a method is a closure, it can be written outside the parenthesis, as a trailing closure.
array.sort { $0 < $1 }
Structs may have methods, computed properties, initializers, and they can adhere to protocols.
However, structs are not classes. There is no subclassing. They are passed by value, while classes are passed by reference. While they have an initializer, there is no deinitializer.
Other than for the need for inheritance, if two object instances are identical and interchangeable, use a struct, otherwise, use a Class.
struct Rect {
var origin: Point
var size: Size
var area: Double {
return size.width * size.height
}
}
To obey the rules of var
vs let
, mutating methods must use the mutating
keyword:
struct Person {
var age: Int
mutating func growOlder(){
age += 1
}
}
Enums may also have methods and conform to protocols.
enum HomeworkState {
case inProgress
case submitted
init(){
self = .inProgress
}
func description() -> String {
switch self {
case .inProgress:
return "In Progress"
case .submitted:
return "Submitted"
}
}
}
let state = HomeworkState()
print(state.description()) // "In Progress"
Enums can store associated values:
enum HomeworkState {
case inProgress
case submitted
case graded(Int)
}
let homework = HomeworkState.graded(100)
Like classes, you can extend structs, enums, and even primitives like Int
extension Int {
func even() -> Bool {
return self % 2 == 0
}
}
2.even() // true
Pattern matching allows a concise way to filter values, and bind those values to constants.
switch statusCode {
case 200...299:
print("Success")
default:
print("Error")
}
They can also match on variations of tuples:
let point = (0, 10)
switch point {
case (0, 0):
print("Origin")
case (let x, 0):
print("X-Axis:\(x)")
case (0, let y):
print("Y-Axis: \(y)")
case (let x, let y):
print("Off axis: \(x), \(y)")
}
// "Y-Axis: 10"
They can be applied to enum associated values:
switch homeworkStatus {
case .inProgress:
print("In progress")
case .submitted:
print("Submitted")
case .graded(60 ..< 70):
print("Received a D")
case .graded(70 ..< 80):
print("Received a C")
case .graded(80 ..< 90):
print("Received a B")
case .graded(90 ... 100):
print("Aced it!")
case .graded(let grade):
print("Failed, with a \(grade)")
}
It is possible to match on type using is
, or match and bind using as
.
func makeAnimalDoSomething(animal: Animal){
switch animal {
case let dog as Dog:
dog.wagTail()
case let bird as Bird:
bird.flapWings()
default:
animal.eat()
}
}
Like Objective-C, Swift uses reference counting for memory management, built on ARC.
As such, you must use weak references to avoid reference cycles.
class MailFolder {
weak var sendingAccount: Account?
}
For references up the object graph, it can be more convenient to use unowned
references, which act like weak implicitly unwrapped optionals. However, only use unowned
references when you're sure the parent will exist for the lifetime of the child.
class Parent {
var child: Child!
init(){
child = Child(parent: self)
child.parent = self
}
}
class Child {
unowned var parent: Parent
init(parent: Parent){
self.parent = parent
}
}