SwiftUI Sheet 永远不会从内存中释放对象

bod*_*ich 17 memory-leaks swift swiftui

我发现一个非常奇怪的行为,sheet 或 fullScreenCover 不会释放传递给其 item: 参数的对象。

运行良好,并且在使用 Xcode 14 或 15 构建的iOS 16上释放了内存。 (模拟器、设备)

使用 Xcode 15 构建的iOS 17上内存泄漏且未释放。(模拟器,设备 17.0.2)

有人从苹果那里找到过这方面的信息吗?

更新: Apple 已在 iOS 17.2 上修复了此错误。因此,我们仅在 iOS 17.0...17.1 上出现此内存泄漏。

struct CoordinatorView: View {
    @State var sheetVM: SheetVM?
    
    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"
            })
        } label: {
            Text("Navigate")
        }
        .sheet(item: $sheetVM) { sheetVM in
            SheetView(viewModel: sheetVM)
        }
    }
}

final class SheetVM: ObservableObject, Identifiable {
    @Published var title: String = "Title"
    private let dataGetter: () async throws -> String
    
    init(dataGetter: @escaping () async throws -> String) {
        self.dataGetter = dataGetter
        print("SheetVM init")
        Task { [refresh] in
            await refresh()
        }
    }
    
    @MainActor
    @Sendable
    private func refresh() async {
        do {
            title = try await dataGetter()
        } catch {
            print("Failed to get data")
        }
    }
    
    deinit { print("... SheetVM deinit") }
}

struct SheetView: View {
    @ObservedObject var viewModel: SheetVM
    
    var body: some View {
        Text("\(viewModel.title)")
    }
}

struct SheetView_Previews: PreviewProvider {
    static var previews: some View {
        CoordinatorView()
    }
}
Run Code Online (Sandbox Code Playgroud)

bod*_*ich 11

Apple 工程师已经认识到,工作表内存泄漏是 iOS 17 上的一个错误和已知问题 (r. 115856582),影响工作表和全屏显示。他们提出了一种解决方法,您可以使用 UIKit 桥接在 SwiftUI 内容之上创建自己的演示控制器(防止内存保留问题)。

https://developer.apple.com/forums/thread/737967?answerId=767599022#767599022

这是完整的代码片段(尽管它不支持 Binding 对象,所以你不能使用类似的东西.sheet(item: $object)

import SwiftUI

enum SheetPresenterStyle {
    case sheet
    case popover
    case fullScreenCover
    case detents([UISheetPresentationController.Detent])
}

class SheetWrapperController: UIViewController {
    let style: SheetPresenterStyle
    
    init(style: SheetPresenterStyle) {
        self.style = style
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if case let (.detents(detents)) = style, let sheetController = self.presentationController as? UISheetPresentationController {
            sheetController.detents = detents
            sheetController.prefersGrabberVisible = true
        }
    }
}

struct SheetPresenter<Content>: UIViewRepresentable where Content: View {
    let label: String
    let content: () -> Content
    let style: SheetPresenterStyle
    
    init(_ label: String, style: SheetPresenterStyle, @ViewBuilder content: @escaping () -> Content) {
        self.label = label
        self.content = content
        self.style = style
    }
    
    func makeUIView(context: UIViewRepresentableContext<SheetPresenter>) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(label, for: .normal)
        
        let action = UIAction { _ in
            let hostingController = UIHostingController(rootView: content())
            hostingController.view.translatesAutoresizingMaskIntoConstraints = false
            
            let viewController = SheetWrapperController(style: style)
            switch style {
            case .sheet:
                viewController.modalPresentationStyle = .automatic
            case .popover:
                viewController.modalPresentationStyle = .popover
                viewController.popoverPresentationController?.sourceView = button
            case .fullScreenCover:
                viewController.modalPresentationStyle = .fullScreen
            case .detents:
                viewController.modalPresentationStyle = .automatic
            }
            
            viewController.addChild(hostingController)
            viewController.view.addSubview(hostingController.view)
            
            NSLayoutConstraint.activate([
                hostingController.view.topAnchor.constraint(equalTo: viewController.view.topAnchor),
                hostingController.view.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
                hostingController.view.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
                hostingController.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor),
            ])
            
            hostingController.didMove(toParent: viewController)
            
            if let rootVC = button.window?.rootViewController {
                rootVC.present(viewController, animated: true)
            }
        }
        
        button.addAction(action, for: .touchUpInside)
        return button
    }
    
    func updateUIView(_ uiView: UIButton, context: Context) {}
}

typealias ContentView = ContentViewB

struct ContentViewA: View {
    @State private var showSheet = false
    @State private var showPopover = false
    @State private var showFullScreenCover = false
    
    var body: some View {
        VStack {
            Button("Present Sheet") { showSheet.toggle() }
            Button("Present Popover") { showPopover.toggle() }
            Button("Present Full Screen Cover") { showFullScreenCover.toggle() }
            
            Text("First")
                .sheet(isPresented: $showSheet) {
                    SheetView()
                }
                .popover(isPresented: $showPopover) {
                    PopoverView()
                }
                .fullScreenCover(isPresented: $showFullScreenCover) {
                    FullScreenCoverView()
                }
        }
    }
}

