Creating strong reference cycles with closures

Earlier in this chapter, we said that: for the most part, Swift will handle memory management for us. The for the most part section of the quote means that if everything is written correctly, Swift will handle the memory management of the closures for us. However, there are times where memory management fails. Memory management will work correctly for all of the examples that we have seen in this chapter so far. It is possible to create a strong reference cycle that would prevent Swift's memory management from working correctly. Let's look at what happens if we create a strong reference cycle with closures.

A strong reference cycle will happen if we assign a closure to a property of a class instance and within that closure, we capture the instance of the class. This capture occurs because we access a property of that particular instance using self, such as self.someProperty, or we assign self to a variable or constant, such as let c = self. By capturing a property of the instance, we are actually capturing the instance itself, thereby creating a strong reference cycle where the memory manager will not know when to release the instance. As a result, the memory will not be freed.

Let's begin by creating a class that has a closure and an instance of the String type as its two properties. We will also create a typealias for the closure type in this class, and define a deinit() method that prints a message to the console. The deinit() method is called when the class gets released and the memory is freed. We will know when the class gets released when the message from the deinit() method is printed to the console. This class will be named TestClassOne. Let's look at the following code:

class TestClassOne { 
  typealias nameClosure = (() -> String)  
  var name = "Jon" 
  lazy var myClosure: nameClosure = {  
    return self.name 
  } 
  deinit { 
    print("TestClassOne deinitialized") 
  } 
} 

Now, let's create a second class that will contain a method that accepts a closure that is of the nameClosure type, which was defined in the TestClassOne class. This class will also have a deinit() method, so we can also see when it gets released. We will name this class TestClassTwo. Let's look at the following code:

class TestClassTwo { 
  func closureExample(handler: TestClassOne.nameClosure) {  
    print(handler()) 
  } 
  deinit { 
    print("TestClassTwo deinitialized") 
  } 
} 

Now, let's look at this code in action, by creating instances of each class and then trying to manually release the instance by setting them to nil:

var testClassOne: TestClassOne? = TestClassOne()  
var testClassTwo: TestClassTwo? = TestClassTwo() 
 
testClassTwo?.closureExample(handler: testClassOne!.myClosure)  
testClassOne = nil 
print("testClassOne is gone")  
testClassTwo = nil  
print("testClassTwo is gone") 

What we do in this code is create two optionals, which may contain an instance of our two test classes or nil. We need to create these variables as optionals because we will be setting them to nil later in the code so that we can see whether the instances are released properly.

We then call the closureExample() method of the TestClassTwo instance and pass it the myClosure property from the TestClassOne instance. We now try to compare both instances by setting them to nil. Keep in mind that when an instance of a class is released, it attempts to call the deinit() method of the class if it exists. In our case, both classes have a deinit() method that prints a message to the console, so we know when the instances are actually released.

If we run this project, we will see the following messages printed to the console:

testClassOne is gone 
TestClassTwo deinitialized 
testClassTwo is gone  

As we can see, we do attempt to release the TestClassOne instances, but the deinit() method of the class is never called, indicating that it was not actually released; however, the TestClassTwo instance was properly released because the deinit() method of that class was called.

To see how this is supposed to work without the strong reference cycle, change the myClosure closure to return a string type that is defined within the closure itself, as shown in the following code:

lazy var myClosure: nameClosure = {  
  return "Just Me" 
} 

Now, if we run the project, we should see the following output:

TestClassOne deinitialized 
testClassOne is gone 
TestClassTwo deinitialized 
testClassTwo is gone

This shows that the deinit() methods from both the TestClassOne and TestClassTwo instances were properly called, indicating that they were both released properly.

In the first example, we capture an instance of the TestClassOne class within the closure because we accessed a property of the TestClassOne class using self.name. This created a strong reference from the closure to the instance of the TestClassOne class, preventing memory management from releasing the instance.

Swift does provide a very easy and elegant way to resolve strong reference cycles in closures. We simply need to tell Swift not to create a strong reference by creating a capture list. A capture list defines the rules to use when capturing reference types within a closure. We can declare each reference to be a weak or unowned reference rather than a strong reference.

A weak keyword is used when there is the possibility that the reference will become nil during its lifetime; therefore, the type must be an optional. The unowned keyword is used when there is no possibility of the reference becoming nil.

We define the capture list by pairing the weak or unowned keywords with a reference to a class instance. These pairings are written within square brackets ([ ]). Therefore, if we update the myClosure closure and define an unowned reference to self, we should eliminate the strong reference cycle. The following code shows what the new myClosure closure will look like:

lazy var myClosure: nameClosure = { [unowned self] in 
  return self.name 
} 

Notice the new line, [unowned self] in, this line says that we do not want to create a strong reference to the instance of self. If we run the project now, we should see the following output:

TestClassOne deinitialized
testClassOne is gone 
TestClassTwo deinitialized
testClassTwo is gone
  

This shows that both the TestClassOne and TestClassTwo instances were properly released.