Luk*_*nek 10 swiftui observedobject swiftui-sheet
我\xe2\x80\x99在 SwiftUI 中遇到了与工作表演示相关的令人费解的行为。关闭工作表时,我注意到关联的实例(工作表\xe2\x80\x99s 视图持有的视图模型)don\xe2\x80\x99t 似乎已正确取消初始化。
\n根据我的测试,唯一deinit按预期被调用的场景是使用@StateObject. 相反,@ObservedObject新@Observable宏 don\xe2\x80\x99t 似乎都会触发调用deinit。
下面,我\xe2\x80\x99ve提供了一组展示各种场景的示例。每个尝试以不同的方式提供视图模型。要测试解雇,您只需在显示的工作表上向下滑动即可:
\nimport SwiftUI\n\n// ============================================================================ //\n// MARK: - App\n// ============================================================================ //\n\n@main\nstruct SwiftUISheetDeinitIssueApp: App {\n var body: some Scene {\n WindowGroup {\n CaseA_ContentView()\n }\n }\n}\n\n// ============================================================================ //\n// MARK: - Case A: @StateObject (Works!)\n// ============================================================================ //\n\nstruct CaseA_ContentView: View {\n @State var isPresented = false\n \n var body: some View {\n Button("Show Sheet") {\n self.isPresented = true\n }\n .sheet(isPresented: $isPresented) {\n CaseA_SheetView()\n }\n }\n}\n\nstruct CaseA_SheetView: View {\n @StateObject var model = CaseA_SheetViewModel()\n \n var body: some View {\n Text("Sheet")\n }\n}\n\nfinal class CaseA_SheetViewModel: ObservableObject {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\n\n// ============================================================================ //\n// MARK: - Case B: @ObservedObject & Inline Model Creation (Doesn\'t Work!)\n// ============================================================================ //\n\nstruct CaseB_ContentView: View {\n @State var isPresented = false\n \n var body: some View {\n Button("Show Sheet") {\n self.isPresented = true\n }\n .sheet(isPresented: $isPresented) {\n CaseB_SheetView(model: CaseB_SheetViewModel())\n }\n }\n}\n\nstruct CaseB_SheetView: View {\n @ObservedObject var model: CaseB_SheetViewModel\n \n init(model: CaseB_SheetViewModel) {\n self.model = model\n }\n \n var body: some View {\n Text("Sheet")\n }\n}\n\nfinal class CaseB_SheetViewModel: ObservableObject {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\n\n// ============================================================================ //\n// MARK: - Case C: @ObservedObject + @State in Parent (Doesn\'t Work!)\n// ============================================================================ //\n\nstruct CaseC_ContentView: View {\n @State var sheetViewModel: CaseC_SheetViewModel?\n \n var body: some View {\n Button("Show Sheet") {\n self.sheetViewModel = CaseC_SheetViewModel()\n }\n .sheet(item: self.$sheetViewModel) { model in\n CaseC_SheetView(model: model)\n }\n }\n}\n\nstruct CaseC_SheetView: View {\n @ObservedObject var model: CaseC_SheetViewModel\n \n init(model: CaseC_SheetViewModel) {\n self.model = model\n }\n \n var body: some View {\n Text("Sheet")\n }\n}\n\nfinal class CaseC_SheetViewModel: ObservableObject, Identifiable {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\n\n// ============================================================================ //\n// MARK: - Case D: Content @StateObject + Sheet @ObservedObject (Doesn\'t Work!)\n// ============================================================================ //\n\nstruct CaseD_ContentView: View {\n @StateObject var model = CaseD_ContentViewModel()\n \n var body: some View {\n Button("Show Sheet") {\n self.model.sheetViewModel = CaseD_SheetViewModel()\n }\n .sheet(item: self.$model.sheetViewModel) { model in\n CaseD_SheetView(model: model)\n }\n }\n}\n\nfinal class CaseD_ContentViewModel: ObservableObject, Identifiable {\n @Published\n var sheetViewModel: CaseD_SheetViewModel?\n}\n\nstruct CaseD_SheetView: View {\n @ObservedObject var model: CaseD_SheetViewModel\n \n init(model: CaseD_SheetViewModel) {\n self.model = model\n }\n \n var body: some View {\n Text("Sheet")\n }\n}\n\nfinal class CaseD_SheetViewModel: ObservableObject, Identifiable {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\n\n// ============================================================================ //\n// MARK: - Case E: @Observable\n// ============================================================================ //\n\nstruct CaseE_ContentView: View {\n @State\n var sheetViewModel: CaseE_SheetViewModel?\n \n var body: some View {\n Button("Show Sheet") {\n self.sheetViewModel = CaseE_SheetViewModel()\n }\n .sheet(item: self.$sheetViewModel) { model in\n CaseE_SheetView(model: model)\n }\n }\n}\n\nstruct CaseE_SheetView: View {\n let model: CaseE_SheetViewModel\n \n init(model: CaseE_SheetViewModel) {\n self.model = model\n }\n \n var body: some View {\n Text("Sheet")\n }\n}\n\n@Observable\nfinal class CaseE_SheetViewModel: Identifiable {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\n\n// ============================================================================ //\n// MARK: - Case G\n// ============================================================================ //\n\nstruct CaseG_ContentView: View {\n @State var isPresented = false\n \n var body: some View {\n Button("Show Sheet") {\n self.isPresented = true\n }\n .sheet(isPresented: $isPresented) {\n CaseG_SheetView(model: CaseG_SheetViewModel())\n }\n }\n}\n\nstruct CaseG_SheetView: View {\n @StateObject var model: CaseG_SheetViewModel\n \n init(model: CaseG_SheetViewModel) {\n self._model = StateObject(wrappedValue: model)\n }\n \n var body: some View {\n Text("Sheet")\n }\n}\n\nfinal class CaseG_SheetViewModel: ObservableObject {\n init() { print("\\(Self.self).\\(#function)") }\n deinit { print("\\(Self.self).\\(#function)") }\n}\nRun Code Online (Sandbox Code Playgroud)\n当我启动内存图时,它显示该类的活动实例CaseX_SheetViewModel,并带有指向它的以下引用:
<AnyViewStorage<ModifiedContent<SheetContent<CaseX_SheetView>, _EnvironmentKeyWritingModifier<Binding<PresentationMode>>>>: 0x281f60640>\nRun Code Online (Sandbox Code Playgroud)\n我的所有测试都是在运行最新 iOS 17 测试版的真实设备以及 iOS 17 模拟器上使用最新的 Xcode 15 RC 完成的。
\n是否缺少 I\xe2\x80\x99m 的内容,或者这是 SwiftUI 中的错误?如果是后者,有人找到合适的解决方法吗?
\n@StateObjectin view\xe2\x80\x99s 初始值设定项的模式(请参阅修改后的案例 G)。不幸的是,它没有解决\xe2\x80\x99 问题。onDismiss或onDisappear)的信息,以使其停止长时间运行的任务。最后,一个月后,我在他们的论坛上发现了苹果关于这个错误的官方声明。这篇文章确认了 SwiftUI\xe2\x80\x99ssheet和fullScreenCover演示修改器中的内存泄漏是一个已知问题,并建议使用 UIKit 的解决方法。他们\xe2\x80\x99ve还提供了完整的代码片段以供参考。
正如 @JinwooKim在他们的回答中指出的那样,这个问题似乎在 iOS 17.2 上得到了解决。我可以通过上面的所有案例来确认这一点!
\n这是我的解决方案,不使用 UIKit 演示风格。
注意:这是非常不安全的。不要在生产代码中使用它!
用法:
.fullScreenCover(isPresented: $isPresenting) {
SheetView_2()
.fixMemoryLeak()
}
Run Code Online (Sandbox Code Playgroud)
import UIKit
import SwiftUI
fileprivate let storageKey: UnsafeMutableRawPointer = .allocate(byteCount: 1, alignment: 1)
fileprivate let willDealloc: UnsafeMutableRawPointer = .allocate(byteCount: 1, alignment: 1)
fileprivate var didSwizzle: Bool = false
extension View {
func fixMemoryLeak() -> some View {
if #available(iOS 17.2, *) {
return self
} else if #available(iOS 17.0, *) {
swizzle()
return background {
FixLeakView()
}
} else {
return self
}
}
}
fileprivate struct FixLeakView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
.init()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
@MainActor final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.isUserInteractionEnabled = false
view.backgroundColor = .clear
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
guard
let type: UIViewController.Type = NSClassFromString("_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_") as? UIViewController.Type,
let hostingController: UIViewController = parentViewController(for: type) else {
return
}
if
let delegate = Mirror(reflecting: hostingController).children.first(where: { $0.label == "delegate" })?.value,
let some = Mirror(reflecting: delegate).children.first(where: { $0.label == "some" })?.value,
let presentationState = Mirror(reflecting: some).children.first(where: { $0.label == "presentationState" })?.value,
let base = Mirror(reflecting: presentationState).children.first(where: { $0.label == "base" })?.value,
let requestedPresentation = Mirror(reflecting: base).children.first(where: { $0.label == "requestedPresentation" })?.value,
let value = Mirror(reflecting: requestedPresentation).children.first(where: { $0.label == ".0" })?.value,
let content = Mirror(reflecting: value).children.first(where: { $0.label == "content" })?.value,
let storage = Mirror(reflecting: content).children.first(where: { $0.label == "storage" })?.value
{
objc_setAssociatedObject(hostingController, storageKey, storage, .OBJC_ASSOCIATION_ASSIGN)
}
}
}
}
fileprivate func swizzle() {
guard !didSwizzle else { return }
defer { didSwizzle = true }
let method: Method = class_getInstanceMethod(
NSClassFromString("_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_")!,
#selector(UIViewController.viewDidDisappear(_:))
)!
let original_imp: IMP = method_getImplementation(method)
let original_func = unsafeBitCast(original_imp, to: (@convention(c) (UIViewController, Selector, Bool) -> Void).self)
let new_func: @convention(block) (UIViewController, Bool) -> Void = { x0, x1 in
if
x0.isMovingFromParent || x0.isBeingDismissed,
let storage: AnyObject = objc_getAssociatedObject(x0, storageKey) as? AnyObject,
!(storage is NSNull),
objc_getAssociatedObject(storage, willDealloc) as? Bool ?? true
{
Task { @MainActor [unowned storage] in
// guard try? await Task.sleep(for: .seconds(0.3)) else {
// return
// }
let retainCount: UInt = _getRetainCount(storage)
let umanaged: Unmanaged<AnyObject> = .passUnretained(storage)
for _ in 0..<retainCount - 1 {
umanaged.release()
}
}
objc_setAssociatedObject(storage, willDealloc, true, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
original_func(x0, #selector(UIViewController.viewDidDisappear(_:)), x1)
}
let new_imp: IMP = imp_implementationWithBlock(new_func)
method_setImplementation(method, new_imp)
}
extension UIViewController {
fileprivate func parentViewController(for type: UIViewController.Type) -> UIViewController? {
var responder: UIViewController? = parent
while let _responder: UIViewController = responder {
if _responder.isKind(of: type) {
return _responder
}
responder = _responder.parent
}
return nil
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1233 次 |
| 最近记录: |