Memory Leaks in SwiftUI and Combine:

Shashidhar Jagatap
6 min readDec 31, 2023

--

SwiftUI:

Memory leaks in SwiftUI can occur due to various scenarios involving the management of view lifecycles and associated data.

Here are possible ways memory leaks can happen in SwiftUI:

1. Strong Reference Cycles Between Views and View Models:

  • Creating strong reference cycles between views and their associated view models or data managers can lead to memory leaks.
  • If a view strongly retains its view model, and the view model strongly refers back to the view, it forms a cyclic dependency that prevents deallocation.

2. Retaining Large Data in Views or View Models:

  • Holding strong references to large data objects, such as images, collections, or models, within views or view models without proper cleanup can cause memory buildup.
  • Keeping references to extensive data objects in long-lived views without releasing them appropriately leads to increased memory consumption.

3. Uncleaned Observers or Listeners:

  • Subscribing to notifications, NotificationCenter, or Combine publishers without properly removing observers or listeners can cause retained objects to stay in memory.
  • Forgetting to unsubscribe from notifications or Combine publishers when the view is no longer in use results in lingering objects and potential memory leaks.

4. Unreleased View References:

  • Holding strong references to views that are no longer in use or have been dismissed, especially in navigation or presentation stacks, might cause retained objects, leading to memory leaks.
  • If references to views are kept indefinitely without proper cleanup, it can cause memory consumption issues.

Preventing memory leaks in SwiftUI involves mindful management of object references, using appropriate property wrappers (@State, @Binding, @ObservedObject, @EnvironmentObject) to manage object lifecycles, breaking strong reference cycles with [weak self] in closures, and ensuring proper cleanup of observers, listeners, and unused views. Being cautious about these scenarios and adopting best practices in SwiftUI coding can help mitigate memory leak risks and ensure efficient memory management in SwiftUI-based applications.

Here are specific examples illustrating how to address potential memory leaks in SwiftUI scenarios:

Fixing Strong Reference Cycles Between Views and View Models:

Issue:

class UserViewModel: ObservableObject {
var userDetails: UserDetails = UserDetails()
// ...other properties and methods
}

struct UserDetailsView: View {
@ObservedObject var viewModel = UserViewModel()

var body: some View {
UserDetailsSubview(viewModel: viewModel)
// ...
}
}

struct UserDetailsSubview: View {
@ObservedObject var viewModel: UserViewModel

var body: some View {
// Use viewModel.userDetails...
// ...
}
}

Fix:

class UserViewModel: ObservableObject {
weak var coordinator: Coordinator?
var userDetails: UserDetails = UserDetails()
// ...other properties and methods
}

struct UserDetailsView: View {
@StateObject var viewModel = UserViewModel()

var body: some View {
UserDetailsSubview(viewModel: viewModel)
// ...
}
}

struct UserDetailsSubview: View {
@ObservedObject var viewModel: UserViewModel

var body: some View {
// Use viewModel.userDetails...
// ...
}
}

Explanation:

  • Using @StateObject instead of @ObservedObject in the parent view ensures that the view model (UserViewModel) is created only once and manages its lifecycle appropriately.
  • Employing weak references in the view model (UserViewModel) prevents strong reference cycles between the view and the view model, thus avoiding memory leaks.

Handling Uncleaned Observers or Listeners:

Issue:

