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
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
So I am going out on a limb here but in
I think this is where the problem lies. This function gets called by this system fairly rapidly
Combine that with this line
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().