Introduction to Swift#
Swift is a new language for developing applications for iOS, macOS, watchOS, and tvOS.
Swift is a safe, fast, and interactive programming language.
Swift supports code previews (playgrounds), a feature that allows programmers to run Swift code and see results in real-time without compiling and running the application.
Swift avoids many common programming errors by adopting modern programming paradigms:
- Variables are always initialized before use.
- Checks for array index out-of-bounds errors.
- Checks for integer overflow.
- Optional values ensure that nil values are handled explicitly.
- Memory is managed automatically.
- Error handling allows recovery from unexpected failures.
Basic Section#
Constants and Variables#
Declare constants and variables; constants and variables must be declared before use, using let
to declare constants and var
to declare variables.
Example:
let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0
// Type annotation
var welcomeMessage: String
Comments#
Single-line comments use double forward slashes (//), and multi-line comments use (/* multi-line */). Swift's multi-line comments can be nested within other multi-line comments.
Example:
// This is a comment
/* This is also a comment,
but it is multi-line */
/* This is the beginning of the first multi-line comment
/* This is the second nested multi-line comment */
This is the end of the first multi-line comment */
Semicolons#
Swift does not require you to use semicolons (;) at the end of each statement.
Multiple independent statements on the same line must be separated by semicolons.
let cat = "🐱"; print(cat)
// Outputs "🐱"
Identifiers#
Identifiers are names assigned to variables, constants, methods, functions, enums, structs, classes, protocols, etc. The letters that make up an identifier have specific rules, and the naming rules for identifiers in Swift are as follows:
-
Case-sensitive; Myname and myname are two different identifiers;
-
The first character of an identifier can start with an underscore (_) or a letter, but cannot be a digit;
-
Other characters in an identifier can be underscores (_), letters, or digits.
For example: userName, User_Name, _sys_val, height, etc., are valid identifiers, while 2mail, room#, and class are invalid identifiers.
Note: The letters in Swift use Unicode encoding. Unicode is a universal encoding system that includes Asian character encodings, such as Chinese, Japanese, Korean, etc., and even the emojis we use in chat tools.
If you must use a keyword as an identifier, you can add backticks (`) before and after the keyword, for example:
let `class` = "xiaobai"
Keywords#
Keywords are reserved sequences of characters similar to identifiers, and cannot be used as identifiers unless enclosed in backticks (`). Keywords are predefined reserved identifiers that have special meaning to the compiler. Common keywords include the following four types.
Keywords related to declarations
class deinit enum extension
func import init internal
let operator private protocol
public static struct subscript
typealias var
Keywords related to statements
break case continue default
do else fallthrough for
if in return switch
where while
Expression and type keywords
as dynamicType false is
nil self Self super
true _COLUMN_ _FILE_ _FUNCTION_
_LINE_
Keywords used in specific contexts
associativity convenience dynamic didSet
final get infix inout
lazy left mutating none
nonmutating optional override postfix
precedence prefix Protocol required
right set Type unowned
weak willSet
Swift Whitespace#
Swift has specific requirements for the use of whitespace.
In Swift, operators cannot be placed directly next to variables or constants. For example, the following code will cause an error:
let a= 1 + 2
The error message is:
error: prefix/postfix '=' is reserved
This means that using the equals sign directly next to the preceding or following value is reserved.
The following code will also cause an error (continue to pay attention to whitespace):
let a = 1+ 2
The error message is:
error: consecutive statements on a line must be separated by ';'
This is because Swift thinks the statement ends at 1+, and 2 is considered the next statement.
Only writing it this way will not cause an error:
let a = 1 + 2; // Coding standards recommend using this format
let b = 3+4 // This is also OK
Integers and Floating-Point Numbers#
Using Int uniformly can improve code reusability, avoid conversions between different types of numbers, and match the type inference of numbers.
Example:
let minValue = UInt8.min // minValue is 0, of type UInt8
let maxValue = UInt8.max // maxValue is 255, of type UInt8
Type Safety and Type Inference#
Swift is a type-safe language, which means Swift allows you to know the type of a value clearly.
If you do not explicitly specify a type, Swift will use type inference to select the appropriate type (int, double).
Example:
let meaningOfLife = 42
// meaningOfLife will be inferred as Int type
let pi = 3.14159
// pi will be inferred as Double type
Numeric Literals and Numeric Type Conversion#
Example:
let decimalInteger = 17
let binaryInteger = 0b10001 // Binary 17
let octalInteger = 0o21 // Octal 17
let hexadecimalInteger = 0x11 // Hexadecimal 17
Type Aliases#
Type aliases are another name defined for existing types. You can use the typealias keyword to define a type alias.
Example:
typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0
Boolean Values#
Example:
let orangesAreOrange = true
let turnipsAreDelicious = false
Tuples#
Tuples combine multiple values into a single compound value. The values in a tuple can be of any type and do not need to be of the same type.
Example:
let http404Error = (404, "Not Found")
// http404Error's type is (Int, String), value is (404, "Not Found")
Optional Types#
Use optional types to handle situations where a value may be missing. An optional type represents two possibilities: either there is a value, and you can unwrap the optional type to access that value, or there is no value at all.
Example:
var serverResponseCode: Int? = 404
// serverResponseCode contains an optional Int value 404
serverResponseCode = nil
// serverResponseCode now contains no value
Error Handling#
Error handling addresses potential error conditions that may occur during program execution.
Example:
func makeASandwich() throws {
// ...
}
do {
try makeASandwich()
eatASandwich()
} catch SandwichError.outOfCleanDishes {
washDishes()
} catch SandwichError.missingIngredients(let ingredients) {
buyGroceries(ingredients)
}
Assertions and Preconditions#
Assertions and preconditions are checks made at runtime.
let age = -3
assert(age >= 0, "A person's age cannot be less than zero")
// Since age < 0, the assertion will trigger
Basic Operators#
Swift supports most standard C language operators and also provides range operators not found in C, such as a..<b or a...b.
Assignment operators, arithmetic operators, compound assignment operators, comparison operators, ternary operators, nil-coalescing operators, range operators, logical operators
Operators are classified as unary, binary, and ternary operators.
The closed range operator (a...b) defines a range that includes all values from a to b (including a and b).
The half-open range operator (a..<b) defines a range from a to b but does not include b.
The closed range operator has another expression form that can express ranges extending infinitely in one direction, (a..., ...b).
Example:
let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
print("Person \(i + 1) is named \(names[i])")
}
// Person 1 is named Anna
// Person 2 is named Alex
// Person 3 is named Brian
// Person 4 is named Jack
Strings and Characters#
String literals, string interpolation, counting characters, accessing and modifying strings, substrings, comparing strings
Initialize an empty string, string mutability, strings are value types, concatenating strings and characters (+, +=).
Using characters, you can iterate through a string using a for-in loop to get the value of each character in the string.
String interpolation is a way to construct a new string that can include constants, variables, literals, and expressions. You can insert constants, variables, literals, and expressions into an existing string to form a longer string.
Swift provides three ways to compare text values: string character equality, prefix equality, and suffix equality.
Example:
// Multi-line string literal
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
// The following two strings are actually the same
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
// String interpolation
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
// Counting characters
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in cafe is 4"
var emptyString = "" // Empty string literal
var anotherEmptyString = String() // Initialization method
// Both strings are empty and equivalent.
let catCharacters: [Character] = ["C", "a", "t", "!"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!"
Collection Types#
The Swift language provides three basic collection types: arrays (Array), sets (Set), and dictionaries (Dictionary) for storing collection data. An array is a collection of ordered data. A set is a collection of unordered, unique data. A dictionary is a collection of unordered key-value pairs.
Mutability of collections, arrays (Arrays), sets (Sets), collection operations, dictionaries
Arrays use an ordered list to store multiple values of the same type. The same value can appear multiple times at different positions in an array.
Sets are used to store values of the same type without a defined order. When the order of elements in a set is not important or when you want to ensure that each element appears only once, you can use a set instead of an array.
Collection operations can efficiently perform some basic operations on collections, such as combining two collections, determining common elements between two collections, or checking whether two collections are entirely contained, partially contained, or not intersecting.
Dictionaries are an unordered collection that stores relationships between key-value pairs, where all keys must be of the same type and all values must also be of the same type. Each value (value) is associated with a unique key (key), which serves as the identifier for that value's data in the dictionary.
Example:
// Collection
var someInts = [Int]()
print("someInts is of type [Int] with \(someInts.count) items.")
// Prints "someInts is of type [Int] with 0 items."
var threeDoubles = Array(repeating: 0.0, count: 3)
// threeDoubles is a [Double] array, equivalent to [0.0, 0.0, 0.0]
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles is inferred as [Double], equivalent to [2.5, 2.5, 2.5]
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as [Double], equivalent to [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
// The enumerated() method iterates through the array
var shoppingList: [String] = ["Eggs", "Milk"]
for (index, value) in shoppingList.enumerated() {
print("Item \(String(index + 1)): \(value)")
}
Control Flow#
For-In loops, While loops (Repeat-While), Conditional statements, Control transfer statements, Early exit (guard), Checking API availability
Like if statements, the execution of guard depends on the boolean value of an expression. We can use guard statements to require that conditions must be true to execute the code following the guard statement. Unlike if statements, a guard statement always has an else clause, which executes the code in the else clause if the condition is not true.
Swift has built-in support for checking API availability, and the compiler uses available information in the SDK to verify that all APIs used in our code are available on the project's specified deployment target. If we attempt to use an unavailable API, Swift will throw an error at compile time.
Example:
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
print("\(animalName)s have \(legCount) legs")
}
// General format of repeat-while loop
repeat {
statements
} while condition
// Early exit
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."
Functions#
Function definition and calling, function parameters and return values, function parameter labels and parameter names, function types, nested functions
Optional tuple return types.
When defining an input-output parameter, add the inout keyword before the parameter definition.
Example:
// Function
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill! Glad you could visit from Cupertino."
// Optional tuple return type
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
// Implicitly returned function
func greeting(for person: String) -> String {
"Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// Prints "Hello, Dave!"
// Parameter labels
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill! Glad you could visit from Cupertino."
Closures#
Closures are self-contained blocks of function code that can be passed around and used in your code. They are similar to anonymous functions (Lambdas) in some programming languages.
Closure expressions, trailing closures, value capturing, closures as reference types, escaping closures (@escaping), auto closures
If you need to pass a long closure expression as the last parameter to a function, it is useful to replace this closure with a trailing closure form.
A closure can capture constants or variables from its defining context. Even if the original scope that defined these constants and variables no longer exists, the closure can still reference and modify these values within its body.
Example:
// Closure expression syntax
{ (parameters) -> return type in
statements
}
// Trailing closure
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings is inferred as an array of strings, i.e., [String]
// Its value is ["OneSix", "FiveEight", "FiveOneZero"]
// Value capturing
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
// Auto closure, lazy evaluation
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
Enums#
Use the enum
keyword to create an enumeration and place the entire definition within a pair of braces.
Enum syntax, using switch statements to match enum values, iterating enum members, associated values, raw values (default values), recursive enums (indirect)
You can define Swift enums to store associated values of any type, and each enum member can have a different associated value type.
Example:
// Enum syntax
enum SomeEnumeration {
// Enum definitions go here
}
enum CompassPoint {
case north
case south
case east
case west
}
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Prints "Mostly harmless"
// Associated values
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
// Recursive enums
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
// (5 + 4) * 2
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product))
// Prints "18"
Structs and Classes#
Comparison of structs and classes, structs and enums are value types, classes are reference types
Structs and classes serve as a general and flexible structure, forming the basis for building code. You can define properties and add methods to your structs and classes using the syntax for defining constants, variables, and functions.
Example:
// Classes and structs
struct SomeStructure {
// Define the struct here
}
class SomeClass {
// Define the class here
}
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
Properties#
Stored properties, computed properties, property observers, property wrappers, global variables and local variables, type properties (static)
Properties associate values with specific classes, structs, or enums. Stored properties store constants and variables as part of an instance, while computed properties directly compute (rather than store) values. Computed properties can be used with classes, structs, and enums, while stored properties can only be used with classes and structs.
Property observers monitor and respond to changes in property values, and property observers are called every time a property is set a value, even if the new value is the same as the current value.
- willSet is called before the new value is set
- didSet is called after the new value is set
Property wrappers add a layer of separation between managing how properties are stored and defining the code for the properties.
Type properties are also accessed using the dot operator. However, type properties are accessed through the type itself rather than through an instance.
Example:
// Properties
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size() // Stored property
var center: Point { // Computed property
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"
// Property wrappers
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
Methods#
Instance methods, type methods (static)
Methods are functions associated with specific types.
Classes, structs, and enums can define instance methods; instance methods encapsulate specific tasks and functionalities for a given type.
Classes, structs, and enums can also define type methods; type methods are associated with the type itself.
Example:
// Methods
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
Subscripts#
Subscripts can be defined in classes, structs, and enums, providing a shortcut for accessing elements in a collection, list, or sequence.
Subscript syntax (subscript), subscript usage, subscript options, type subscripts (static)
subscript(index: Int) -> Int {
get {
// Return an appropriate Int type value
}
set(newValue) {
// Perform appropriate assignment operation
}
}
// Example
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18"
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2
// Type subscripts
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
static subscript(n: Int) -> Planet {
return Planet(rawValue: n)!
}
}
let mars = Planet[4]
print(mars)
Inheritance#
Defining a base class, subclass generation, overriding (override), preventing overriding (final)
A class that does not inherit from other classes is called a base class.
Example:
// Inheritance
class SomeClass: SomeSuperclass {
// Here is the subclass definition
}
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
// Do nothing—because vehicles do not necessarily make noise
}
}
class Car: Vehicle {
var gear = 1
override var description: String {
return super.description + " in gear \(gear)"
}
}
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}
Initialization Process#
The initialization process is the preparation process before using an instance of a class, struct, or enum.
Initial assignment of stored properties, custom initialization process, default initializer, initializer delegation for value types, class inheritance and initialization process, failable initializers, required initializers
Initializers can complete part of the initialization process of an instance by calling other initializers. This process is called initializer delegation, which avoids code duplication between multiple initializers.
Swift provides two initializers for class types to ensure that all stored properties in an instance receive initial values, called designated initializers and convenience initializers.
You can add one or more failable initializers to the definition of a class, struct, or enum. The syntax is to add a question mark after the init keyword (init?).
A required initializer indicates that all subclasses of that class must implement that initializer by adding the required modifier before the class's initializer.
Example:
// Initialization process
init() {
// Perform initialization here
}
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"
struct Color {
let red, green, blue: Double
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
}
Deinitialization Process#
Deinitializers are only applicable to class types, and a deinitializer is called immediately before an instance of a class is released. Deinitializers are marked with the keyword deinit, similar to how initializers are marked with init.
Swift automatically releases instances that are no longer needed to free up resources.
Example:
// Deinitialization process
deinit {
// Perform deinitialization here
}
class Bank {
static var coinsInBank = 10_000
static func distribute(coins numberOfCoinsRequested: Int) -> Int {
let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receive(coins: Int) {
coinsInBank += coins
}
}
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.distribute(coins: coins)
}
func win(coins: Int) {
coinsInPurse += Bank.distribute(coins: coins)
}
deinit {
Bank.receive(coins: coinsInPurse)
}
}
Optional Chaining#
Optional chaining is a method that allows you to request and call properties, methods, and subscripts on an optional value that might currently be nil.
You can define an optional chain by placing a question mark (?) after the optional value of the property, method, or subscript you want to call, similar to placing an exclamation mark (!) after an optional value to force unwrap its value. The main difference is that when an optional value is nil, optional chaining will simply fail the call, while force unwrapping will trigger a runtime error.
Example:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
let john = Person()
let roomCount = john.residence!.numberOfRooms
// This will trigger a runtime error
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."
john.residence = Residence()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."
Error Handling#
Error handling is the process of responding to errors and recovering from them. Swift provides first-class support for throwing, catching, propagating, and manipulating recoverable errors at runtime.
Representing and throwing errors, handling errors, specifying cleanup actions
In Swift, errors are represented by values of types that conform to the Error protocol.
There are four ways to handle errors in Swift. You can propagate errors thrown by functions to the code that calls that function (throws), handle errors with do-catch statements, handle errors as optional types (try?), or assert that an error will not occur (try!).
The defer statement delays the execution of code until just before the current scope exits.
Example:
// Error handling
enum VendingMachineError: Error {
case invalidSelection // Invalid selection
case insufficientFunds(coinsNeeded: Int) // Insufficient funds
case outOfStock // Out of stock
}
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
// Specifying cleanup actions
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Process the file.
}
// close(file) will be called here, i.e., at the end of the scope.
}
}
Type Casting#
Type casting in Swift is implemented using the is and as operators. These two operators provide a clear way to check the type of a value or convert its type.
Defining class hierarchies for type casting, checking types (is), downcasting (as? or as!), type casting for Any and AnyObject
Type casting can be used on class and subclass hierarchies to check the type of a specific class instance and convert that class instance's type to other types within that hierarchy.
Swift provides two special type aliases for uncertain types:
-
Any can represent any type, including function types.
-
AnyObject can represent an instance of any class type.
Example:
// Type casting
// A base class MediaItem
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
let library = [
Movie(name: "Casablanca", director: "Micheal Curtiz"),
Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
Movie(name: "Citizen Kane", director: "Orson Wells"),
Song(name: "The One And Only", artist: "Chesney Hawkes"),
Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
}
}
print("Media library contains \(movieCount) movies and \(songCount)")
// Prints "Media library contains 2 movies and 3 songs"
for item in library {
if let movie = item as? Movie {
print("Movie: \(movie.name), dir. \(movie.director)")
} else if let song = item as? Song {
print("Song: \(song.name), by \(song.artist)")
}
}
// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley
Nested Types#
Swift allows you to define nested types, which can include nested enums, classes, and structs within supported types.
Practicing nested types, referencing nested types
To nest one type within another, write the definition of the nested type inside the {} of its outer type, and you can define multiple levels of nesting as needed.
Example:
// Nested types
struct BlackjackCard {
// Nested Suit enum
enum Suit: Character {
case spades = "1", hearts = "2", diamonds = "3", clubs = "4"
}
// Nested Rank enum
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .ace:
return Values(first: 1, second: 11)
case .jack, .queen, .king:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}
// BlackjackCard's properties and methods
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// Prints "theAceOfSpades: suit is 1, value is 1 or 11"
let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// 2
Extensions#
Extensions can add new functionality to an existing class, struct, enum, or protocol.
Extension syntax, computed properties, initializers, methods, subscripts, nested types
Extensions in Swift can:
-
Add computed instance properties and computed type properties
-
Define instance methods and type methods
-
Provide new initializers
-
Define subscripts
-
Define and use new nested types
-
Make existing types conform to a protocol
Extension syntax:
extension SomeType {
// Add new functionality to SomeType here
}
Extensions can add computed instance properties and computed type properties to existing types.
Extensions can add new initializers to existing types.
Extensions can add new instance methods and type methods to existing types.
Extensions can add new subscripts to existing types.
Extensions can add new nested types to existing classes, structs, and enums.
Example:
// Extension syntax
extension SomeType {
// Add new functionality to SomeType here
}
// Adding one or more protocols
extension SomeType: SomeProtocol, AnotherProtocol {
// Implementations required by the protocols go here
}
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 3)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
3.repetitions {
print("Hello!")
}
// Hello!
// Hello!
// Hello!
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt is now 9
Protocols#
Protocols define a blueprint that specifies methods, properties, and other requirements for implementing a specific task or functionality.
Classes, structs, or enums can conform to protocols and provide concrete implementations for the requirements defined by the protocol.
Protocol syntax, property requirements, method requirements, mutating method requirements, initializer requirements, protocols as types, delegation, protocol type collections, protocol inheritance, class-only protocols, protocol composition, checking protocol conformance, optional protocol requirements, protocol extensions,
Protocol syntax:
protocol SomeProtocol {
// This is the definition part of the protocol
}
Protocols can require conforming types to provide specific instance properties or type properties with specific names and types.
Protocols can require conforming types to implement certain specified instance methods or class methods.
In instance methods of value types (i.e., structs and enums), the mutating keyword is used as a prefix before the func keyword to indicate that the method can modify the instance it belongs to and any properties of that instance.
Delegation is a design pattern that allows classes or structs to delegate some functionality they are responsible for to instances of other types.
Example:
// Protocol syntax
protocol SomeProtocol {
// This is the definition part of the protocol
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
// This is the definition part of the struct
}
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// This is the definition part of the class
}
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
protocol FullyNamed {
var fullName: String { get }
}
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
Generics#
Generic code allows you to write flexible and reusable functions and types that can work with any type based on your custom requirements.
You can avoid writing duplicate code and express the intent of your code in a clear and abstract way.
Generic functions, type parameters, named type parameters, generic types, generic extensions, type constraints, associated types
Example:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", anotherString is now "hello"
Opaque Types#
Functions or methods with opaque return types hide the type information of the return value.
Functions no longer provide a specific type as the return type but describe the return value based on the protocol it supports.
Problems solved by opaque types, returning opaque types, differences between opaque types and protocol types
Hiding type information is very useful when dealing with the relationship between modules and calling code, as the underlying data type returned can remain private.
Opaque types are the opposite of generics. Opaque types allow a function to choose a return type that is independent of the calling code.
If there are multiple places in a function that return an opaque type, all possible return values must be of the same type. The main difference between returning an opaque type and returning a protocol type is whether type consistency is guaranteed.
An opaque type can correspond to only one specific type, even though the function caller does not know which type it is; a protocol type can correspond to multiple types as long as they conform to the same protocol.
Example:
protocol Shape {
func draw() -> String
}
struct Triangle: Shape {
var size: Int
func draw() -> String {
var result = [String]()
for length in 1...size {
result.append(String(repeating: "*", count: length))
}
return result.joined(separator: "\n")
}
}
let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
// *
// **
// ***
struct FlippedShape<T: Shape>: Shape {
var shape: T
func draw() -> String {
let lines = shape.draw().split(separator: "\n")
return lines.reversed().joined(separator: "\n")
}
}
let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
// ***
// **
// *
Automatic Reference Counting#
Swift uses Automatic Reference Counting (ARC) to track and manage memory in your application.
This situation occurs when two class instances hold strong references to each other, causing each instance to keep the other alive. This is known as a strong reference cycle.
Swift provides two ways to resolve strong reference cycle issues you encounter when using class properties: weak references (weak reference) and unowned references (unowned reference).
-
When declaring a property or variable, prefix it with the weak keyword to indicate that it is a weak reference.
-
When declaring a property or variable, prefix it with the unowned keyword to indicate that it is an unowned reference.
Example:
// Automatic Reference Counting practice
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"
// Strong reference cycle
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john = nil
unit4A = nil
// Weak reference
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
// Prints "John Appleseed is being deinitialized"
Memory Safety#
By default, Swift prevents unsafe behavior in your code.
Understanding memory access conflicts, access conflicts with in-out parameters, access conflicts with self in methods, access conflicts with properties.
Example:
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore) // Normal
balance(&playerOneScore, &playerOneScore)
// Error: playerOneScore access conflict
Access Control#
Access control can restrict access to your code from other source files or modules.
-
The open and public levels allow entities to be accessed by all entities in the same module source file and also from outside the module by importing that module. Typically, you would use open or public levels to specify the external interface of a framework.
-
The internal level allows entities to be accessed by any entity in the same module source file but not by entities outside the module. Typically, if an interface is only used internally within an application or framework, it can be set to internal level.
-
The fileprivate level restricts access to entities only within the file they are defined in. If part of a feature's implementation details only needs to be used within a file, you can use fileprivate to hide it.
-
The private level restricts access to entities only within their defining scope and extensions within the same file. If part of a feature's details only needs to be used within the current scope, you can use private to hide it.
Open is the highest access level (least restrictive), and private is the lowest access level (most restrictive).
Open can only apply to classes and class members, and the main difference between open and public is that classes and members defined as open can be inherited and overridden outside the module.
Example:
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}
class SomeInternalClass {} // Implicitly internal
var someInternalConstant = 0 // Implicitly internal
public class SomePublicClass { // Explicit public class
public var somePublicProperty = 0 // Explicit public class member
var someInternalProperty = 0 // Implicitly internal class member
fileprivate func someFilePrivateMethod() {} // Explicit fileprivate class member
private func somePrivateMethod() {} // Explicit private class member
}
class SomeInternalClass { // Implicitly internal class
var someInternalProperty = 0 // Implicitly internal class member
fileprivate func someFilePrivateMethod() {} // Explicit fileprivate class member
private func somePrivateMethod() {} // Explicit private class member
}
fileprivate class SomeFilePrivateClass { // Explicit fileprivate class
func someFilePrivateMethod() {} // Implicitly fileprivate class member
private func somePrivateMethod() {} // Explicit private class member
}
private class SomePrivateClass { // Explicit private class
func somePrivateMethod() {} // Implicitly private class member
}
Advanced Operators#
Swift also provides several advanced operators that can perform complex operations on values. These include bitwise operators and shift operators.
Bitwise operators, overflow operators, precedence and associativity, operator functions, custom operators
Example:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // Equals 0b11110000
var potentialOverflow = Int16.max
// potentialOverflow's value is 32767, which is the maximum integer Int16 can hold
potentialOverflow += 1
// This will cause an error
struct Vector2D {
var x = 0.0, y = 0.0
}
extension Vector2D {
static func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector is a new Vector2D instance with values (5.0, 5.0)