struct NotificationView: View {
var body: some View {
// ...
}

init() {
NotificationCenter.default.addObserver(self, selector: #selector(handleNotification(_:)), name: NSNotification.Name("NotificationName"), object: nil)
}

@objc func handleNotification(_ notification: Notification) {
// Handle notification...
}
}

Fix:

import SwiftUI
import Combine

struct NotificationView: View {
@State private var notificationReceived = false
private var notificationPublisher = NotificationCenter.default.publisher(for: Notification.Name("NotificationName"))
.map { _ in true }
.eraseToAnyPublisher()

var body: some View {
Text("Notification Received: \(notificationReceived ? "Yes" : "No")")
.onReceive(notificationPublisher) { received in
self.notificationReceived = received
}
}
}
  • Instead of directly adding an observer using NotificationCenter, this approach uses NotificationCenter.default.publisher to create a Combine publisher for a specific notification.
  • The map operation is used to transform the notification into a boolean value (true when the notification is received).
  • eraseToAnyPublisher is applied to provide a type-erased version of the publisher.
  • onReceive is used in the SwiftUI view to listen to the publisher. When the notification is received, it updates the notificationReceived state, triggering the view to update accordingly.

These fixes demonstrate how to address specific scenarios in SwiftUI to prevent memory leaks by properly managing object lifecycles, breaking strong reference cycles, and ensuring the cleanup of observers or listeners when they are no longer needed.

Combine:

Memory leaks in Combine can occur due to various scenarios involving subscription management, retaining strong references, or improper handling of resources.

Here are possible ways memory leaks can happen in Combine:

1. Uncanceled Subscriptions:

Issue: Forgetting to cancel Combine subscriptions (`AnyCancellable`) may result in retain cycles and memory leaks, especially in long-lived objects.

Example: Creating a subscription to a publisher without storing the cancellable or canceling it properly when the object using the subscription deallocates.

2. Strong References to Publishers/Subscribers:

Issue: Holding strong references to publishers, subscribers, or closures within classes or objects can prevent their deallocation, leading to memory leaks.
Example: Retaining a strong reference to a subscriber or publisher within an object, causing the subscriber or publisher to live longer than necessary.

3. Unreleased Resources in Custom Publishers:

Issue:Improper cleanup of associated resources within custom Combine publishers might cause memory retention issues.

Example: Failing to release or clean up resources like timers, caches, or network connections within the lifecycle of a custom publisher, causing memory leaks.

4. Strong Captures in Closures:

Issue: Unintentionally capturing self strongly in closures without proper [weak self] capture or appropriate management can lead to memory retention.
Example: Not using [weak self] when capturing self in a closure that’s part of Combine’s subscription handling, causing a retain cycle.

Addressing these potential memory leak scenarios in Combine involves meticulous subscription management, avoiding strong reference cycles, properly handling resources within custom publishers, using capture lists in closures, and ensuring the cancellation of subscriptions when they’re no longer needed.

Here are specific examples illustrating how to address potential memory leaks in various Combine scenarios:

Fixing Uncanceled Subscriptions:

Issue:

class DataFetcher {
private var cancellable: AnyCancellable?

func fetchData() {
let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.sink { data in
// Handle data...
}
cancellable = publisher
}
}

Fix:

class DataFetcher {
private var cancellable: AnyCancellable?

func fetchData() {
let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.sink { [weak self] data in
// Handle data...
}
cancellable = publisher
}

func cancelFetch() {
cancellable?.cancel()
}
}

Explanation:

  • Using [weak self] capture list in the closure helps avoid a strong reference cycle, preventing the retain cycle.
  • Adding a method (cancelFetch()) to cancel the subscription (AnyCancellable) when it's no longer needed ensures proper cleanup and prevents memory leaks.

Handling Strong References to Publishers/Subscribers:

Issue:

class DataManager {
private var cancellable: AnyCancellable?

func fetchData() {
let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.sink { data in
// Handle data...
}
cancellable = publisher
}
}

Fix:

class DataManager {
private var cancellable: AnyCancellable?

func fetchData() {
let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.sink { [weak self] data in
// Handle data...
}
cancellable = publisher
}

deinit {
cancellable?.cancel()
}
}

Explanation:

  • Using [weak self] in the closure helps break the strong reference cycle, preventing memory retention.
  • Adding deinit to the class ensures that the subscription (AnyCancellable) is canceled when the object is deallocated, preventing memory leaks.

Proper Cleanup in Custom Publishers:

Issue:

class CustomPublisher {
private var cancellable: AnyCancellable?

func startPublishing() {
let publisher = // Create a custom publisher...
cancellable = publisher.sink { value in
// Handle value...
}
}
}

Fix:

class CustomPublisher {
private var cancellable: AnyCancellable?

func startPublishing() {
let publisher = // Create a custom publisher...
cancellable = publisher.sink { [weak self] value in
// Handle value...
}
}

deinit {
cancellable?.cancel()
}
}

Explanation:

  • Using [weak self] in the closure helps avoid a strong reference cycle, preventing memory retention.
  • Adding deinit to the class ensures that the subscription (AnyCancellable) is canceled when the object is deallocated, preventing memory leaks caused by the custom publisher.

By implementing these fixes, you can effectively address potential memory leaks in Combine scenarios by managing subscriptions, breaking strong reference cycles, and ensuring proper cleanup of resources, ultimately improving memory management and preventing memory leaks.

Thank you for reading…

Connect with me for more updates:

Happy Coding !!

Bonus : SwiftUI + Combine Framework

Support my efforts by :

--

--

Shashidhar Jagatap

iOS Developer | Objective C | Swift | SwiftUI | UIKit | Combine | Crafting Engaging Mobile Experiences |