A practical guide to Weak Referencing in Swift
A weak reference is a reference that does not keep a stronghold on the instance it refers to, and so does not stop ARC from disposing of the referenced instance.
This behavior prevents the reference from becoming part of a strong reference cycle. Practically, you will indicate that a property or a variable is to be stored with a weak reference by marking it with the weak
keyword before its declaration; unowned
is another keyword that can also be used to indicate the weak nature of a reference to a property or a variable. The difference between unowned
and weak
will be explained in this article.
1. Weak referencing in asynchronous callbacks
Swift allows for a block of code to be passed to a function for future execution. The time it takes for the execution to start would usually depend on some conditions and results of the aforementioned function. The execution of the passed
block of code is what we refer to as a callback. The block executed during the callback can beescaping
or not, it can also be anautoclosure
.
Assume, you triggered a long-lasting task, and you are expecting a callback on completion of that task. The latter might be a service call, a hardware task, a call to an SDK, etc.
As mentioned earlier, you cannot foretell the instant when the callback will take places. Therefore you cannot also guarantee the state of your application or the originating view at that instance. So the application can be in the background due to a call that came through or to an erroneous screen lock, the screen might be dismissed due to OOB authentication screen that popped-in, the view controller might even be deallocated because you waited too long as you ended navigating to another screen.
In short, the possibilities are multiple and unpredictable. It arises then the need to ensure that irrespective of the state of the application of the screen, the callback does not lead to a fatal crash. We need to weak-reference
all the properties or variables for which we cannot guarantee the existence at the instant of the callback.
1. Given a MediumArticleLoader
protocol
which represents the requirements of aHTTP client
that is meant to load medium articles. The request to load articles can complete with a list of fetched articles or an error. Assume that the said request is triggered by a tap on a UIButton
and that the fetched articles are to be displayed on a tableview
.
struct Article { // model for medium article let authorFullname: String let publicationDate: Date var title: String var content: String var rating: Int}enum ServiceError: Error { //possible errors that might occur when fetching articles case describedBy(String) case `default`}protocol MediumArticleLoader { // requirements of a HTTP client func loadMediumArticles(completion:@escaping (([Article]?, ServiceError?) -> Swift.Void))}class MediumArticleTableViewController: UITableViewController { var mediumArticleLoader: MediumArticleLoader? // to be set
func displayError(_ error: Error?) { // Implementation goes here } func updateDisplayedArticles(_ articles: [Article]) { // Implementation goes here } @IBAction func didTapLoadArticleButton(_ sender: UIButton) { mediumArticleLoader?.loadMediumArticles { [weak self]
(fetchedArticles, error) in if error==nil { self?.displayError(error) } else if let loadedArticles = fetchedArticles { self?.updateDisplayedArticles(loadedArticles) } else { self?.displayError(ServiceError.default) } } }}
Notes and recommendations about the callback block:
- Minimize the number of calls to an optional self inside the callback. The important to keep in mind is that
self
isoptional
and there is no benefit to having a lot of call toself?
that might benoop
if self is nil. My suggestion is that you extract the code that is part of your callback into aninstance method
ornested
ruction and that you pass the adequate parameters to that method. - Avoid calling functions or methods with a parameter list that includes instance properties. For example, in the code below, the line
self?.navigateToMediumHomePage(username: (self?.usernameTextField.text)!, token: session.token)
should be avoided for many reasons including the following:
** (self?.usernameTextField.text)!
is almost a logical contraction in all strictness. I have seen similar lines of code so many times, and I repeat to the writer that there is a fundamental flow in lines contains both a ?
and a !
. Indeed, at one point we hypothesize that the value might be nil
with the use of ?
and at the other point we assert with the use of !
, that we should always have a value.
** All the variables used in self?.navigateToMediumHomePage(username: (self?.usernameTextField.text)!, token: session.token)
are accessible within the self
realm which alleviates the need to pass them through. I recommend extracting the call into an instance method that can be defined as func navigateToMediumHomePageForUserWithToken(_ token: String).
The created function must have the particularity of not taking any of the instance properties as one of its parameters.
struct Session { let token: String}protocol MediumAuthenticator { func authenticate(username: String?, password: String?, completion:@escaping ((Session?, ServiceError?) -> Swift.Void))}class MediumLoginViewController: UIViewController { var mediumAuthenticator: MediumAuthenticator? // to be set @IBOutlet weak var usernameTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! func displayError(_ error: Error?) { // Your implementation } func navigateToMediumHomePage(username: String, token: String) { // Your implementation } func navigateToMediumHomePage(userSessionToken token: String) { // Your implementation
// Compute user name here from usernameTextField } @IBAction func didTapLoginButton(_ sender: UIButton) { mediumAuthenticator?.authenticate(username: usernameTextField.text, password: passwordTextField.text) {
[weak self] (session, error) in if let error = error { self?.displayError(error) } else if let session = session { self?.navigateToMediumHomePage(username: (self?.usernameTextField.text)!, token: session.token) // BAD // self?.navigateToMediumHomePage(userSessionToken: session.token) // GOOD (use this call instead of the previous) } else { self?.displayError(ServiceError.default) } } }}
2. Weak referencing in asynchronous dispatches
Swift defined a number of dispatch queues
which all offer asynchronous
and synchronous
execution of blocks of codes. When using them to perform tasks asynchronously we need to pay particular attention to weak referencing.
DispatchQueue.global(qos: .userInteractive).async { }DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 5) {}DispatchQueue.main.asyncAfter(deadline: .now() + 5) {}
When a block code is called for asynchronous execution in a dispatch queue
, it is the OS that decides when to perform your block of code based on various parameters: the requested queue,
the quality of service
if applicable, the number of operations
in the queue, etc. We recall that we mentioned earlier the issues that originate from not controlling the time of the execution of your block code. The issues will even be more pronounced if you wish to delay the execution of the block by the use ofasyncAfter
. All the preceding imposes ipso facto that you must access variables weakly
where needed.
extension NSNotification.Name { struct CustomNotificationName { static let kAccountRequiresRefreshNotificationName =
"accountRequiresRefresh" } static let accountRequiresRefresh =
NSNotification.Name(CustomNotificationName.kAccountRequiresRefreshNotificationName)}class MyClass: NSObject { override init() { super.init() NotificationCenter.default.addObserver(forName: .accountRequiresRefresh, object: nil, queue: .current, using: { [weak self] _ in // prevent strong capture of self, which might prevent deallocation // perform you update or call self here, self is optional --> use self?. }) } func downloadArticleImage() { DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 5) { [weak self] in// call to the download API } } deinit { NotificationCenter.default.removeObserver(self) }
}
3. The forbidden “unowned”
Unowned
and weak
keywords both offer the same means of storing properties/variables with a weak reference i.e without keeping a stronghold to them. For the sake of differentiating, we will refer tounowned keyword
as unowned reference and to weak keyword
a weak reference. Let’s highlight one of the key differences between unowned and weak references.
An object stored with an unowned
reference always expects to have a nonnull
value, whereas both null and nonnull values, are permissible into a variable property referenced with a weak
reference.
- A corollary of the above is that since a
weak-referenced
variable is inherently capable of holding null as a value, it shall always, therefore, be declared as anoptional property
.
Some practical cases for the use of unowned
i. Completion blocks where the responder shall always exist. In the snippet below storeProductDelegate
for whom the app store page was requested the shall always exist if the callback
is to be triggered and because there are no states to persist when attempting to load the app store page, there is no need to store the storeProductDelegate
. Though we cannot guarantee the existence of self at the instant of the call-back, we must guarantee however guarantee that the storeProductDelegate
will always exist if the current self
exist withouth however having a stronghold
to it.
private func launchStoreProductPage(id productId: UInt64,
storeProductDelegate: SKStoreProductViewControllerDelegate) {class StoreProductInteractor: NSObject { private func launchStoreProductPage(id productId: UInt64, storeProductDelegate: SKStoreProductViewControllerDelegate) { let product = [SKStoreProductParameterITunesItemIdentifier: NSNumber(value: productId)] let storeController = SKStoreProductViewController() storeController.delegate = storeProductDelegate storeController.loadProduct(withParameters: product) {[unowned storeProductDelegate, weak self](success, error) in if success { // self is optional --> use self?. // storeProductDelegate is not optional --> use storeProductDelegate } else { // self is optional --> use self?. // storeProductDelegate is not optional --> use storeProductDelegate } }}}
ii. Storing of a hardware resource that must always exist on a non-faulty device and that should not be strongly referenced by multiple objects. There are cases where an instance of hardware is shared by objects which on they turn have circular dependencies
between each other. It is crucial to limit the strong reference to the shared hardware instance to prevent retain cycles
. Let’s consider an instance of aCameraPermissionManager
which should not be owned or by an instance of a PhotoLoaderViewController
for aforementioned reasons.
class CameraPermissionManager { }class PhotoInteractor { unowned var cameraPermissionManager: CameraPermissionManager // we cannot own[have a strong reference] cameraPermissionManager, but cameraPermissionManager must always exist init(cameraPermissionManager: CameraPermissionManager) { self.cameraPermissionManager = cameraPermissionManager }}
iii. Asynchronous UI in updates. When a UI update is being performed in a dispatch block or in a callback, access to properties or variables must be done with caution. However, it can be guaranteed that the variable will exist, unowned
is the best option.
class MyClassWithAnimation: NSObject { func performTypeAAnimation() { UIView.animate(withDuration: 0.5, delay: 0.5, options: .curveEaseOut, animations: { [unowned self] in // self should never be nil, unless something terrible had happened }) }
func performTypeBAnimation() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {// self should never be nil, unless something terrible had happened } }}
4. Weak referencing when storing delegates
A delegate
object is an object that responds or handles the results of certain events or operations that have taken place in a given object called the delegating object
. The latter must have knowledge ofthe delegate ,
usually by having it as one of its property, in order to forward calls to it when needed. In general, the delegate is a responder or slave
to the delegating object
but it is a commander or driver
of the UI
or the logic of another object. It is, in the context of this latter that the delegate should be owned i.e strongly held,
and not in the context of the delegating class
.
- The delegating class must exist and function even if the delegate does not exist. The delegate is merely a responder, the engine is the delegating class, a good analogy is that the engine of a vehicle must be able to function even if there are no wheels that transform the output into motion. Therefore, it is recommended to store delegate as an
optional
property in general. - The actions of the delegate should be idempotent to the states of the delegating class. Do not make delegate actions change any of the stored states of the
delegating class
. In general, it should be safe to make the access level of the delegate property aspublic
. - The delegate constraints should be defined in by protocol that has to be
class-bound
in order to allow for weakreferencing
.
protocol ScannerDelegate: class { func scanner(_ scanner: Scanner, didCompletedScanningWithData scannedData: Data)}class BarCodeViewController: UIViewController { weak var scannerDelegate: ScannerDelegate?
}
- There are cases where the delegate can be strongly stored, especially if it is the delegating class that is creating the delegate. Do not fall into the trap of assuming that
delegate = weak
. Suppose you are creating a custom text view with autoformatting and the behavior is fixed. You might need to set an internaldelegate
to update/format the content accordingly, in all practicality that delegate would have to be storedstrongly
. Note that in this case the idempotent nature thedelegate
actions, mentioned earlier may be broken.
5. Weak referencing for two way ownership
Let’s consider two objects a and b. A two-way relationship is one where a has b as a property and a has b as a property. A common case of two-way ownership is delegation. A UITableviewController
for instance, will have the tableview
as a property of type UITableView
and the tableview
might have the very same UITableviewController
as a property of type stored as UITableViewDataSource
or UITableViewDelegate
or both.
class UITableViewController {
var tableView: UITableView! { get set } // owns a UITableView with strong reference
}class UITableView {
weak var delegate: UITableViewDelegate? { get set } // owns a UITableViewDelegate view with weak referenceweak var dataSource: UITableViewDataSource? { get set } // owns a UITableViewDataSource view with weak reference}
In the case of a UITableViewController,
the tableview
property is strongly owned, however in the case of a UIViewController
which has an tableview
outlet added via the nib, the tableview
outlet property will have to be held with a weak reference. Let’s attempt to explain why IB Outlets are generally weakly referenced. An outlet is declared as a weak reference (weak
) to prevent strong reference cycles. The outlet is owned by both its view controller as an outlet and by its view as a subview, when the outlet points upstream in the view hierarchy, a reference cycle is likely to be created. Since in general, one is not entirely certain that an outlet will never point upstream in the view hierarchy, it is recommended to use a weak reference to ensure that a retain cycle is never created.
class MyUIViewController: UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak private(set) var tableView: UITableView! { get set } // owns a UITableView with weak reference, linked to the a tableview added in the nib for MyUIViewController// MARK:- UITableViewDataSource// required methods for UITableViewDataSource go here// MARK:- UITableViewDelegate// required methods for UITableViewDelegate go here}class UITableView {
weak var delegate: UITableViewDelegate? { get set } // owns a UITableViewDelegate view with weak referenceweak var dataSource: UITableViewDataSource? { get set } // owns a UITableViewDataSource view with weak reference}
There are a plethora of other cases where such bidirectional ownership might be used, and in all those cases we should remember to weak reference one of the objects in the other. Here are a few cases: Let’s assume an operation manager that tracks and controls a given operation which depends on a given set of conditions. Let’s say that based on those conditions, the operation can be canceled, stopped or restarted. We need some means of informing the operation manager of any change of state in the operation. If we find ourselves working in a performance-critical system, using a polling approach will be inefficient, a suitable solution will require using a pushing approach. In other terms, the operation
has to the one notifying its manager of any of its change of state, it would, therefore, need to have knowledge (own a copy) of its manager
. Inversely, it is also primordial for the operation manager to have knowledge (own a copy) of the operation in order to command it. If we already established that weak referencing must be used in such cases, a crucial question to answer before setting the weak and strong references
adequately, is: What really owns what? A systematic approach to finding the answer to this question is to identify the object with the longer lifecycle, the latter is usually the true owner and must, therefore, be the one holding a strong reference to the other.
class Operation {
weak var opertionManager: OperationManager? { get set } // owns a OperationManager with wealreference
}class OperationManager {
let operation: Operation? { get set } // owns a OperationManager with wealreference}
6. Weak referencing when storing a list of responders or observers
In previous sections, we have discussed various cases where a single object was to be held with a reference, in this last section we will discuss the case where we need to store a collection of objects by a weak reference.
Let’s get back to our operation manager and let’s modify its behavior. Instead of commanding a single operation, let’s make it command multiple operations that might depend on each other. As previously established, each one of the operations will hold a weak reference to the operation manager. In turn, the operation manager has to hold a weak reference to each one of the operations it controls. In general, we would have fixed a number of managed operations, this imposes the use of collection
.
The collection that will help achieve weak referencing is NSPointerArray
. I suggest you now read about NSPointerArray
before continue reading this section. In summary, the NSPointerArray
array class is modeled after theNSArray
, but can also hold nil
values. You can insert or remove nil
values which contribute to the array's count
.
A pointer array can be initialized to maintain strong or weak references to objects, or according to any of the memory or personality options defined by NSPointerFunctions.Options
.
class OperationManager: NSObject { private var weakOperations =
NSPointerArray.weakObjects() // NSPointer array with
// weak referenced objects public func addOperation(_ operation: AbstractOperation) { objc_sync_enter(self) // safe to synchronize unowned var weakOperation = operation // because at this point
// we guarantee the existence of
// operation but we do not want to own it let pointer =
Unmanaged.passUnretained(weakOperation).toOpaque() weakOperations.compact() // remove operations that
// might have been deallocated weakOperations.addPointer(pointer) // insert weak reference objc_sync_exit(self) }}
Before we define aremoveOperation
, let’s explain the addOperation
in the snippet above.
Firstly, we have to define a weakOperations
property of type NSPointerArray
, we have to use the designated constructor for weakObjects
to ensure that our object will be weakly referenced.
Secondly, we define a function to help add objects into the weakOperations
property func addOperation(_ operation: AbstractOperation)
. We have synchronized our function since the insertion of objects might occur in various threads and given that we are inserting pointers, there are just so many issues, that might arise from a lack of synchronization. The discussion of these issues is beyond the scope of this article. The line unowned var weakOperation = operation
ensures that we are only adding non null
objects and that we are at no point creating a stronghold to any object inserted. This important to prevent the addition of nulls and to assert the hypothesis indicated by the non optional
AbstractOperation parameter of this method. You might omit unowned
if you wish to add an object that can be null at the time of insertion.
Thirdly, in the line let pointer =
, we create a weak pointer indicated by the
 Unmanaged.passUnretained(weakOperation).toOpaque()passUnretained
, clean up all deallocated objects weakOperations.compact()
and insert the weak pointer weakOperations.addPointer(pointer)
. Of course, we end by exiting our synchronization block.
In general, you would not need an explicit remove command since the objects are weakly stored. Every time that compact()
is called, all deallocated operations will no longer be part of the weakOperations
. However, let’s assume that for some reasons you needed an explicit removal.
// Inside OperationManager class@objc public func removeOperation(_ operation: Operation) { objc_sync_enter(self) // safe to synchronize, especially // if your operation addition // might occur in various thread weakOperations.compact() // remove operations that // might have been deallocated var validOperations: [AnyObject]? =
weakOperations.allObjects as [AnyObject]? // obtain
// remaining operationsvar operationToRemove = operation as? AnyObject // convert operation// to remove into any since we store// pointers and type specific objects if let index = validOperations?.firstIndex(where: { $0 === operationToRemove }) { validOperations = nil // remove array having strong holds weakOperations.removePointer(at: index) } objc_sync_exit(self)}
The line let validOperations = weakOperations.allObjects as [AnyObject]?
helps us create an array
storing objects of type AnyObject
. We need this array to compute the index of pointer corresponding to the object to remove. Since we store the object without any specified type
, we had to use the AnyObject
which will represent any possible object.
All we need before removing the pointer is to compute the index of the object to be removed. After we found that index, we will need to destroy the array since it has strong references to the objects it contains. To find the index, we use validOperations?.firstIndex(where: { $0 === operationToRemove })
. Note that the usage of the identity operator ===
is crucial. Using the equality operator ==
might erroneously lead to the removal of a different object. It is important to mention that equal objects in Swift, are not necessarily the same object. The result of the equality check
operation between two objects of a given type depends solely on how the given type adheres to Equatable protocol
, whereas the result of the identity check
operation depends on whether the objects share the very same memory address
and have same content in all aspects
.
A playground containing all snipped and code of this article can be found in this git repo.