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)
| 归档时间: |
|
| 查看次数: |
2259 次 |
| 最近记录: |