为何设置UISheetPresentationController.prefersGrabberVisible会导致类无法释放?
问题描述
设置UISheetPresentationController的prefersGrabberVisible属性为true后,StateObjectExampleCoordinator.featureEViewModel实例无法执行deinit方法,预期行为是弹出ViewController时该实例会被正常释放。
代码模块
UIKit导航包装器
// // ViewControllable.swift // WeakUnownedExample // // Created by Jason Rich Darmawan Onggo Putra on 02/08/23. // import SwiftUI final class NavStackHolder { let id: UUID // try to remove weak // then navigate: to a page -> go back // the NavStack holder will not deinit. weak var viewController: UIViewController? init(id: UUID = UUID()) { self.id = id print("\(type(of: self)) \(#function) \(id.uuidString)") } deinit { print("\(type(of: self)) \(#function) \(id.uuidString)") } } protocol ViewControllable: View { var holder: NavStackHolder { get set } func viewWillAppear(_ viewController: UIViewController) func viewDidAppear(_ viewController: UIViewController) func viewWillDisappear(_ viewController: UIViewController) func viewDidDisappear(_ viewController: UIViewController) } extension ViewControllable { var viewController: UIViewController { let viewController = HostingController(rootView: self) self.holder.viewController = viewController return viewController } func viewWillAppear(_ viewController: UIViewController) {} func viewDidAppear(_ viewController: UIViewController) {} func viewWillDisappear(_ viewController: UIViewController) {} func viewDidDisappear(_ viewController: UIViewController) {} } final class HostingController<ContentView>: UIHostingController<ContentView> where ContentView: ViewControllable { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) rootView.viewWillAppear(self) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) rootView.viewDidAppear(self) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) rootView.viewWillDisappear(self) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) rootView.viewDidDisappear(self) } } final class NavigationController: UINavigationController { let id: UUID = UUID() init() { super.init(nibName: nil, bundle: nil) print("\(type(of: self)) \(#function) \(id.uuidString)") } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { print("\(type(of: self)) \(#function) \(id.uuidString)") } var willPopHandler: (() -> Bool)? override func popViewController(animated: Bool) -> UIViewController? { _ = willPopHandler?() willPopHandler = nil return super.popViewController(animated: animated) } }
SceneDelegate代码
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) let navigationController = NavigationController() let rootCoordinator = RootCoordinator(navigationController: navigationController) window?.rootViewController = rootCoordinator.navigationController window?.makeKeyAndVisible() _ = rootCoordinator.enter() }
根协调器
// // RootView.swift // WeakUnownedExample // // Created by Jason Rich Darmawan Onggo Putra on 02/08/23. // import SwiftUI final class RootCoordinator { let id: UUID unowned let navigationController: NavigationController unowned var stateObjectCoordinator: StateObjectExampleCoordinator? init( id: UUID = UUID(), navigationController: NavigationController = NavigationController() ) { self.id = UUID() self.navigationController = navigationController print("\(type(of: self)) \(#function) \(id.uuidString)") } deinit { print("\(type(of: self)) \(#function) \(id.uuidString)") } func enter() -> Bool { let view = RootView(coordinator: self) navigationController.pushViewController(view.viewController, animated: false) return true } func enterStateObjectCoordinator() -> Bool { let coordinator = StateObjectExampleCoordinator(navigationController: navigationController) stateObjectCoordinator = coordinator guard let stateObjectCoordinator else { return false } return stateObjectCoordinator.enter() } } struct RootView: ViewControllable { var holder: NavStackHolder var coordinator: RootCoordinator init( holder: NavStackHolder = NavStackHolder(), coordinator: RootCoordinator = RootCoordinator() ) { self.holder = holder self.coordinator = coordinator } var body: some View { List { Button { _ = coordinator.enterStateObjectCoordinator() } label: { Text("State Object Example") } } } func viewWillAppear(_ viewController: UIViewController) { viewController.navigationController?.setNavigationBarHidden(true, animated: false) } }
StateObjectExample协调器及相关代码
// // StateObjectExample.swift // WeakUnownedUIKitNavigationExample // // Created by Jason Rich Darmawan Onggo Putra on 04/08/23. // import SwiftUI final class StateObjectExampleCoordinator: NSObject { let id: UUID unowned let navigationController: UINavigationController unowned var featureEViewModel: FeatureEViewModel? init( id: UUID = UUID(), navigationController: UINavigationController = NavigationController() ) { self.id = id self.navigationController = navigationController print("\(type(of: self)) \(#function) \(id.uuidString)") } deinit { print("\(type(of: self)) \(#function) \(id.uuidString)") } func enter() -> Bool { let featureEViewModel = FeatureEViewModel() let view = FeatureEView(coordinator: self, featureEViewModel: featureEViewModel) self.featureEViewModel = featureEViewModel navigationController.pushViewController(view.viewController, animated: true) // dismiss then pop. guard let navigationController = navigationController as? NavigationController else { return false } navigationController.willPopHandler = { self.dismiss(animated: false) } return true } func present() -> Bool { guard let featureEViewModel else { return false } let view = FeatureFView(coordinator: self, featureEViewModel: featureEViewModel) let viewControllerToPresent = view.viewController guard let sheetController = viewControllerToPresent.sheetPresentationController else { return false } sheetController.detents = [.medium(), .large()] sheetController.largestUndimmedDetentIdentifier = .medium // bug: class object will not deinit // sheetController.prefersGrabberVisible = true sheetController.delegate = self navigationController.present(viewControllerToPresent, animated: true) return true } func dismiss(animated: Bool) -> Bool { navigationController.dismiss(animated: animated) return true } } extension StateObjectExampleCoordinator: UISheetPresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { navigationController.popViewController(animated: true) } } struct FeatureFView: ViewControllable { var holder: NavStackHolder let coordinator: StateObjectExampleCoordinator @ObservedObject var featureEViewModel: FeatureEViewModel init( holder: NavStackHolder = NavStackHolder(), coordinator: StateObjectExampleCoordinator = StateObjectExampleCoordinator(), featureEViewModel: FeatureEViewModel = FeatureEViewModel() ) { self.holder = holder self.coordinator = coordinator self.featureEViewModel = featureEViewModel } var body: some View { VStack { Text("Feature F") Text("count \(featureEViewModel.count)") Button { featureEViewModel.count += 1 } label: { Text("increment") } } } } final class FeatureEViewModel: ObservableObject { let id: UUID @Published var count: Int init(id: UUID = UUID(), count: Int = 0) { self.id = id self.count = count print("\(type(of: self)) \(#function) \(id.uuidString)") } deinit { print("\(type(of: self)) \(#function) \(id.uuidString)") } } struct FeatureEView: ViewControllable { var holder: NavStackHolder let coordinator: StateObjectExampleCoordinator // use @ObservedObject instead of @StateObject @ObservedObject var featureEViewModel: FeatureEViewModel init( holder: NavStackHolder = NavStackHolder(), coordinator: StateObjectExampleCoordinator = StateObjectExampleCoordinator(), featureEViewModel: FeatureEViewModel = FeatureEViewModel() ) { self.holder = holder self.coordinator = coordinator self.featureEViewModel = featureEViewModel } var body: some View { VStack { Text("Feature E") Text("count \(featureEViewModel.count)") Button { featureEViewModel.count += 1 } label: { Text("increment") } Spacer() } } func viewWillAppear(_ viewController: UIViewController) { viewController.navigationController?.setNavigationBarHidden(false, animated: true) } func viewDidAppear(_ viewController: UIViewController) { _ = coordinator.present() } }
问题原因及解决方法
核心问题
- 错误的引用类型使用:
StateObjectExampleCoordinator中featureEViewModel被声明为unowned var但却是可选类型,这不符合Swift的引用规则——unowned适用于确定不会为nil的对象,可选类型的弱引用应该使用weak。 - 闭包强引用循环:
enter()方法中设置willPopHandler时,闭包默认强引用self,导致NavigationController持有闭包,闭包持有StateObjectExampleCoordinator,形成循环引用。 - Sheet代理未及时清空:Sheet被dismiss后,代理引用未主动清空,可能导致额外的持有。
修复步骤
1. 修改featureEViewModel的引用类型
将StateObjectExampleCoordinator中的:
unowned var featureEViewModel: FeatureEViewModel?
改为:
weak var featureEViewModel: FeatureEViewModel?
2. 闭包使用弱引用避免循环
在enter()方法中,修改willPopHandler的闭包捕获:
navigationController.willPopHandler = { [weak self] in self?.dismiss(animated: false) }
3. 清空Sheet代理
在presentationControllerDidDismiss方法中添加代理清空逻辑:
extension StateObjectExampleCoordinator: UISheetPresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if let sheetController = presentationController as? UISheetPresentationController { sheetController.delegate = nil } navigationController.popViewController(animated: true) } }
4. 确保FeatureFView中的协调器引用为弱引用
将FeatureFView中的:
let coordinator: StateObjectExampleCoordinator
改为:
weak var coordinator: StateObjectExampleCoordinator?
同时修改初始化方法和使用处的调用逻辑,确保可选值的安全访问。
完成以上修改后,即使设置sheetController.prefersGrabberVisible = true,featureEViewModel也能在ViewController弹出时正常执行deinit方法。
内容的提问来源于stack exchange,提问作者Jason Rich Darmawan
相关产品推荐
相关产品推荐

