Handling navigation bar background gradient in iPhone X

Viewed 2121

By far I handled gradient in navigation bar in the following manner_

let gradient = CAGradientLayer()
    let sizeLength = UIScreen.main.bounds.size.height * 2
    let defaultNavigationBarFrame = CGRect(x: 0, y: 0, width: sizeLength, height: 64)
    gradient.frame = defaultNavigationBarFrame
    gradient.colors = [UIColor(hex:"92CF1F").cgColor, UIColor(hex:"79AB1B").cgColor]

    UINavigationBar.appearance().setBackgroundImage(self.image(fromLayer: gradient), for: .default)
    UINavigationBar.appearance().tintColor = UIColor.white
    UINavigationBar.appearance().isTranslucent = false
    UINavigationBar.appearance().clipsToBounds = false

    if DeviceType.IS_IPAD{
        UINavigationBar.appearance().titleTextAttributes = [NSFontAttributeName : UIFont .systemFont(ofSize: 24, weight: UIFontWeightLight), NSForegroundColorAttributeName: UIColor.white]
    }
    else
    {
        UINavigationBar.appearance().titleTextAttributes = [NSFontAttributeName : UIFont .systemFont(ofSize: 20, weight: UIFontWeightLight), NSForegroundColorAttributeName: UIColor.white]
    }

    UISearchBar.appearance().backgroundColor = UIColor.clear

But Now in iPhone X I have issue due to "64" as navigation bar height for gradient as below_

enter image description here

Please suggest a fix for this that can be dynamically used in each case.

4 Answers

Using Alisson Barauna's answer to your question, I managed to fix this issue by just updating my UINavigationBar extension to look like this:

extension UINavigationBar {

@objc func setGradientBackground(colors: [UIColor]) {

    var updatedFrame = bounds

    if UIDevice().userInterfaceIdiom == .phone {
        if UIScreen.main.nativeBounds.height == 2436{
            updatedFrame.size.height += 44
        } else {
            updatedFrame.size.height += 20
        }
    }

    let gradientLayer = CAGradientLayer(frame: updatedFrame, colors: colors)
    setBackgroundImage(gradientLayer.createGradientImage(), for: UIBarMetrics.default)
}

By doing this the height will just be set to the larger iPhone X size (44), if the program detects that an iPhone X is being used (as the screen height is 2436).

I have checked for iPhone X mode, by the height of the screen, and then I change the gradient image generated to fill the navigation bar. I think it's not the best workaround, but it works fine for now.

class func checkiPhoneModel() -> String {
    if UIDevice().userInterfaceIdiom == .phone {
        switch UIScreen.main.nativeBounds.height {
        case 1136:
            return "5, 5s, 5c, se"
        case 1334:
            return "6, 6s, 7"
        case 2208:
            return "6+, 6S+, 7+"
        case 2436:
            return "X"
        default:
            return "unknown"
        }
    }
    return ""
}

Then you can create an extension for UINavigationBar, adding a method to change the color for a gradient image:

extension UINavigationBar {

    func setGradientBackground(colors: [UIColor]) {

        var size:CGFloat

        if Reachability.checkiPhoneModel() == "X" {
          size = 44
        }
        else {
            size = 20
        }

        var updatedFrame = bounds
        updatedFrame.size.height += size
        let gradientLayer = CAGradientLayer(frame: updatedFrame, colors: colors)

        setBackgroundImage(gradientLayer.creatGradientImage(), for: UIBarMetrics.default)
    }
}

The size var is set to 44 when is iPhone X, and other models it receives 20.

Then on your viewDidLoad, just call the method recently created:

self.navigationController?.navigationBar.setGradientBackground(colors: [.red, .yellow])

The result is something like this. In my case, the gradient uses alpha zero at the bottom of the navigationBar

How about stretching the image with UIImage.resizableImage() like this: (This is nearly perfect solution)

open class NavigationController: UINavigationController {

    var gradientColors = [UIColor]() {
        didSet {
            updateGradient()
        }
    }

    open func updateGradient() {
        guard gradientColors.count > 1 else {
            // There has to be at least two colors to set gradient..
            return
        }
        let gradient = CAGradientLayer()
        let defaultNavigationBarFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 64)

        gradient.frame = defaultNavigationBarFrame
        gradient.colors = gradientColors.map({ $0.cgColor })

        if let gradientImage = gradient.snapshotImage() {
            let image =  gradientImage.resizableImage(withCapInsets: UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1), resizingMode: UIImage.ResizingMode.stretch)
            UINavigationBar.appearance().setBackgroundImage(image, for: .default)
        }
    }
}

The CALayer.snapshotImage() implementation:

public extension CALayer {
    public func snapshotImage() -> UIImage? {
        return UIGraphicsImageRenderer(bounds: frame).image { (imageContext) in
            render(in: imageContext.cgContext)
        }
    }
}

Not only can you no longer assume a navigation bar height of 64, you can no longer assume a static height.

I think the simplest way of achieving what you're trying to do is to use UINavigationController's init(navigationBarClass:toolbarClass:) method with a custom UINavigationBar subclass that overrides layerClass to specify your CAGradientLayer directly.

Unfortunately I don't think there's a way to "fire and forget" this with UIAppearance.

Related