1

I need to apply a gradient globally to my status and navigation bars and have it adjust properly to orientation changes. Because I want this to be global, I'm trying to use UIAppearance. Surprisingly, UIAppearance doesn't make this very easy.

It looks great in Portrait, but the gradient is too tall when in Landscape so you can't see the whole thing:

Comparison of portrait and landscape gradient nav bar

Here's my code to this point:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let navigationBarAppearance = UINavigationBar.appearance()
    navigationBarAppearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
    navigationBarAppearance.isTranslucent = false
    navigationBarAppearance.tintColor = UIColor.white

    let status_height = UIApplication.shared.statusBarFrame.size.height
    let gradientLayer = CAGradientLayer(frame: CGRect(x: 0, y: 0, width: 64, height: status_height + 44), colors: [UIColor.init(hex: "005382"), UIColor.init(hex: "00294B")])
    let layerImage = gradientLayer.createGradientImage()
    navigationBarAppearance.barTintColor = UIColor(patternImage: layerImage ?? UIImage())
}

and I'm using this extension:

extension CAGradientLayer {
  convenience init(frame: CGRect, colors: [UIColor]) {
    self.init()
    self.frame = frame
    self.colors = []
    for color in colors {
      self.colors?.append(color.cgColor)
    }
    startPoint = CGPoint(x: 0, y: 0)
    endPoint = CGPoint(x: 0, y: 1)
  }

  func createGradientImage() -> UIImage? {

    var image: UIImage? = nil

    UIGraphicsBeginImageContext(bounds.size)

    if let context = UIGraphicsGetCurrentContext() {
      render(in: context)
      image = UIGraphicsGetImageFromCurrentImageContext()
    }

    UIGraphicsEndImageContext()

    return image
  } 
}

I know I could check the orientation and then change the gradient accordingly but I'd need to do that on every view controller so that would defeat the purpose of using UIAppearance and being able to do it in one place.

Most of the SO threads I've found provide solutions for making the top bar's gradient at the view controller level, but not the global level.

EDIT: Tried answer from @Pan Surakami on my UITabBarController but I still have white navigation bars:

enter image description here

Here's my storybaord setup:

enter image description here

And code:

class MenuTabBarController: UITabBarController {
    var notificationsVM = NotificationsVModel()
    var hasNewAlerts: Bool = false

    override func viewDidLoad() {
      super.viewDidLoad()

      setTabs()

      styleUI()

      notificationsVM.fetchData { (success, newNotifications) in
            if success {
                self.hasNewAlerts = newNotifications.count > 0 ? true : false
                DispatchQueue.main.async {
                    if let tabBarItems = self.tabBar.items {
                        for (_, each) in tabBarItems.enumerated() {
                            if each.tag == 999 { //only update the alerts tabBarItem tag == '999'
                                self.updateAlertBadgeIcon(self.hasNewAlerts, each)
                            }
                        }
                    }
                }
            }
        }
    }

  fileprivate func setTabs() {
    let tab1 = GradientNavigationController(rootViewController: FeedViewController())
    let tab2 = GradientNavigationController(rootViewController: NotificationsTableViewController())
    let tab3 = GradientNavigationController(rootViewController: SearchViewController())
    let tab4 = GradientNavigationController(rootViewController: ComposeDiscussionViewController())
    UITabBarController().setViewControllers([tab1, tab2, tab3, tab4], animated: false)
  }

