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:
-
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.
-
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.
-
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.
-
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:
- 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, andContentType
, which is the type of the content view that will be displayed inside the sheet. -
It has a
@StateObject
property calledmodel
that holds an instance ofSheetWrapperModel<ItemType>
. -
It also has a
content
closure that takes an instance ofItemType
as an argument and returns aView
. This closure is used to generate the content of the sheet.
- 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 theSheetView
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.
- Object Lifetime Management:
-
The
SheetWrapperModel
class holds anunowned
reference to theItemType
. This means that theSheetWrapperModel
does not retain theItemType
. -
The
SheetWrapper
view holds a strong reference to theSheetWrapperModel
. This ensures that theSheetWrapperModel
is not released when the sheet is dismissed. -
The
ContentView
holds a strong reference to theContentModel
. TheContentModel
holds a strong reference to theSheetModel
. This ensures that theSheetModel
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.
Firebase is a platform that contains many useful features including the Firestore database which we've used in the past.
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.
Firebase is a platform that contains many useful features including the Firestore database which we've used in the past.