Environment objects injected on a TabView that contains a NavigationStack never deallocate
Originator: | argentumko | ||
Number: | rdar://FB13687535 | Date Originated: | |
Status: | Open | Resolved: | |
Product: | SwiftUI | Product Version: | iOS 17.4 |
Classification: | Incorrect/Unexpected Behavior | Reproducible: | Always |
In a SwiftUI app that contains a TabView with one or more NavigationStack's inside it, using `environment(...)` or `environmentObject(...)` modifier causes the injected objects to leak, not being released and deallocated even once the tab view goes away. We've been able to diagnose that the root cause of it is that environment values/objects are inserted in the corresponding UIKit trait environment, and under the above conditions the trait collection gets captured by: 1. A globally cached (and thus never deallocated) UILabel created by the first UINavigationBar in the app. 2. A private pointer interaction object created by UINavigationBar, registered with the UIWindow, but under some circumstances never deregistered (and thus leaking). Please refer to the attached demo project for a code sample that demonstrates the problem. We've also included the workarounds we've found to the above issues which may shed some light on the problem. This issue occurs at least on iOS 16.4 and iOS 17.2-17.4 (both device and simulator), but may have existed in many past iOS releases too. Building using Xcode 15.2-15.3.
Comments
Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!
Contents of the attached sample project:
@main struct MemoryLeakDemoApp: App { var body: some Scene { WindowGroup { ContentView() } } }
struct ContentView: View {
}
struct LeakyView: View {
}
struct CheckerView: View {
}
final class Canary: ObservableObject {
}
/// As of iOS 13-17.2 (and likely in newer versions too), SwiftUI environment (both values and objects) applied to a
TabView
that contains aNavigationView/Stack
leaks due to being injected into a UIKit trait collection of the underlying navigation controller, which then gets captured by: /// - AUILabel
cached globally byUINavigationBar
for the purposes of large title label measurement (only if it's the first navigation bar created in the entire app). /// - A pointer interaction's helper view that gets registered with the window byUINavigation
but never deregistered. /// Members of this extension work around these leaks. extension UIApplication {}
// MARK: - Private extensions
private extension UIApplication {
}