struct ContentViewB: View {
    
    var body: some View {
        VStack {
            SheetPresenter("Present Sheet", style: .sheet) {
                SheetView()
            }
            SheetPresenter("Present Popover", style: .popover) {
                PopoverView()
            }
            SheetPresenter("Present Full Screen Cover", style: .fullScreenCover) {
                FullScreenCoverView()
            }
            SheetPresenter("Present Presentation Detents", style: .detents([.medium(), .large()])) {
                PresentationDetentsView()
            }
            Text("First")
        }
    }
}

struct SheetView: View {
    private let log = LifecycleLogger(name: "SheetView")
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Text("SheetView")
        Button("Back") { dismiss() }
    }
}

struct PopoverView: View {
    private let log = LifecycleLogger(name: "PopoverView")
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Text("PopoverView")
        Button("Back") { dismiss() }
    }
}

struct FullScreenCoverView: View {
    private let log = LifecycleLogger(name: "FullScreenCoverView")
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Text("FullScreenCoverView")
        Button("Back") { dismiss() }
    }
}

struct PresentationDetentsView: View {
    private let log = LifecycleLogger(name: "PresentationDetentsView")
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        Text("PresentationDetentsView")
        Button("Back") { dismiss() }
    }
}

class LifecycleLogger {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name).init")
    }
    
    deinit {
        print("\(name).deinit")
    }
}
Run Code Online (Sandbox Code Playgroud)


mal*_*hal -3

(我已经更新了我的答案,因此以前的所有评论都不再相关,当您有空闲时间时应将其删除,谢谢)。

此 SwiftUI 代码存在几个主要问题。第一个是视图模型对象,在 SwiftUI 中,视图结构层次结构旨在保存视图数据,因此它本质上已经是一个视图模型。第二个被设计为仅适用于值类型,而不适用于对象,因为(正如您所经历的)它们在不再需要时@State不会被设置,因此对象永远不会被释放。nil对于内存堆栈上的值类型来说这不是问题,但对于堆上的对象来说这是一个大问题。

在这种情况下,最好不要使用对象,但如果您确实想要,可以通过实现自己的版本来解决该问题,如下所示:

struct SheetView: View {
    @State var sheetConfig = SheetConfig()
    
    var body: some View {
        Button {
            sheetConfig.present()
        } label: {
            Text("Navigate")
        }
        .sheet(isPresented: $sheetConfig.isPresented) {
            Color.yellow
        }
    }
}

struct SheetConfig {
    var object: SheetVM?
    
    var isPresented: Bool = false {
        didSet {
            if !isPresented {
                object = nil
            }
        }
    }
    
    mutating func present() {
        object = SheetVM()
        isPresented = true
    }
}

// Health warning, don't try to implement view models using Combine's ObservableObject and instead learn the View struct for your view data.
final class SheetVM: ObservableObject {
    @Published var title: String = "Title"
    
    init() {
        print("SheetVM init")
    }
    
    deinit {
        print("... SheetVM deinit")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 哈哈。我需要在这里上课。我没有问是否需要使用struct。问题是关于使用表释放内存,如果我们能专注于它,我将不胜感激。 (3认同)
  • 因为我放入工作表中的视图(显然没有在本示例中添加,因为它与问题无关)接收 ObservableObject 作为参数并具有 @ObservedObject 属性。我认为很明显我会进一步通过sheetVM。绝对不只是在工作表内显示 Color.yellow。现在问“为什么视图中需要 ObservedObject,Apple 告诉我们不要在视图中使用类......”。哦等等,苹果并没有告诉我们这一点,他们甚至创建了专门用于视图的 ObservedObject 属性包装器。 (3认同)
  • 这是一个简化的示例。我不会在这里上传整个项目。当然,我在工作表中使用 ViewModel 类。我不需要改变那个类来构造,你的建议有什么意义?我需要上课! (2认同)
  • 在 SwiftUI 中,View 结构已经是一个视图模型,因此您不需要类。另外,由于这是 Swift,所以一般来说你应该更喜欢结构而不是类。 (2认同)
  • 现在您可以通过很多链接看到这个答案,其他人也面临这个确切的问题。没有“这是因为您正在使用状态类”。这是一个多次向 Apple 报告的错误,Apple 工程师只是说感谢您的报告,而不是说不使用类。/sf/answers/5406471871/ (2认同)
  • 你在说什么?我从来没有说过他们对国家有任何意见。他们用sheet和fullScreenCover犯了一个错误。 (2认同)
  • 告诉我在这种情况下,状态包装器下有什么类。你甚至没有尝试去理解,无论是问题还是 SwiftUI。如果我从按钮更新状态初始化,为什么要告诉我有关状态初始化的信息,在我的示例中,我不会每次都初始化它,它仅在 SheetView 隐式初始化上初始化一次。/sf/ask/5400730961/ (2认同)