我正在尝试创建一个自定义 UITabBar,但是当我呈现 tabBar 的视图控制器之一时会导致内存泄漏

发布于 2025-01-11 09:23:53 字数 7370 浏览 6 评论 0原文

我正在尝试创建一个自定义 tabBarController,但似乎由于呈现不同的视图控制器而导致内存泄漏。当我在不同选项之间切换时,我可以看到内存使用量攀升。我还检查了视图层次结构,发现有一堆 UITransitionView。我的 CustomTabBar 如下:

import UIKit

struct CustomTabBarViewControllerObject {
    let title: String
    let icon: UIImage
    let viewController: UIViewController
    let index: Int
}

class CustomTabBar: UIView {

    // MARK: - Singleton
    
    /// if you plan on using the CustomTabBar singleton then the following should be set:
    ///  - viewControllerObjects
    ///  - presentingVC
    ///  - selectedIconTintColor
    ///  - unselectedIconTintColor

    static let shared = CustomTabBar(frame: .zero)
    
    //MARK: - Variables
    private var _viewControllerObjects: [CustomTabBarViewControllerObject]?
    public var viewControllerObjects: [CustomTabBarViewControllerObject]? {
        get {
            return self._viewControllerObjects
        } set {
            _viewControllerObjects = newValue
            if frame.width > 0 {
                updateViewControllers()
            }
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if frame.width > 0 {
            updateViewControllers()
        }
    }
    
    private var viewControllerButtons: [UIButton] = []
    
    private var displayTitles = false
    private let iconSize = CGSize(width: 30, height: 30)
    public var selectedIconTintColor: UIColor!
    public var unselectedIconTintColor: UIColor!
    public var presentingVC: UIViewController!
    
    private var selectedIndex = 0
    
    //MARK: - init
    private init(_ presentingVC: UIViewController? = nil, frame: CGRect, displayTitles: Bool = false, selectedIconTintColor: UIColor = .text1, unselectedIconTintColor: UIColor = .text3, bgColor: UIColor = .background1) {
        self.displayTitles = displayTitles
        self.selectedIconTintColor = selectedIconTintColor
        self.unselectedIconTintColor = unselectedIconTintColor
        self.presentingVC = presentingVC
        super.init(frame: frame)
        
        backgroundColor = bgColor
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //MARK: - Methods
    
    private func updateViewControllers() {
        guard let viewControllerObjects = viewControllerObjects else {
            return
        }
        
        // remove all current icons
        for view in subviews {
            view.removeFromSuperview()
        }

        let vcCount = CGFloat(viewControllerObjects.count)
        let numberOfSpaces = vcCount+1
        let interIconSpacing = (frame.width-(vcCount*iconSize.width))/numberOfSpaces
        //let interIconSpacing = (frame.width-(iconSize.width*(CGFloat(vcCount)+1)))/(CGFloat(vcCount)+1)
        
        // app fails if there are too many items in view controller
        if interIconSpacing < 5 {fatalError("Too many elements in viewControllerObjects")}
        
        var lastXPosition: CGFloat = 0
        let yPosition: CGFloat = frame.height/2-iconSize.height/2
        
        var iconWidthSizeAdjustment: CGFloat = -30
        
        // iterate through viewControllerObjects to add them to the view with icon and action
        for vcObject in viewControllerObjects {
            iconWidthSizeAdjustment+=30
            let vcButton = UIButton(frame: CGRect(origin: CGPoint(x: lastXPosition + interIconSpacing + iconWidthSizeAdjustment, y: yPosition), size: iconSize))
            vcButton.setBackgroundImage(vcObject.icon, for: .normal)
            vcButton.layoutIfNeeded()
            vcButton.subviews.first?.contentMode = .scaleAspectFit
            vcButton.addAction(UIAction(title: "", handler: { [unowned self] _ in
                if vcObject.index != selectedIndex {
                    vcObject.viewController.modalPresentationStyle = .fullScreen
                    self.presentingVC.present(vcObject.viewController, animated: false, completion: nil)
                    self.presentingVC = vcObject.viewController
                    self.updateSelected(newSelectedIndex: vcObject.index)
                }
            }), for: .touchUpInside)
            
            if vcObject.index == selectedIndex {
                vcButton.tintColor = selectedIconTintColor
            } else {
                vcButton.tintColor = unselectedIconTintColor
            }
            addSubview(vcButton)
            viewControllerButtons.append(vcButton)
            
            lastXPosition = lastXPosition + interIconSpacing
        }
        roundCorners([.topLeft, .topRight], radius: 15)
        addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)
    }

    func updateSelected(newSelectedIndex: Int) {
        if viewControllerButtons.indices.contains(selectedIndex) && viewControllerButtons.indices.contains(newSelectedIndex) {
            viewControllerButtons[selectedIndex].tintColor = unselectedIconTintColor
            viewControllerButtons[newSelectedIndex].tintColor = selectedIconTintColor
            selectedIndex = newSelectedIndex
        } else {
            fatalError("Index does not exist: \(newSelectedIndex)")
        }
    }
}

这是所有 tabBar 项目继承自的 CustomTabBarViewController 类:

import UIKit

class CustomTabBarViewController: UIViewController {
    
