When you first started coding with Swift it’s likely that the code you write doesn’t have much structure. You experiment in Playgrounds, you write a bit of code here and a bit of code there. You declared constants and variables as you need them and experiment with code snippets you see online to see how they work, all without any real thought about how you have structured and organised that code. When you’re first starting out thats fine but as your experience grows and you start writing larger and more complex programs it reaches the point where you really need to start thinking about how things are organised.
With a little care, adding some structure to the things you write can help you design, develop and test components individually, before you combine them into larger and more complex solutions. It can also help you write code that is easier to understand, both by yourself and by others, and help write code that is significantly easier to maintain in the long-term.
In this article, we are going to look at the different options we have available in Swift for logically organising our code. We’ll see how these options form a kind of nested hierarchy and see how they allow finer and finer control over our code. We’ll also look at how these options then affect the visibility and longevity of the items we declare. I’ll explain more about that later.
To kick off with, let’s first look at the options we have at our disposal for logically organising and structuring our code.
Table of Contents
Giving Your Swift Code Some Structure
In Swift, there are four primary levels at which we organise code:
- Modules
- Source Files
- Classes
- Code Blocks
Modules
The Swift Language Guide describes a module in Swift is the following:
A module is a single unit of code distribution — a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s
import
keyword.
In layman’s terms, this means that each build target in Xcode (such as an app bundle or framework) is treated as a separate module. Modules provide a macro-level way of organising our code. For example, we could potentially break the code for our application into two parts; a framework to hold some common functionality and the application containing the user interface that uses that framework. We could even have multiple frameworks. It’s really up to us. Modules than are granular, they’re high-level and they provide an initial division for our code. Beyond this macro-level organisation, we then have the source files that make up our projects.
Source Files
You will most likely have seen Swift source files already. They the files that have the .swift
extension, and it is these files, these vessels for our code, that provide our next level of structure and organisation.
As we’ve already seen, at the highest level our projects are divided into modules and it is these modules that in turn are composed of one or more source files.
Source files in Swift are much as you would expect. They’re text files that contain our Swift source code and in all honesty, there not much more to them than that. Our source files allow us to organise our code by putting declarations and statements into different files. It’s actually where some of the skill with writing Swift code comes in, deciding which pieces of code go where!
But we’re not done. Even within these source files, we have further tools we can use to help us organise and structure things.
Classes
As we’ve just talked about, each code file in Swift can contain a bunch of different code statements. They can define constants, variables, functions and types along with a whole bunch of other logic that make up the real guts of our applications and frameworks.
The Swift language itself is what is called an Object-Oriented Language. This means that we can define custom types called classes that act as the blueprints for objects in our applications. Objects are really representations of the ideas and concepts our applications and frameworks are about. A user profile. A password. A transaction. They’re all examples of objects and by using them in our application we have an additional set of scaffolding around which we can organise and structure things.
The individual classes and objects that our apps and frameworks are about provide an initial high-level grouping for our code. Classes collect together the variables and constants that represent the objects state. They also providecode blocks (also known as methods when associated with a class) that represent the objects behaviour and help further divide our code into smaller units. It is these code blocks that provide our final level of organisation and structure.
Code Blocks
in Swift we use code blocks in a variety of different situations. As a concept they’re pretty simple. All they do is group a set of related statements that perform a particular task into a single organisational unit. In Swift, code blocks generally start with an opening curly brace ({
) and continue until a corresponding closing curly brace is reached (}
) with all the statements in-between making up the body of that code block.
The statements within body of the code block can be anything we like. Declarations, expressions, control statements or anything else we need, all of them grouped together and executed in the order that they appear in the source code.
In reality, we’ve already seen examples of code blocks already. An objects methods are a code block. Functions are also code blocks. A branch of an if
statement – that’s a code block. The body of a loop. That’s another. There are examples everywhere.
Lifetime and Scope
So here it is. By now, you should have a pretty good idea of the different levels of organisation and structure we can apply to our code. We’ve seen how modules, source files, classes and code blocks can be used to structure and shape the code we write, but what you may not have realised, or seen interwoven with these organisational levels, are two very important concepts – the concepts of lifetime and scope.
Lifetimes
In Swift, as in much of computer science, an items lifetime is the period of time in which the item is available in memory. It’s a similar concept to human lifetime. In human terms we’re born, we live for some period (our lifetime), and then we die. It’s similar in Swift. An item, (say a constant or variable) also has a lifetime. It’s declared, (analogous to being born), it lives for some period within our code (it’s lifetime) and is eventually removed from memory (it’s death).
Closely related to the concept of lifetime is the related concept of scope.
Scope
The scope of an item in Swift represents the period of time for which the item is accessible within our program.
As such, the items scope is always a subset of the items lifetime due to the fact that the item may exist in memory but may not be accessible. Essentially, an objects scope defines the areas of our program from which the item can be accessed.
In Swift, the scope of variables, constants and other named declarations can be subdivided into one of two general categories; top-level or global scope and local or nested scope.
Global Scope
By default, variables, constants or other named declarations that are declared at the top-level of a source file are said to have a top-level or global scope. This means that they accessible from code in any source file within the same module.
Note: It should be noted though that this is the default behaviour and can be overridden in Swift through the use of access modifiers. I’m not going to go into access modifiers today so further details on this will have to wait for a future article.
There is a word of caution when it comes to declaring items at a global scope though.
Using items declared at a global scope is generally seen to be bad practice in Swift. History and experience has taught us, (both in Swift and in other languages), that globally accessible items are a common sources of bugs within our applications. This is primarily due to their global nature. A change to a global variable in one area for can (and often does), have completely unexpected effects in an unrelated part of our application leading to bugs that are difficult to find and fix. For this reason, using global items isn’t recommended but that leaves us with a questions. What’s the alternative? Well, the alternative is declaring things at a local or nested scope.
Local or Nested Scope
Local or nested scope are subsets of the global scope and are generally formed at each new organisational boundary we looked at earlier. Source files can form a new scope. Objects form a new level of scope. Functions and methods form a new level of scope, as do code blocks.
The key to scope is this:
Any item that is declared in Swift is declared at a certain point within a nested hierarchy of scopes. In terms of accessibility, declared items are only accessible from code that is at the same or at a lower level of scope than the level the item was declared at.
Understanding Scope – A Worked Example
Now that we have looked at both global and local scope, in this next section we’re going to look at an example of how this works in code. Consider the code below which is written at the top of a .swift
file or in a playground:
let three = 3 // global scope
print("Global Scope:")
print(three) // Legal as can see things in the same or enclosing scope.
func outerFunction() {
print("OuterFunction Scope:")
var two = 2 // scoped to the braces of the outerFunction
print(two) // Legal as can see things in the same or enclosing scope.
print(three) // Also legal as can see things in same or enclosing scope.
if true {
print("If Statement Scope:")
var one = 1 // scoped to the braces of the `if` statement.
print(one) // Legal as can see things in the same or enclosing scope.
print(two) // Legal as can see things in same or enclosing scopes.
print(three) // Also legal for the same reason.
}
// print(one) - Would cause an error. Variable `one` is no accessible from outer scope.
}
// print(two) - Would cause an error. Variable `two` is not accessible from outer scope.
outerFunction()
// Global Scope:
// 3
// OuterFunction Scope:
// 2
// 3
// If Statement Scope:
// 1
// 2
// 3
At the top-level of our code we have the declaration of the constant three
. The constant three
is said to have global scope. It is declared at the top level of our source file outside of any other block of code and as such will be accessible from anywhere within our code file as well as from any other code file that is in the same module.
Next, we have the declaration of outerFunction()
.
As we talked about functions earlier. As such, the functions associated curly brackets ({}
) create a new level nested level of scope whenever they are encountered.
Inside this new nested level of scope we then declare the constant two
and due to the fact that we declared two
within this scope, the constant will only be accessible within either that function or any nested level of scope.
We explore this further with our print
statements.
Firstly, we print out the value of the constant two
. We can do this because the code that is printing it is at the same level of scope as the declaration of the constant.
Similarly, we also print out the constant three
. This also works, in this case because the constant three
is accessible anywhere within our code due to it’s global scope.
If we keep following the code down, the next thing we come to is the declaration an if
statement. Again, the if
statement is an organisational unit, it’s essentially a code block, and the open and closing curly brackets ({}
) again create a new nested level of scope.
Inside this new nested scope, we then declare the constant one
.
Within the if
statement the constants one
, two
and three
are all accessible. This is because our code is accessing them from a level that is the same or at a lower level of scope than the level they were declared at.
Now here’s the kicker. Look at the commented out print
statement immediately after the closing brace of the if
statement.
In this (admittedly commented out) statement, we attempt to print out the value of the constant one
. The reality is if we uncommented this line, the line would fail? Have you worked out why?
Here’s a hint. Remember, that the constant one
was declared at our lowest level of scope, inside the scope defined by the if
statement. At the point the if
statement exits (essentially the point the closing curly bracket (}
) of the if statement is encountered), execution moves back up one level of scope. This means the constant one
is no longer accessible and is said to have “gone out of scope”. It may still be in memory, it’s just that we can no longer access it.
The problem then is that in our print
statement we DO try to access it and as we’ve just talked about Swift doesn’t allow us to access a item that was declared in a lower level of scope from code at a higher level of scope.
If we move on with our example code, we have another example of this with the constant two
.
At the point we exit the outerFunction()
function we again move up one level of scope. At this point, the constant two
, (which was declared within the scope of the outerFunction()
function), also goes out of scope and is no longer accessible. As with the previous example, our commented out print
statement (this time after the closing brace of the outerFunction()
function) tries to access the constant despite the fact that it has gone out of scope. Again, not allowed. Get the idea?
Shadowing
Now, from the previous example you should have a good idea of how different items can be declared at different levels of scope and how the scope they’re declared in affects where they can be accessed in your code. But what you might not have thought about is what happens if we declared two items at different levels of scope with the same name. Consider this code:
let x = 10
print("x outside myFunc equals \(x)")
func myFunc() {
let x = 20
print("x within myFunc equals: \(x)")
}
myFunc()
print("x outside myFunc equals:\(x)")
// x outside myFunc equals: 10
// x within myFunc equals: 20
// x outside myFunc equals: 10
Let’s walk it through step-by-step.
In the first line, we declare a new constant x
and assign it a value of 10
. This is followed by a print
statement to print out its value. Pretty simple so far.
We then declare a new function called myFunc()
. As we’ve already seen, functions create a new level of scope within our code and this is where things get interesting.
Within the new nested scope created by the function, we declare another constant called x
. Same name, different value.
So what is going on? Well, this is an example of something called shadowing. With shadowing, we can declare an item at a lower level of scope that will shadow or cover an item with the same name at a higher level of scope. We can see this in the following print
statement.
When we print out the value of x
inside the myFunc()
function, we get a value of 20
but once the function has returned, and the constant x
declared inside the function is no longer in scope, the next print
statement which is outside that scope again prints out the original value of x
, the value from the global scope.
Namespaces
Ok so we’ve seen what happens if we defined two items with the same name at different scopes, but what about if we defined two items with the same name in the same scope?
Well, you may already know, this isn’t allowed in Swift. You can try this for yourself:
var x = 4
var x = 6 // Causes a compiler error.
Now, you may be thinking this is a silly example. You’d never do this, and you’re most probably right. But imagine this scenario.
You have decide to write a new app and plough into writing your code. Declaring variables and functions and implementing the applications logic. As part of this you define a global variable called user
to store some details about the apps user, things like their name, maybe a profile picture that sort of stuff. Everything is going well but then you decide you want to make use of a third party library to help within implementing a new social sharing feature. Unbeknownst to you, the author of that third-party code also defined a global variable called user
in their code. The first you know of this is when you try to integrate the library into your code and the compiler starts complaining. So what do we do? Well this is where our final concept of today comes in; Namespaces.
Namespaces and scope are closely related. A namespace in Swift is a named region of a program. As such they provide virtual grouping within our code where things outside of the namespace cannot access things inside of the namespace without first mentioning the namespaces itself. This provides a virtual partitioning separating the names in one namespace from the names in another. It also allows two items to be declared with the same name as long as they are in different namespaces.
In Swift, unlike in some other languages, the declaration of namespaces is implicit. This means that we don’t explicitly declare namespaces for ourselves. Instead Swift creates a namespace at the boundary of each module. That means your app has it’s own namespace and any framework you write will have it’s own separate namespace. It also means that any frameworks that you import will also have their own namespaces, thereby avoiding the namespace clash that we saw in our fictitious scenario above.
So one obvious question then is how do we actually distinguish between two items with the same name in different namespaces?
Accessing Items Within Namespaces
To access items in one namespace from the code in another we must first import the module containing the item we wish to access. We do this in Swift using the import
declaration. The import
declaration in its most basic form, consists of the import
keyword followed by the name of the module that we wish to import:
import ModuleName
By default, this imports everything in the named module and makes it accessible in our code. In addition to the basic form though, we can also import specific symbols from a module. In this case we use the import
keyword and follow it with the kind of thing we want to import (class, function, variable etc), the name of the module we wish to import it from, and the name of the item we wish to import:
” import itemType ModuleName.ItemName
When using this form, the item type can be one of:
typealias
struct
class
enum
protocol
var
func
For example, say I wanted to import the NSString
class from the Foundation
framework, I could write:
import class Foundation.NSString
The syntax is an example of how to access an item within a namespace. In Swift, this is done using a period (.
). The period separates the name of the namespace (essentially the module name) from the item we wish to access and we can use this syntax to distinguish between two items in different namespaces. For example, say I had defined a class in my code called NSString
. I could, quite legitimately also import the NSString
class from the Foundation
framework and still refer to each of them uniquely:
import class Foundation.NSString
class NSString {
var welcome = "Hello!"
}
var myStr = NSString() // Local NSString Class
var otherStr = Foundation.NSString() // Foundation NSString Class.
myStr.welcome
One thing to note here is that as a shortcut, when we accessing code that is in the same namespace Swift allows us to drop the namespaces prefix. This is why in the example above we don’t need to use the namespace prefix for the locally declared NSString
class.
In reality, it’s not that common to have to explicitly deal with namespaces in Swift. They are there as a convenience and provide a nice mechanism for protecting us from namespace clashes such as in the example above. The key thing to remember is that the namespaces in Swift are implicitly declared for you at the module boundaries. If you remember that you shouldn’t have any issues.
Anyway, that about wraps it up for today. By now you should have a much better understanding of lifetimes, scope and namespaces and how they affect the Swift code you write. As ever, if you do have any questions or you think I’ve got anything wrong, please get in touch.