You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何设置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()
    }
}

问题原因及解决方法

核心问题

  1. 错误的引用类型使用:StateObjectExampleCoordinator中featureEViewModel被声明为unowned var但却是可选类型,这不符合Swift的引用规则——unowned适用于确定不会为nil的对象,可选类型的弱引用应该使用weak。
  2. 闭包强引用循环:enter()方法中设置willPopHandler时,闭包默认强引用self,导致NavigationController持有闭包,闭包持有StateObjectExampleCoordinator,形成循环引用。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 04:12:33