Home > Net >  I am trying to create a custom UITabBar, but there is a memory leak caused when I present one of the
I am trying to create a custom UITabBar, but there is a memory leak caused when I present one of the

Time:03-10

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),
        ])
    }
    
}

CodePudding user response:

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().

CodePudding user response:

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

  • Related