    public let customTabBar = CustomTabBar.shared
    
    public let mainView = UIView(frame: .zero)
    public var tabBarHeight: CGFloat = 60
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpTabBar()
    }
    
    func setUpTabBar() {
        // setting up properties of customTabBar
        customTabBar.selectedIconTintColor = .text1
        customTabBar.unselectedIconTintColor = .text2
        customTabBar.presentingVC = self
        customTabBar.backgroundColor = .background1
        
        // adding viewControllers for tabBar
        customTabBar.viewControllerObjects = [
            CustomTabBarViewControllerObject(title: "Home", icon: Images.home, viewController: UINavigationController(rootViewController: HomeViewController()), index: 0),
            CustomTabBarViewControllerObject(title: "Search", icon: Images.search, viewController: UINavigationController(rootViewController: SearchViewController()), index: 1)
        ]
        
        //adding mainView to view
        view.addSubview(mainView)
        mainView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mainView.topAnchor.constraint(equalTo: view.topAnchor),
            mainView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            mainView.leftAnchor.constraint(equalTo: view.leftAnchor),
            mainView.rightAnchor.constraint(equalTo: view.rightAnchor),
        ])
        
        // add customTabBar to view
        view.addSubview(customTabBar)
        customTabBar = false
        NSLayoutConstraint.activate([
            customTabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            customTabBar.leftAnchor.constraint(equalTo: view.leftAnchor),
            customTabBar.rightAnchor.constraint(equalTo: view.rightAnchor),
            customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight),
        ])
    }
    
}

I am trying to create a custom tabBarController, but it seems like there is a memory leak caused by presenting the different view controllers. I can see the memory usage climb when I toggle between different options. I also checked the view hierarchy and noticed that there were a bunch of UITransitionViews. My CustomTabBar is below:

import UIKit

struct CustomTabBarViewControllerObject {
    let title: String
    let icon: UIImage
    let viewController: UIViewController
    let index: Int
}

class CustomTabBar: UIView {

    // MARK: - Singleton
    
    /// if you plan on using the CustomTabBar singleton then the following should be set:
    ///  - viewControllerObjects
    ///  - presentingVC
    ///  - selectedIconTintColor
    ///  - unselectedIconTintColor

    static let shared = CustomTabBar(frame: .zero)
    
    //MARK: - Variables
    private var _viewControllerObjects: [CustomTabBarViewControllerObject]?
    public var viewControllerObjects: [CustomTabBarViewControllerObject]? {
        get {
            return self._viewControllerObjects
        } set {
            _viewControllerObjects = newValue
            if frame.width > 0 {
                updateViewControllers()
            }
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if frame.width > 0 {
            updateViewControllers()
        }
    }
    
    private var viewControllerButtons: [UIButton] = []
    
    private var displayTitles = false
    private let iconSize = CGSize(width: 30, height: 30)
    public var selectedIconTintColor: UIColor!
    public var unselectedIconTintColor: UIColor!
    public var presentingVC: UIViewController!
    
    private var selectedIndex = 0
    
    //MARK: - init
    private init(_ presentingVC: UIViewController? = nil, frame: CGRect, displayTitles: Bool = false, selectedIconTintColor: UIColor = .text1, unselectedIconTintColor: UIColor = .text3, bgColor: UIColor = .background1) {
        self.displayTitles = displayTitles
        self.selectedIconTintColor = selectedIconTintColor
        self.unselectedIconTintColor = unselectedIconTintColor
        self.presentingVC = presentingVC
        super.init(frame: frame)
        
        backgroundColor = bgColor
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //MARK: - Methods
    
    private func updateViewControllers() {
        guard let viewControllerObjects = viewControllerObjects else {
            return
        }
        
        // remove all current icons
        for view in subviews {
            view.removeFromSuperview()
        }

        let vcCount = CGFloat(viewControllerObjects.count)
        let numberOfSpaces = vcCount+1
        let interIconSpacing = (frame.width-(vcCount*iconSize.width))/numberOfSpaces
        //let interIconSpacing = (frame.width-(iconSize.width*(CGFloat(vcCount)+1)))/(CGFloat(vcCount)+1)
        
        // app fails if there are too many items in view controller
        if interIconSpacing < 5 {fatalError("Too many elements in viewControllerObjects")}
        
        var lastXPosition: CGFloat = 0
        let yPosition: CGFloat = frame.height/2-iconSize.height/2
        
        var iconWidthSizeAdjustment: CGFloat = -30
        
        // iterate through viewControllerObjects to add them to the view with icon and action
        for vcObject in viewControllerObjects {
            iconWidthSizeAdjustment+=30
            let vcButton = UIButton(frame: CGRect(origin: CGPoint(x: lastXPosition + interIconSpacing + iconWidthSizeAdjustment, y: yPosition), size: iconSize))
            vcButton.setBackgroundImage(vcObject.icon, for: .normal)
            vcButton.layoutIfNeeded()
            vcButton.subviews.first?.contentMode = .scaleAspectFit
            vcButton.addAction(UIAction(title: "", handler: { [unowned self] _ in
                if vcObject.index != selectedIndex {
                    vcObject.viewController.modalPresentationStyle = .fullScreen
                    self.presentingVC.present(vcObject.viewController, animated: false, completion: nil)
                    self.presentingVC = vcObject.viewController
                    self.updateSelected(newSelectedIndex: vcObject.index)
                }
            }), for: .touchUpInside)
            
            if vcObject.index == selectedIndex {
                vcButton.tintColor = selectedIconTintColor
            } else {
                vcButton.tintColor = unselectedIconTintColor
            }
            addSubview(vcButton)
            viewControllerButtons.append(vcButton)
            
            lastXPosition = lastXPosition + interIconSpacing
        }
        roundCorners([.topLeft, .topRight], radius: 15)
        addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)
    }

    func updateSelected(newSelectedIndex: Int) {
        if viewControllerButtons.indices.contains(selectedIndex) && viewControllerButtons.indices.contains(newSelectedIndex) {
            viewControllerButtons[selectedIndex].tintColor = unselectedIconTintColor
            viewControllerButtons[newSelectedIndex].tintColor = selectedIconTintColor
            selectedIndex = newSelectedIndex
        } else {
            fatalError("Index does not exist: \(newSelectedIndex)")
        }
    }
}

This is the CustomTabBarViewController class that all tabBar items inherit from:

import UIKit

class CustomTabBarViewController: UIViewController {
    
