SwiftUI Sheet never releases object from memory – Swift

by
Alexei Petrov
memory-leaks swift-concurrency swiftui-ontapgesture

Quick Fix: Employ Apple’s workaround until the bug is fixed. Create custom presentation controllers with UIKit’s UIViewController, UIHostingController, and corresponding views.

The Problem:

In SwiftUI, when using sheet or fullScreenCover with an item parameter, objects passed to the item are held in memory even after the sheet or modal is dismissed. This issue is observed in iOS 17 built with Xcode 15 causing memory leaks. However, the behavior was functioning correctly in iOS 16 with both Xcode 14 and 15. It is important to note that Apple has resolved this bug in iOS 17.2, affecting only iOS versions 17.0…17.1.

The Solutions:

Solution 2: Bug reporting to Apple Feedback Assistant

The recommended solution is to report the issue to Apple via the Feedback Assistant app on Mac or the website feedbackassistant.apple.com.

This approach is advantageous because:

  1. Direct Input to Apple Engineers: By reporting the bug through the Feedback Assistant app or website, you are directly communicating your observations and concerns to Apple’s engineering team. This increases the likelihood of your issue being addressed promptly.

  2. Detailed Feedback: Feedback Assistant allows you to provide detailed explanations of the problem, including screenshots, logs, and any relevant information that can help Apple engineers understand and replicate the issue. This can expedite the resolution process.

  3. Official Bug Tracking: When you submit a bug report through Feedback Assistant, it is assigned a unique identifier and tracked by Apple’s engineering team. This ensures that your issue is not overlooked or lost in the communication process.

  4. Updates and Resolution: Once you submit your bug report, you can track its progress and receive updates on the status of the issue. If a fix or workaround becomes available, Apple will notify you through the Feedback Assistant app or via email.

Utilizing Feedback Assistant for bug reporting offers a structured and effective channel to communicate directly with Apple engineers, ensuring that your issue receives the attention it deserves.

Solution 3: Create a wrapper view for the sheet content

This solution involves creating a wrapper view for the sheet content that holds a strong reference to the view model. This ensures that the view model is not released when the sheet is dismissed. Here's how it works:

  1. Create a Wrapper View:
struct SheetWrapper<ItemType: AnyObject, ContentType: View>: View {
    @StateObject var model: SheetWrapperModel<ItemType>
    var content: (ItemType) -> ContentType

    var body: some View {
        content(model.item)
    }
}
  • This wrapper view takes two generic parameters: ItemType, which is the type of the view model, and ContentType, which is the type of the content view that will be displayed inside the sheet.

  • It has a @StateObject property called model that holds an instance of SheetWrapperModel<ItemType>.

  • It also has a content closure that takes an instance of ItemType as an argument and returns a View. This closure is used to generate the content of the sheet.

  1. Use the Wrapper View in Your Code:
struct ContentView: View {
    @ObservedObject var model = ContentModel()

    var body: some View {
        Button("Show sheet") {
            model.sheet = SheetModel()
            model.isPresented = true
        }
        .sheet(
            isPresented: $model.isPresented,
            onDismiss: { model.sheet = nil },
            content: {
                SheetWrapper(model: .init(item: model.sheet!)) { item in
                    SheetView(viewModel: item)
                }
            }
        )
    }
}
  • In this code, the SheetWrapper view is used to wrap the SheetView and pass the sheet view model to it.

  • The isPresented binding is used to control the presentation of the sheet.

  • The onDismiss closure is used to release the sheet view model when the sheet is dismissed.

  1. Object Lifetime Management:
  • The SheetWrapperModel class holds an unowned reference to the ItemType. This means that the SheetWrapperModel does not retain the ItemType.

  • The SheetWrapper view holds a strong reference to the SheetWrapperModel. This ensures that the SheetWrapperModel is not released when the sheet is dismissed.

  • The ContentView holds a strong reference to the ContentModel. The ContentModel holds a strong reference to the SheetModel. This ensures that the SheetModel is not released when the sheet is dismissed.

This solution effectively prevents the memory leak by ensuring that the view model is retained by the presenter (ContentView/ContentModel) and not by the sheet view (SheetView).

Solution 4: Boolify the binding

To resolve the memory leak issue caused by the SwiftUI Sheet never releasing the object from memory, you can utilize the `boolify` extension to transform the optional item binding into a Boolean binding. This allows you to employ the `isPresented:..` variant of the sheet, which doesn’t suffer from the memory leak problem.

Here’s an example implementation of the `boolify` extension:

extension Binding {    
  static func boolify<T: Any>(_ binding: Binding<T?>) -> Binding<Bool> {
    Binding<Bool> {
      binding.wrappedValue != nil
    } set: { newValue in
      guard !newValue else {
        assertionFailure("only false is valid")
        return
      }
      binding.wrappedValue = nil
    }
  }
}

With this extension, you can modify your code to use the `isPresented:..` variant of the sheet as follows:

struct CoordinatorView: View {
  @State var isSheetPresented = false // Boolean binding
  @State var sheetVM: SheetVM? // Optional view model

  var body: some View {
    Button {
      sheetVM = .init(dataGetter: {
        /// External injection needed here. This is just a simplified example
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return "New title"
      })
      isSheetPresented = true
    } label: {
      Text("Navigate")
    }
    .sheet(isPresented: $isSheetPresented) { // Using isPresented binding
      SheetView(viewModel: sheetVM)
    }
  }
}

By utilizing the `boolify` extension, you can convert the optional item binding to a Boolean binding, enabling the use of the `isPresented:..` variant. This approach should resolve the memory leak issue while still allowing you to navigate to the sheet view.

Q&A

Why SwiftUI Sheet never releases object from memory on iOS 17?

Sheet or fullScreenCover on iOS 17 creates memory leak on iOS 17 and not releasing object that were passed via item: parameter.

How to use builiding UIKit bridge to avoid memory leaks?

Apple engineers suggested the workaround where you can use bridge to UIKit to create your own presentation controllers above your SwiftUI content.

How to boolify the optional item binding?

Create an extension with function boolify which takes binding and modifies it to work with isPresented binding variant of the sheet.

Video Explanation:

The following video, titled "Uploading Images to Firebase Storage (and retrieving them ...", provides additional insights and in-depth exploration related to the topics discussed in this post.

Play video

Firebase is a platform that contains many useful features including the Firestore database which we've used in the past.