Scope and Context Explained in Swift

Written by LearnAppMaking on March 25 2021 in App Development, Swift

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.

  1. What is Scope in Swift?
  2. Global, Local, Function and Class Scope
  3. Scope in Practical iOS Development
  4. How to Fix “error: cannot find ‘x’ in scope”
  5. 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:

func getAge() -> Int
{
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, like age, 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?

  1. A variable age is defined inside the getAge() function, with var age = 42, on the first line of the function.
  2. A variable age is defined outside (below) the getAge() function, with var 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:

  1. You cannot use a variable outside the scope it’s declared in
  2. 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:

  1. The scope of the Product class is global. It’s globally defined, so we can create Product objects anywhere in the code.
  2. The scope of the Kind enum is limited to the class. We can use the Kind type inside the class, and not outside of it (see below).
  3. 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:

  1. The global scope, in which the Product class is defined
  2. The class scope, in which the kind property, Kind enum and canEat() function are defined
  3. The function scope, inside the canEat() function, in which we’re using the value of the kind 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

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 via Product.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:

func reverse(_ string: String) -> String
{
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 to import 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 Product → Clean 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 of URLSession. 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:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.