    public let customTabBar = CustomTabBar.shared
    
    public let mainView = UIView(frame: .zero)
    public var tabBarHeight: CGFloat = 60
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpTabBar()
    }
    
    func setUpTabBar() {
        // setting up properties of customTabBar
        customTabBar.selectedIconTintColor = .text1
        customTabBar.unselectedIconTintColor = .text2
        customTabBar.presentingVC = self
        customTabBar.backgroundColor = .background1
        
        // adding viewControllers for tabBar
        customTabBar.viewControllerObjects = [
            CustomTabBarViewControllerObject(title: "Home", icon: Images.home, viewController: UINavigationController(rootViewController: HomeViewController()), index: 0),
            CustomTabBarViewControllerObject(title: "Search", icon: Images.search, viewController: UINavigationController(rootViewController: SearchViewController()), index: 1)
        ]
        
        //adding mainView to view
        view.addSubview(mainView)
        mainView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mainView.topAnchor.constraint(equalTo: view.topAnchor),
            mainView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            mainView.leftAnchor.constraint(equalTo: view.leftAnchor),
            mainView.rightAnchor.constraint(equalTo: view.rightAnchor),
        ])
        
        // add customTabBar to view
        view.addSubview(customTabBar)
        customTabBar = false
        NSLayoutConstraint.activate([
            customTabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            customTabBar.leftAnchor.constraint(equalTo: view.leftAnchor),
            customTabBar.rightAnchor.constraint(equalTo: view.rightAnchor),
            customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight),
        ])
    }
    
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

魄砕の薆 2025-01-18 09:23:53

所以我在这里冒险,但

override func layoutSubviews() {
    super.layoutSubviews()
    if frame.width > 0 {
        updateViewControllers()
    }
}

我认为这就是问题所在。这个函数被这个系统相当快地调用,

 private func updateViewControllers() { ...

将其与这一行结合起来

addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)

,基本上你就有了一个灾难的秘诀。这只会不断添加阴影。可能还有其他对象被重新添加,但这是我在没有完整测试和调试的情况下注意到的。基本上阴影相当昂贵。他们使用大量的计算和内存。这基本上每次都会重新添加阴影。我首先评论添加阴影,看看这是否会减少资源使用。如果没有,则注释掉layoutSubviews()中的updateViewController()。

So I am going out on a limb here but in

override func layoutSubviews() {
    super.layoutSubviews()
    if frame.width > 0 {
        updateViewControllers()
    }
}

I think this is where the problem lies. This function gets called by this system fairly rapidly

 private func updateViewControllers() { ...

Combine that with this line

addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)

And basically you have a recipe for disaster. This just keeps adding shadows. There may be other objects getting re added as well but this is what I noticed without full testing and debug. Basically shadows are fairly expensive. They use both a decent amount of computing as well as ram. This will basically re add the shadow every time. I would start by commenting the add shadow and seeing if that reduces resource usage. If it doesn't then comment out the updateViewController() in layoutSubviews().

家住魔仙堡 2025-01-18 09:23:53

您可以使用仪器进行调试,以查看导致峰值的分配情况。
https://www.raywenderlich.com/16126261-instruments-快速入门教程

You can debug using instruments to see what is being allocated causing the spike.
https://www.raywenderlich.com/16126261-instruments-tutorial-with-swift-getting-started

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文