Scope and Context Explained in Swift
What’s “scope” in Swift programming? The concept of scope states that, if you’ve declared a variable in one “place” of your code, you cannot use it outside that place. It’s an implicit but essential rule in programming, and it can be challenging to grasp at first.
In this tutorial, we’ll focus on exploring scope, and what it means for practical iOS development.
Here’s what we’ll discuss:
- How to solve the Cannot find ‘x’ in scope error
- What’s scope, and how is it different from context?
- Types of scope: global, local, function, closure, block, etc.
- Working with scope in practical iOS development
Ready? Let’s go.
- What is Scope in Swift?
- Global, Local, Function and Class Scope
- Scope in Practical iOS Development
- How to Fix “error: cannot find ‘x’ in scope”
- Further Reading
What is Scope in Swift?
We’re going to start with an example that demonstrates different scopes. Check out the following Swift code:
{
var age = 42
age += 1
return age
}
var age = 99
var anotherAge = getAge()
anotherAge += 1
print(age)
print(anotherAge)
Take a moment to read through the code. Without cheating, can you guess what the values of age
and anotherAge
are?
You’re working with 2 kinds of scope in this example, the global and local scope.
- The local scope is present within the function
getAge()
, which is why it’s often called function scope. Variables, likeage
, that are declared inside the function, cannot be accessed outside of it. - The global scope is present everywhere – that’s why its called global. Variables defined at the highest level of the code, i.e. outside of functions, classes, etc., can be used anywhere. (There are exceptions, though.)
Now, let’s look at that code again. The variable age
is defined inside the getAge()
function. We cannot access that same variable outside the function. This is an important rule in working with scopes.
We also can’t redeclare a variable with the same name, in the same scope, because variable names must be unique within their scope. What we’ve done though, is define another variable with the same name in a different scope. See which one?
- A variable
age
is defined inside thegetAge()
function, withvar age = 42
, on the first line of the function. - A variable
age
is defined outside (below) thegetAge()
function, withvar age = 99
.
These 2 variables have the same name, but they’re declared in different scopes. They have 2 separate values. Their “regions” of code – their scope – don’t conflict with each other. That’s why we can use them both, with the same name but different values, separately!
Here’s how the above code works, line by line:
When the code runs, a variable age
is initialized with value 99
. We then initialize a variable called anotherAge
with the value that’s returned from the getAge()
function, which is 43
(42 + 1). That value is then incremented by one, with anotherAge += 1
.
Finally, we’re printing out the values of these variables. The value of age
is 99
, because it hasn’t changed. The value of anotherAge
is 44
. It’s initialized as 42
, incremented inside the function, and incremented outside of it. Despite 2 of these 3 variables having the same name, they don’t conflict with each other, because they’re declared in different scopes.
Starting to get the hang of scope? It’s nothing more than the “region” or place in which you have access to certain variables. We’ve also identified 2 essential rules:
- You cannot use a variable outside the scope it’s declared in
- Variable names must be unique within their own scope
There are exceptions to these rules, as you’ll soon see. For example, a property from a class scope can have the same name as a variable in a function scope – but you’ll need to use self to access the former.
It’s simplest to think of scope as an actual scope, you know, the one you find on top of a rifle – or a viewfinder in your photocamera, or in binoculars. When you look through the scope, you can’t see what’s outside of your view!
Local scope is the scope you’re currently in, i.e. typing in, that block of code between squiggly brackets { }
. If you want to practice hands-on with scope, just keep track of what your current, local scope is, and to which variables, types, etc. you have access.
In Swift, functions are at the deepest level of scope, which is why a local scope is often the same as the function scope. Closures are at a level deeper than functions, but closures can “close over”, which makes them kinda special. More about that later!
Global, Local, Function and Class Scope
So far, we’ve only looked at the global and local scopes. There’s also something called a function scope, and classes have a scope too. In fact, frameworks, modules and Swift files themselves have a scope. Scopes have levels, they’re hierarchical, and types are scoped too. Pfew! Where do we even begin!?
Let’s start with a simple class. Like this:
class Product
{
}
As far as we can see, this class is defined in the global scope. We’re now going to add an enumeration to this class, as a nested type. Like this:
class Product
{
var kind = Kind.thing
enum Kind {
case food
case thing
}
}
In the above code, we’ve defined an enum called Kind
. It has 2 cases – for the sake of this example, we’re considering any product either a food (can eat it) or a thing (cannot eat it). We’ve also added an instance property kind
of type Kind
, which is initialized with enum value .thing
by default.
Let’s discuss this example in terms of scope. What’s the scope of all that stuff? Here’s what:
- The scope of the
Product
class is global. It’s globally defined, so we can createProduct
objects anywhere in the code. - The scope of the
Kind
enum is limited to the class. We can use theKind
type inside the class, and not outside of it (see below). - The scope of the
kind
property is also limited to the class. We can use that property inside the class, with self.
However, something else is going on. We can use Product
in the global scope, and because the Kind
enum has internal
access control by default, we can use it as the Product.Kind
type anywhere in the code.
The code below can be used anywhere in the code. We can reach the Kind
nested type via the Product
class:
let banana = Product()
if banana.kind == Product.Kind.food {
print("banana is a food")
}
And, likewise, the kind
property is defined in the class scope, but because it’s also publicly accessible, we can access that property on any object of type Product
.
let dishwasher = Product()
dishwasher.kind = .thing
A crucial distinction here is that the Product
class and the dishwasher
variable are declared, and thus available, in the global scope. That’s why we can use them in the above code snippet.
Let’s get back to those different types of scope. Here, check out the Swift code below. We’re adding a canEat()
function to the Product
class:
class Product
{
var kind = Kind.thing
enum Kind {
case food
case thing
}
func canEat() -> Bool {
return kind == .food
}
}
We’re dealing with 3 levels of scope here:
- The global scope, in which the
Product
class is defined - The class scope, in which the
kind
property,Kind
enum andcanEat()
function are defined - The function scope, inside the
canEat()
function, in which we’re using the value of thekind
property of a current class instance
The Product
class is defined in the global scope, so we can use that anywhere in the app’s module. The kind
property is defined in the class scope, so we can use that within the Product
class. Same goes for the Kind
enum, and the canEat()
function.
We’re using the kind
property inside the canEat()
function. That means scopes have a hierarchy, because we can access a property from the class scope inside the function scope.
However, if we defined a local variable inside canEat()
, we can’t use that variable in another function in the same class, because they have different scopes.
func canEat() -> Bool {
let hungry = ···
}
func isThing() -> Bool {
print(kind) // OK, because `kind` is in scope
print(hungry) // not OK, because `hungry` not in scope
}
Here’s a shorthand for the hierarchy of scopes:
- Global scope (also, file/module scope)
- Class (or struct) scope
- Function scope
- Closure scope
- Function scope
- Class (or struct) scope
The way to read this hierarchy is to understand that you’ve got access to the global scope (higest) in the closure scope (lowest), but not the other way around. You can access what’s higher in the lower levels, but not what’s lower in the higher levels.
So, to summarize:
- Every region in your code, the stuff between square brackets, has a scope: global scope, class scope, function scope, and so on
- We generally distinguish between local scope and global scope, in order to express whether we have access to a certain variabe, property, or type
- Scopes are hierarchical, which means we can access
Kind
viaProduct.Kind
if we’re in the global scope - Scopes are hierarchical, which means we can access a class property inside a class function, because the function has access to the class scope
The visibility of a type, variable, property, etc. determines whether it can be accessed. This is called access control. We’ve got 5 different levels of access: open
, public
, internal
, fileprivate
, and private
. In general, we shorten that to “is it public?” or “is it private?”, because that’s quicker. You can learn more about access control in Swift in this tutorial: Access Control Explained in Swift
Scope in Practical iOS Development
Scope is everywhere in practical, day-to-day iOS development. When you’re passing values around in your app, tracking to which variables, types etc. you have access, is a continual activity.
Chances are that, if you’re relatively new to iOS development, you’ve already incorporated scope in your reasoning about your code, without knowing it! “Which variable can I access where?”
An interesting case of scope is that of closures. As you may know, a closure is a block of code that can be passed around your code. It’s similar to a function, except that the code itself is the value. You can assign a closure to a variable, pass it into a function, after which it ends up in a different part of your program.
Closures are often used as so-called completion handlers. Say we’re downloading an image asynchronously from the internet. When the downloading completes, at a future point in time, we want to execute some code to show the image. We define this code in a closure and pass it to the function that downloads the image. This function then executes the closure when the download has completed.
Here, check this out:
class DetailViewController: UIViewController
{
@IBOutlet weak var imageView:UIImageView?
func viewDidLoad()
{
network.downloadImage(url, completionHandler: { image in
imageView?.image = image
})
}
}
Note: Scope is a concept regardless of whether you’re using UIKit or SwiftUI. If you’re into SwiftUI, don’t disregard the above example merely because it uses a view controller. The point of this section, as you’ll soon see, is how you can use the property imageView
inside the closure scope. That applies to SwiftUI, and other components, too!
In the above code, we’ve created a view controller class with an imageView
property. Inside the function, we’re calling a hypothetical downloadImage(_:completionHandler:)
function. Its second parameter, the completion handler, between the innermost squiggly brackets, is a closure. When the image download has completed, we’re assigning the downloaded image
value to the image
property of the imageView
, which will show the image.
A closure is called a closure because it “closes over” any values that are referenced in the closure. Because closures can be passed as values through your code, a closure needs a way to reference values that are used inside the closure. This principle is called closing over, or capturing.
In the above example code, the closure holds onto a reference to the imageView
property. It needs that property later, to set the image. When the closure finishes executing, this reference is released.
This capturing only works if the closure has access to the same level of scope as the function it’s defined in. Even though the closure is a separate entity, it can access the imageView
property, because the function viewDidLoad()
has access to that scope. A closure has the same scope as the entity it’s defined in, and in this case, that’s the function scope. The same is true for property observers, computed properties, and other block-level code.
Interesting, isn’t it? Understanding capturing is the ultimate test of your understanding of scope. You’ll have to figure out why the closure, which is executed in the future, as seen from the context of the viewDidLoad()
function, could possibly have access to the imageView
property. It’s because the closure has access to the scope it’s defined in!
Scope and context are often confused. Scope is fixed, because the code you write is fixed. Context is fluid, and it depends on the execution of your code. So, you may have access to a property because it’s in scope, but because of the context of your code, i.e. its point in execution, that property is empty. You can see scope as how your code is defined (fixed), and context as what your code does at any given point in its runtime (fluid). Think of it like riding a motorcycle: the motorcycle has 2 wheels (scope) and whether they’re spinning or not depends on you riding the bike (context).
How to Fix “error: cannot find ‘x’ in scope”
Now that we’ve discussed what scope is and how it works, let’s focus on the most common error you’ll find when working with scopes. It’s this:
error: cannot find ‘…’ in scope
This is a compile-time error, so it’ll pop up when you try to compile code that results in this error. As you’ve guessed, the meaning of it is simple: the variable name you’re referring to doesn’t exist!
Check this out:
{
var result = ""
for c in text {
result = String(c) + result
}
return result
}
let value = reverse("Hello!")
print(value)
When you run the above code, you’ll get the following error:
error: cannot find ‘text’ in scope
What’s going on here? We’ve obviously made a mistake somewhere. Even though the line for c in text
makes sense – loop over every character in text
– the variable text
doesn’t exist. It’s an understandable typo: text
should be string
, which is the input parameter for the reverse()
function.
In general, it’s safe to assume that the “Cannot find ‘…’ in scope” refers to a variable, function, type or other symbol that doesn’t exist. If you want to solve this error, and fix the bug, start with what doesn’t exist and work your way from there (i.e., text
).
It’s good to know that the “Cannot find ‘…’ in scope” error is a symptom, so it’s not the root cause. For example, in the above code, referring to “text” was a typo. It doesn’t help at all if we create a new variable text
just to solve the error, only to discover later that the reverse()
function doesn’t work anymore.
What other problems can cause the “Cannot find ‘…’ in scope” error?
-
Missing Imports: Note that this error can also get triggered by a type that’s missing, not just by variable names spelled wrong. For example, if you’re missing a
View
type – you’ll probably need toimport SwiftUI
. The iOS SDK documentation can help you with this. - Missing Modules: Say you’ve added a library to your project, but it contains a bug. This bug prevents the library from compiling. As a result, the library you’re importing – and its types – are missing. You see the error as “Cannot find …”, but the root cause is the bug in the library!
- Xcode Issue: Unfortunately, Xcode occasionally malfunctions and starts to throw errors when there are none. It could happen that auto-complete crashes, or the Swift compiler fails, and as a result a library, type, variable etc. that you are certain exists, appears to be missing. Do a or press Command + Option + Shift + K to clean your project’s build folder, and run the code again.
- Changed Code: Most code you depend on – libraries, frameworks, SDKs – is in constant flux. We’ve got semantic versioning, release tags, version pinning, etcetera, but at some point you’ll have to upgrade an app from a library’s v1.0 to its 2.0. Meanwhile, the API for that library has changed and they’ve renamed some class you use. What happens? Cannot find … in scope. In this scenario, it’s important to be mindful of your assumptions – because you’re going to look for that class that for sure existed in the v1.0!
-
Missing Variable: This is of course the most common cause: you mistyped a variable name, function name, type, or something else. It’s easy to miss typo: lowercase instead of uppercase, a dot somewhere, or
ULRSession
instead ofURLSession
. Don’t worry! It happens to the best of us (more than we’d like to admit). When in doubt, sleep on it or go for a walk, and look at it later with fresh eyes.
Note: Sometimes you want a variable or function to exist in a certain place, but it simply doesn’t. Let’s say you’ve got 2 views – how do you get that variable from here to there? That’s where passing data from one component to another comes in. Check out these tutorials: How To: Pass Data Between Views with SwiftUI and How To: Pass Data Between View Controllers in Swift
Further Reading
Scope is a concept that’s hard to put into rules and words, but once you get the hang of it, it becomes second nature. You’ve been working with scope all along, maybe even without knowing it. Scope is the answer to the question: “Can I use this variable here?” Neat!
Want to learn more? Check out these resources:
- Access Control Explained in Swift
- The Ultimate Guide to Closures in Swift
- Introduction of Object-Oriented Programming in Swift
- Variables and Constants in Swift Explained
- Self and self in Swift
- How To: Pass Data Between View Controllers in Swift
- How To: Pass Data Between Views with SwiftUI
- Functions in Swift Explained
- Properties in Swift Explained
- Get Started with Debugging in Xcode