Logo for tanaschita.com

Memory management in Combine

Learn how to avoid retain cycles when using the Combine framework in Swift.

12 Sep 2022 · 4 min read

Just like with any other asynchronious operations, memory management is an important part of Combine.

A subscriber needs to retain a subscription for as long as it wants to receive values. However, once a subscription is no longer needed, all references should be released correctly without causing any retain cycles. A common question in this context is whether we should use [weak self] references or not.

Let's look at some examples to better understand memory management in Combine.

Sponsorship logo
Using Proxyman to inspect network traffic
Proxyman is a native debugging proxy that can act as a man-in-the-middle between your application and web server. You can use its powerful toolkit to inspect network calls and debug your application on Mac, iOS Simulator, or remote devices effortlessly.
CLICK TO LEARN MORE

When subscribing with methods like sink(receiveValue:) or assign(to:on:), Combine returns an object of AnyCancellable type to which we should keep a reference to as long as we want to receive new values.

Let's look at the following example:

var lastUpdated = Date()
var cancellable: AnyCancellable?
func startTimer() {
cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.assign(to: \.lastUpdated, on: self)
}
func stopTimer() {
cancellable = nil
}

To keep the timer publishing values until we call the stopTimer() method, we keep a reference to AnyCancellable by storing it in a property outside of the startTimer() method.

Instead of assign(to:on:), we could use sink(receiveValue:) to achieve the same result:

var lastUpdated = Date()
var cancellable: AnyCancellable?
func startTimer() {
cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink { date in
self.lastUpdated = date
}
}
func stopTimer() {
cancellable = nil
}

While we correctly manage the AnyCancellable instance, we strongly capture self in both methods. At the same time, the cancellable property is strongly captured by self.

Which means that in both examples above, we created a retain cycle.

Weak references when using sink(receiveValue:)

To fix that, the old [weak self] comes to the rescue. When using sink(receiveValue:) to subscribe, we can fix the retain cycle as follows:

.sink { [weak self] date in
self?.lastUpdated = date
}

With that in place, the memory leak is solved. By using [weak self], we capture self as a weak reference instead of a strong reference.

Weak references when using assign(to:on:)

Unfortunately, the assign(to:on:) method doesn't provide a possibility to capture self as a weak reference. If we want to use it anyway, we can create an extension as a workaround:

extension Publisher where Self.Failure == Never {
public func assignWeak<T: AnyObject>(to keyPath: ReferenceWritableKeyPath<T, Self.Output>, on object: T) -> AnyCancellable {
sink { [weak object] (value) in
object?[keyPath: keyPath] = value
}
}
}

In the code above, we extend Combine's Publisher to add the assignWeak(to:on:) method which uses the sink method internally to avoid a strong reference.

The extension allows us to create a weak referenced keypath assignment in one line:

.assignWeak(to: \.lastUpdated, on: self)

Using assign(to:on:) with Published properties

Another way to avoid the retain cycle is to assign to a @Published property.

@Published private(set) var lastUpdated = Date()
func startTimer() {
Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.assign(to: &$lastUpdated)
}

In this case, we don't keep a reference to a Cancellable, Combine manages the subscription based on the lifetime of the lastUpdated property. Which avoids a strong reference cycle. But since there is no Cancellable instance being returned in this approach, we also loose the ability to cancel the timer.

Sponsorship logo
Using Proxyman to inspect network traffic
Proxyman is a native debugging proxy that can act as a man-in-the-middle between your application and web server. You can use its powerful toolkit to inspect network calls and debug your application on Mac, iOS Simulator, or remote devices effortlessly.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

combine

async/await

concurrency

reactive programming

swift

How to bridge async/await functions to Combine's Future type in Swift

Learn how to call async/await code within Combine based APIs.

22 Aug 2022 · 2 min read

Latest articles and tips

© 2022 tanaschita.com

Privacy policy

Impressum