An Overview of Swift 3 Functions and Closures
Previous | Table of Contents | Next |
The Swift Switch Statement | The Basics of Object Oriented Programming in Swift |
Learn SwiftUI and take your iOS Development to the Next Level |
Swift functions, methods and closures are a vital part of writing well-structured and efficient code and provide a way to organize programs while avoiding code repetition. In this chapter we will look at how functions, methods and closures are declared and used within Swift.
What is a Function?
A function is a named block of code that can be called upon to perform a specific task. It can be provided data on which to perform the task and is capable of returning results to the code that called it. For example, if a particular arithmetic calculation needs to be performed in a Swift program, the code to perform the arithmetic can be placed in a function. The function can be programmed to accept the values on which the arithmetic is to be performed (referred to as parameters) and to return the result of the calculation. At any point in the program code where the calculation is required the function is simply called, parameter values passed through as arguments and the result returned.
The terms parameter and argument are often used interchangeably when discussing functions. There is, however, a subtle difference. The values that a function is able to accept when it is called are referred to as parameters. At the point that the function is actually called and passed those values, however, they are referred to as arguments.
What is a Method?
A method is essentially a function that is associated with a particular class, structure or enumeration. If, for example, you declare a function within a Swift class (a topic covered in detail in the chapter entitled The Basics of Object Oriented Programming in Swift), it is considered to be a method. Although the remainder of this chapter refers to functions, the same rules and behavior apply equally to methods unless otherwise stated.
Learn SwiftUI and take your iOS Development to the Next Level |
How to Declare a Swift Function
A Swift function is declared using the following syntax:
func <function name> (<para name>: <para type>, <para name>: <para type>, ... ) -> <return type> { // Function code }
This combination of function name, parameters and return type are referred to as the function signature. Explanations of the various fields of the function declaration are as follows:
• func – The prefix keyword used to notify the Swift compiler that this is a function.
• <function name> - The name assigned to the function. This is the name by which the function will be referenced when it is called from within the application code.
• <para name> - The name by which the parameter is to be referenced in the function code.
• <para type> - The type of the corresponding parameter.
• <return type> - The data type of the result returned by the function. If the function does not return a result then no return type is specified.
• Function code - The code of the function that does the work.
As an example, the following function takes no parameters, returns no result and simply displays a message:
func sayHello() { print("Hello") }
Learn SwiftUI and take your iOS Development to the Next Level |
func buildMessageFor(name: String, count: Int) -> String { return("\(name), you are customer number \(count)") }
Calling a Swift Function
Once declared, functions are called using the following syntax:
<function name> (<arg1>, <arg2>, ... )
Each argument passed through to a function must match the parameters the function is configured to accept. For example, to call a function named sayHello that takes no parameters and returns no value, we would write the following code:
sayHello()
Handling Return Values
To call a function named buildMessage that takes two parameters and returns a result, on the other hand, we might write the following code:
let message = buildMessageFor(name: "John", count: 100)
In the above example, we have created a new variable called message and then used the assignment operator (=) to store the result returned by the function.
When developing in Swift, situations may arise where the result returned by a method or function call is not used. When this is the case, the return value may be discarded by assigning it to ‘_’. For example:
_ = buildMessageFor(name: "John", count: 100)
Local and External Parameter Names
When the preceding example functions were declared, they were configured with parameters that were assigned names which, in turn, could be referenced within the body of the function code. When declared in this way, these names are referred to as local parameter names.
In addition to local names, function parameters may also have external parameter names. These are the names by which the parameter is referenced when the function is called. By default, function parameters are assigned the same local and external parameter names. Consider, for example, the previous call to the buildMessageFor method:
let message = buildMessageFor(name: "John", count: 100)
As declared, the function uses “name” and “count” as both the local and external parameter names.
The default external parameter names assigned to parameters may be removed by preceding the local parameter names with an underscore (_) character as follows:
func buildMessageFor(_ name: String, _ count: Int) -> String { return("\(name), you are customer number \(count)") }
Learn SwiftUI and take your iOS Development to the Next Level |
let message = buildMessageFor("John", 100)
Alternatively, external parameter names can be added simply by declaring the external parameter name before the local parameter name within the function declaration. In the following code, for example, the external names of the first and second parameters have been set to “username” and “usercount” respectively:
func buildMessageFor(username name: String, usercount count: Int) -> String { return("\(name), you are customer number \(count)") }
When declared in this way, the external parameter name must be referenced when calling the function:
let message = buildMessageFor(username: "John", usercount: 100)
Regardless of the fact that the external names are used to pass the arguments through when calling the function, the local names are still used to reference the parameters within the body of the function. It is important to also note that when calling a function using external parameter names for the arguments, those arguments must still be placed in the same order as that used when the function was declared.
Declaring Default Function Parameters
Swift provides the ability to designate a default parameter value to be used in the event that the value is not provided as an argument when the function is called. This simply involves assigning the default value to the parameter when the function is declared. When using default parameters, it is important that the parameters for which a default is being declared be placed at the end of the parameter list so that the compiler does not become confused about which parameters have been omitted during a function call. Swift also provides a default external name based on the local parameter name for defaulted parameters (unless one is already provided) which must then be used when calling the function.
To see default parameters in action the buildMessageFor function will be modified so that the string “Customer” is used as a default in the event that a customer name is not passed through as an argument:
func buildMessageFor(_ name: String = "Customer", count: Int ) -> String { return ("\(name), you are customer number \(count)") }
Learn SwiftUI and take your iOS Development to the Next Level |
let message = buildMessageFor(count: 100) print(message)
When executed, the above function call will generate output to the console panel which reads:
Customer, you are customer 100
Returning Multiple Results from a Function
A function can return multiple result values by wrapping those results in a tuple. The following function takes as a parameter a measurement value in inches. The function converts this value into yards, centimeters and meters, returning all three results within a single tuple instance:
func sizeConverter (_ length: Float) -> (yards: Float, centimeters: Float, meters: Float) { let yards = length * 0.0277778 let centimeters = length * 2.54 let meters = length * 0.0254 return (yards, centimeters, meters) }
The return type for the function indicates that the function returns a tuple containing three values named yards, centimeters and meters respectively, all of which are of type Float:
-> (yards: Float, centimeters: Float, meters: Float)
Having performed the conversion, the function simply constructs the tuple instance and returns it.
Usage of this function might read as follows:
let lengthTuple = sizeConverter(20) print(lengthTuple.yards) print(lengthTuple.centimeters) print(lengthTuple.meters)
Variable Numbers of Function Parameters
It is not always possible to know in advance the number of parameters a function will need to accept when it is called within application code. Swift handles this possibility through the use of variadic parameters. Variadic parameters are declared using three periods (…) to indicate that the function accepts zero or more parameters of a specified data type. Within the body of the function, the parameters are made available in the form of an array object. The following function, for example, takes as parameters a variable number of String values and then outputs them to the console panel:
func displayStrings(_ strings: String...) { for string in strings { print(string) } } displayStrings("one", "two", "three", "four")
Parameters as Variables
All parameters accepted by a function are treated as constants by default. This prevents changes being made to those parameter values within the function code. If changes to parameters need to be made within the function body, therefore, shadow copies of those parameters must be created. The following function, for example, is passed length and width parameters in inches, creates shadow variables of the two values and converts those parameters to centimeters before calculating and returning the area value:
func calcuateArea(length: Float, width: Float) -> Float { var length = length var width = width length = length * 2.54 width = width * 2.54 return length * width } print(calcuateArea(length: 10, width: 20))
Learn SwiftUI and take your iOS Development to the Next Level |
Working with In-Out Parameters
When a variable is passed through as a parameter to a function, we now know that the parameter is treated as a constant within the body of that function. We also know that if we want to make changes to the parameter value we have to create a shadow copy as outlined in the above section. Since this is a copy, any changes made to the variable are not, by default, reflected in the original variable. Consider, for example, the following code:
var myValue = 10 func doubleValue (_ value: Int) -> Int { var value = value value += value return(value) } print("Before function call myValue = \(myValue)") print("doubleValue call returns \(doubleValue(myValue))") print("After function call myValue = \(myValue)")
The code begins by declaring a variable named myValue initialized with a value of 10. A new function is then declared which accepts a single integer parameter. Within the body of the function, a shadow copy of the value is created, doubled and returned.
The remaining lines of code display the value of the myValue variable before and after the function call is made. When executed, the following output will appear in the console:
Before function call myValue = 10 doubleValue call returns 20 After function call myValue = 10
Clearly, the function has made no change to the original myValue variable. This is to be expected since the mathematical operation was performed on a copy of the variable, not the myValue variable itself.
In order to make any changes made to a parameter persist after the function has returned, the parameter must be declared as an in-out parameter within the function declaration. To see this in action, modify the doubleValue function to include with the inout keyword, and remove the creation of the shadow copy as follows:
func doubleValue (_ value: inout Int) -> Int { value += value return(value) }
Finally, when calling the function, the inout parameter must now be prefixed with an & modifier:
print("doubleValue call returned \(doubleValue(&myValue))")
Having made these changes, a test run of the code should now generate output clearly indicating that the function modified the value assigned to the original myValue variable:
Learn SwiftUI and take your iOS Development to the Next Level |
Before function call myValue = 10 doubleValue call returns 20 After function call myValue = 20
Functions as Parameters
An interesting feature of functions within Swift is that they can be treated as data types. It is perfectly valid, for example, to assign a function to a constant or variable as illustrated in the declaration below:
func inchesToFeet (_ inches: Float) -> Float { return inches * 0.0833333 } let toFeet = inchesToFeet
The above code declares a new function named inchesToFeet and subsequently assigns that function to a constant named toFeet. Having made this assignment, a call to the function may be made using the constant name instead of the original function name:
let result = toFeet(10)
On the surface this does not seem to be a particularly compelling feature. Since we could already call the function without assigning it to a constant or variable data type it does not seem that much has been gained.
The possibilities that this feature offers become more apparent when we consider that a function assigned to a constant or variable now has the capabilities of many other data types. In particular, a function can now be passed through as an argument to another function, or even returned as a result from a function.
Before we look at what is, essentially, the ability to plug one function into another, it is first necessary to explore the concept of function data types. The data type of a function is dictated by a combination of the parameters it accepts and the type of result it returns. In the above example, since the function accepts a floating point parameter and returns a floating point result, the function’s data type conforms to the following:
(Float) -> Float
A function which accepts an Int and a Double as parameters and returns a String result, on the other hand, would have the following data type:
(Int, Double) -> String
In order to accept a function as a parameter, the receiving function simply declares the data type of the function it is able to accept.
For the purposes of an example, we will begin by declaring two unit conversion functions and assigning them to constants:
func inchesToFeet (_ inches: Float) -> Float { return inches * 0.0833333 } func inchesToYards (_ inches: Float) -> Float { return inches * 0.0277778 } let toFeet = inchesToFeet let toYards = inchesToYards
The example now needs an additional function, the purpose of which is to perform a unit conversion and print the result in the console panel. This function needs to be as general purpose as possible, capable of performing a variety of different measurement unit conversions. In order to demonstrate functions as parameters, this new function will take as a parameter a function type that matches both the inchesToFeet and inchesToYards function data type together with a value to be converted. Since the data type of these functions is equivalent to (Float) -> Float, our general purpose function can be written as follows:
func outputConversion(_ converterFunc: (Float) -> Float, value: Float) { let result = converterFunc(value) print("Result of conversion is \(result)") }
When the outputConversion function is called, it will need to be passed a function matching the declared data type. That function will be called to perform the conversion and the result displayed in the console panel. This means that the same function can be called to convert inches to both feet and yards, simply by “plugging in” the appropriate converter function as a parameter. For example:
outputConversion(toYards, value: 10) // Convert to Yards outputConversion(toFeet, value: 10) // Convert to Inches
Functions can also be returned as a data type simply by declaring the type of the function as the return type. The following function is configured to return either our toFeet or toYards function type (in other words a function which accepts and returns a Float value) based on the value of a Boolean parameter:
func decideFunction (_ feet: Bool) -> (Float) -> Float { if feet { return toFeet } else { return toYards } }
Learn SwiftUI and take your iOS Development to the Next Level |
Closure Expressions
Having covered the basics of functions in Swift it is now time to look at the concept of closures and closure expressions. Although these terms are often used interchangeably there are some key differences.
Closure expressions are self-contained blocks of code. The following code, for example, declares a closure expression and assigns it to a constant named sayHello and then calls the function via the constant reference:
let sayHello = { print("Hello") } sayHello()
Closure expressions may also be configured to accept parameters and return results. The syntax for this is as follows:
{(<para name>: <para type>, <para name> <para type>, ... ) -> <return type> in // Closure expression code here }
The following closure expression, for example, accepts two integer parameters and returns an integer result:
let multiply = {(_ val1: Int, _ val2: Int) -> Int in return val1 * val2 } let result = multiply(10, 20)
Note that the syntax is similar to that used for declaring Swift functions with the exception that the closure expression does not have a name, the parameters and return type are included in the braces and the in keyword is used to indicate the start of the closure expression code. Functions are, in fact, simply named closure expressions.
Closure expressions are often used when declaring completion handlers for asynchronous method calls. In other words, when developing iOS applications it will often be necessary to make calls to the operating system where the requested task is performed in the background allowing the application to continue with other tasks. Typically in such a scenario, the system will notify the application of the completion of the task and return any results by calling the completion handler that was declared when the method was called. Frequently the code for the completion handler will be implemented in the form of a closure expression. Consider the following code from an example used later in the book:
eventstore.requestAccess(to: .reminder, completion: {(granted: Bool, error: Error?) -> Void in if !granted { print(error!.localizedDescription) } })
When the tasks performed by the requestAccess(to:) method call are complete it will execute the closure expression declared as the completion: parameter. The completion handler is required by the method to accept a Boolean value and an Error object as parameters and return no results, hence the following declaration:
Learn SwiftUI and take your iOS Development to the Next Level |
{(granted: Bool, error: Error?) -> Void in
In actual fact, the Swift compiler already knows about the parameter and return value requirements for the completion handler for this method call, and is able to infer this information without it being declared in the closure expression. This allows a simpler version of the closure expression declaration to be written:
eventstore.requestAccess(to: .reminder, completion: {(granted, error) in if !granted { print(error!.localizedDescription) } })
Closures in Swift
A closure in computer science terminology generally refers to the combination of a self-contained block of code (for example a function or closure expression) and one or more variables that exist in the context surrounding that code block. Consider, for example the following Swift function:
func functionA() -> () -> Int { var counter = 0 func functionB() -> Int { return counter + 10 } return functionB } let myClosure = functionA() let result = myClosure()
In the above code, functionA returns a function named functionB. In actual fact functionA is returning a closure since functionB relies on the counter variable which is declared outside the functionB’s local scope. In other words, functionB is said to have captured or closed over (hence the term closure) the counter variable and, as such, is considered a closure in the traditional computer science definition of the word.
To a large extent, and particularly as it relates to Swift, the terms closure and closure expression have started to be used interchangeably. The key point to remember, however, is that both are supported in Swift.
Summary
Functions, closures and closure expressions are self-contained blocks of code that can be called upon to perform a specific task and provide a mechanism for structuring code and promoting reuse. This chapter has introduced the concepts of functions and closures in terms of declaration and implementation.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
The Swift Switch Statement | The Basics of Object Oriented Programming in Swift |