skip to Main Content

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

2

Answers


  1. 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

    Login or Signup to reply.
  2. 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().

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search