    func updateAlertBadgeIcon(_ hasAlerts: Bool, _ item: UITabBarItem) {
        if hasAlerts {
            item.image = UIImage(named: "alert-unselected-hasAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
            item.selectedImage = UIImage(named: "alert-selected-hasAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
        } else {
            hasNewAlerts = false
            item.image = UIImage(named: "alert-unselected-noAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
            item.selectedImage = UIImage(named: "alert-selected-noAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
        }
    }

    // UITabBarDelegate
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        if item.tag == 999 { //alerts tabBarItem tag == '999'
            updateAlertBadgeIcon(hasNewAlerts, item)
        }
      if item.tag == 0 { //Feed Item clicked
        if let feedNav = children[0] as? UINavigationController, let feedVC = feedNav.viewControllers[0] as? FeedViewController {
          feedVC.tableView.reloadData()
        }

      }
    }

    func styleUI() {
        UITabBar.appearance().backgroundImage = UIImage.colorForNavBar(color:.lightGrey4)
        UITabBar.appearance().shadowImage = UIImage.colorForNavBar(color:.clear) 
        tabBar.layer.shadowOffset = CGSize.zero
        tabBar.layer.shadowRadius = 2.0
        tabBar.layer.shadowColor = UIColor.black.cgColor
        tabBar.layer.shadowOpacity = 0.30
        UITabBarItem.appearance().setTitleTextAttributes([NSAttributedString.Key.foregroundColor : UIColor.grey2,
                                                          NSAttributedString.Key.font: UIFont(name: "AvenirNext-DemiBold", size: 12) as Any],
                                                         for: .normal)
        UITabBarItem.appearance().setTitleTextAttributes([NSAttributedString.Key.foregroundColor : UIColor.darkSapphire,
                                                          NSAttributedString.Key.font: UIFont(name: "AvenirNext-DemiBold", size: 12) as Any],
                                                         for: .selected)
    }
}
Jim
  • 1,260
  • 15
  • 37
  • You should define one base view controller and then inherit all your view controller from the base view controller. In this way, you can write your orientation related code at one place. – Shubham Jan 29 '19 at 17:37
  • That seems like a good idea. But how would I get each view controller to invoke my code that checks the orientation? I'm thinking I would create a method that would need to be called in `ViewDidLoad` but that would mean having to remember doing that in each controller. – Jim Jan 29 '19 at 18:43
  • @Jim, following on Shubhams' idea, you would override `-viewDidLoad:` in your base view controller and place your orientation check there. – pckill Jan 30 '19 at 09:54
  • Thanks, guys. But my app also has a `UITabBarController` and some `UITableViewController`s. I'd like to be able to have code in one place that will change the status and navigation bars in all of these types of controllers. Is this possible? – Jim Jan 31 '19 at 21:16

1 Answers1

0

One way to implement it is to subclass UINavigationController.

  1. Create a new subclass.
class GradientNavigationController: UINavigationController {}
  1. Override traitCollectionDidChange method.
class GradientNavigationController: UINavigationController {
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        let status_height = UIApplication.shared.statusBarFrame.size.height
        let gradientLayer = CAGradientLayer(frame: CGRect(x: 0, y: 0, width: 64, height: status_height + 44), colors: [[UIColor.init(hex: "005382"), UIColor.init(hex: "00294B")])
        let layerImage = gradientLayer.createGradientImage()
        self.navigationBar.barTintColor = UIColor(patternImage: layerImage ?? UIImage())
    }
}
  1. Use this subclass instead of UINavigationController. Either change custom subclass on storiboards or use it in code.

EDIT: Your UITabBarController is configured by the storyboard. So setTabs() used this way has no sense. It just creates another copy of UITabBarController and then removes it. I showed it just as an example of embedding controllers.

  1. Remove the method setTabs().
  2. Open each storyboard which is linked to your tabs. (I cannot see how they are actually configured they are behind storyboard references.)
  3. Make sure that an initial controller is GradientNavigationController.

enter image description here

Pan Surakami
  • 16
  • 1
  • 4
  • Thanks, @Pan Surakami. I think this is almost the answer I'm looking for. This works with `UINavigationController`, but my app also has a `UITabBarController` and some `UITableViewController`s. I'd like to be able to have code in one place that will change the status and navigation bars in all of these types of controllers. Is there a way to do that? – Jim Jan 31 '19 at 21:15
  • @Jim `UITabBarController` is a kind of container controllers. It has no navigation bars itself. It may embeds `UINavigationController` for each tab per your design. `UITableViewController` has no navigation bar too. It itself can be embedded into 'UINavigationController' or any other controller. So `GradientNavigationController ` is the only place to modify `navigationBar`. Just to make sure to use the subclass in container controllers. – Pan Surakami Feb 01 '19 at 11:30
  • I tried this on one `UITableViewController` which is embedded in a `UINavigationController` on my storyboard. I selected the `UINavigationController` and in the IB editor under Custom Class, `GradientNavigationController` wasn't one of the available selections so I had to type it in. Unfortunately, the navigation bar and status bar are still white (not my gradient). Any idea what could be wrong? – Jim Feb 01 '19 at 15:53
  • I would suggest you set a breakpoint in `traitCollectionDidChange ` to verify that it is called. If it is not, then check again in the storyboard that custom class is set. – Pan Surakami Feb 01 '19 at 17:40
  • That was the first thing I did and it is being called. However, I just figured out why it wasn't working. I tried to subclass `UIViewController` and in `traitCollectionDidChange` I was referencing `navigationController?.navigationBar.barTintColor = UIColor(patternImage: layerImage ?? UIImage())`. When I changed it to subclass `UINavigationController` I didn't change that reference. But when I removed `navigationController.?` so it was just `navigationBar.barTintColor = UIColor(patternImage: layerImage ?? UIImage())` it works. I still have to try your suggestions for TableVC and TabVC... – Jim Feb 01 '19 at 17:51
  • '@Pan Surakami, please see my edit where I've tried your suggestion on my `UITabBarController`. – Jim Feb 02 '19 at 16:22
  • @Jim I have changed my answer. If it is not clear, please show a screen of the initial controller from your referenced storyboards. – Pan Surakami Feb 03 '19 at 11:45
  • Thanks for your help, @Pan Surakami. I got it all working now. – Jim Feb 04 '19 at 03:19