37

I am in process of adding large title in navigation bar in one of the application. The issue is title is little long so I will require to add two lines in large title. How can I add large title with two lines in navigation bar?

This is not about default navigation bar title! This is about large title which is introduced in iOS 11. So make sure you add suggestions by considering large title. Thanks

Title text truncated with 3 dots in the navigation bar

aheze
  • 24,434
  • 8
  • 68
  • 125
Jigar Thakkar
  • 6,834
  • 2
  • 17
  • 15

10 Answers10

15

Based in @krunal answer, this is working for me:

extension UIViewController {

func setupNavigationMultilineTitle() {
    guard let navigationBar = self.navigationController?.navigationBar else { return }
    for sview in navigationBar.subviews {
        for ssview in sview.subviews {
            guard let label = ssview as? UILabel else { break }
            if label.text == self.title {
                label.numberOfLines = 0
                label.lineBreakMode = .byWordWrapping
                label.sizeToFit()
                UIView.animate(withDuration: 0.3, animations: {
                    navigationBar.frame.size.height = 57 + label.frame.height
                })
            }
        }
    }
}

In the UIViewController:

override func viewDidLoad() {
    super.viewDidLoad()
    self.title = "This is a multiline title"
    setupNavigationMultilineTitle()
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    setupNavigationMultilineTitle()
}

And for setting font and color on the large title:

navigation.navigationBar.largeTitleTextAttributes = [NSAttributedStringKey.foregroundColor: .red, NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 30)]
Nick Weaver
  • 47,228
  • 12
  • 98
  • 108
Patricio Bravo
  • 406
  • 6
  • 8
  • 1
    This works really well. I do notice a slight gap in the animation when I try this code. It may be that the constant `57` is not enough here. – Ilias Karim Jul 18 '18 at 15:43
  • 1
    Yes, i fix it changing the value depending on the device: let offset: CGFloat = UIDevice.current.iPhone5 ? 53 : 57 – Patricio Bravo Jul 24 '18 at 18:43
  • 5
    This solution does not seems to work in iOS 13 with Xcode 11. – Rodrigo Guimarães Oct 21 '19 at 18:48
  • 1
    Seems like in iOS 13 the navigationBar subview hierarchy is more complex so I'm using a recursive function to change all UILabels. This does work for me with the standard size title, but not the large title, though... still debugging this. – Sherwin Zadeh Jan 16 '20 at 06:53
  • 1
    Not working on iOS 16 – Jalil Aug 18 '22 at 23:14
14

Get a navigation item subviews and locate UILabel from it.

Try this and see:

self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationController?.navigationItem.largeTitleDisplayMode = .automatic

self.title = "This is multiline title for navigation bar"
self.navigationController?.navigationBar.largeTitleTextAttributes = [                     
                                NSAttributedStringKey.foregroundColor: UIColor.black,
                                NSAttributedStringKey.font : UIFont.preferredFont(forTextStyle: .largeTitle)
                                ]

for navItem in(self.navigationController?.navigationBar.subviews)! {
     for itemSubView in navItem.subviews { 
         if let largeLabel = itemSubView as? UILabel {
             largeLabel.text = self.title
             largeLabel.numberOfLines = 0
             largeLabel.lineBreakMode = .byWordWrapping
         }
     }
}

Here is result:

enter image description here

Krunal
  • 77,632
  • 48
  • 245
  • 261
  • @Krunal I've used the for loop that you've provided to find the large title label subview, but when I try customizing the attributedTitle of that label nothing shows up – Neel Sarwal Feb 27 '18 at 18:15
  • @Krunal Yup I get the `UILabel` and settings text and other properties work, but not `attributedTitle`, using iPhone 8 Plus iOS 11.2 – Neel Sarwal Feb 27 '18 at 18:25
  • 2
    I agree with Jigar. I have a back button and this isn't working. It works when I swipe the page from the left side a bit (as if to go back) but then let it bounce back into place. – Coltuxumab Apr 03 '18 at 22:07
  • Adding this lines improves behavior on the larges titles when you have backbutton: `largeLabel.sizeToFit() navigationBar.frame.size.height = 50 + largeLabel.frame.height` – Patricio Bravo Jul 11 '18 at 21:34
  • 1
    does anybody faced the issue when you navigate to a next page where the title is small and bar collapse and when you come back your title switched back to one line followed by ..... – tryKuldeepTanwar Jul 30 '18 at 06:23
  • i tried the same in Xcode12.5. It doesnt work – Aftab Ahmed Jul 08 '21 at 12:22
5

You could try:

  1. Create a custom UINavigationController
  2. Add the protocol UINavigationBarDelegate to the class definition
  3. Override the function navigationBar(_:shouldPush:)
  4. Activate two lines mode using hidden variable item.setValue(true, forKey: "__largeTitleTwoLineMode")
  5. Make navigationController.navigationBar.prefersLargeTitles = true
ion
  • 819
  • 10
  • 8
  • 2
    God bless you mate, you saved my life. Works like a charm!!! :D But I used `navigationItem.setValue(1, forKey: "__largeTitleTwoLineMode")` inside ViewController, so u don't need use `UINavigationBarDelegate`. – Oleksandr Vaker Feb 02 '22 at 19:58
  • As an update, this key does work but for some reason when the new view is pushed the large title is collapsed by default. Scrolling does reveal the full title but it is quite strange. I tried everything, sizetofit(), async, preferLargeTitle etc. This only happens on title that spans multiple lines. @OleksandrVaker did you guys encounter this problem? – yaozhang Oct 06 '22 at 02:17
  • @yaozhang actually, this stuff won't work in release. Cause this is a private API Keys, and apple will discard your apply to release. I did this stuff in other way by adding a label, which will swiping by scrolling. – Oleksandr Vaker Oct 06 '22 at 08:30
4

The linebreak solution seems to be problematic when there's a back button. So instead of breaking lines, I made the label auto adjust font.

func setupLargeTitleAutoAdjustFont() {
    guard let navigationBar = navigationController?.navigationBar else {
        return
    }
    // recursively find the label
    func findLabel(in view: UIView) -> UILabel? {
        if view.subviews.count > 0 {
            for subview in view.subviews {
                if let label = findLabel(in: subview) {
                    return label
                }
            }
        }
        return view as? UILabel
    }

    if let label = findLabel(in: navigationBar) {
        if label.text == self.title {
            label.adjustsFontSizeToFitWidth = true
            label.minimumScaleFactor = 0.7
        }
    }
}

Then it needs to be called in viewDidLayoutSubviews() to make sure the label can be found, and we only need to call it once:

private lazy var setupLargeTitleLabelOnce: Void = {[unowned self] in
    if #available(iOS 11.0, *) {
        self.setupLargeTitleAutoAdjustFont()
    }
}()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    let _ = setupLargeTitleLabelOnce
}

If there's any navigationController pop event back to this controller, we need to call it again in viewDidAppear(). I haven't found a better solution for this - there's a small glitch of label font changing when coming back from a pop event:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    if #available(iOS 11.0, *) {
        setupLargeTitleAutoAdjustFont()
    }
}
Bonan
  • 709
  • 4
  • 18
1

(Edit 7/13: I notice that this solution is not support scrollView, so now I'm in research)

I found a perfect solution on Swift5

but sorry for my poor English because I'm JapaneseStudent.

In case of 2 lines In case of 3 lines

At first, set navigation settings for largeTitle normally in viewDidLoad

//Set largeTitle
navigationItem.largeTitleDisplayMode = .automatic
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.navigationBar.largeTitleTextAttributes = [.font: UIFont.systemFont(ofSize: (fontSize + margin) * numberOfLines)]//ex) fontSize=26, margin=5, numberOfLines=2
        
//Set title
title = "multiple large\ntitle is working!"

It is most important point of this solution that font-size at largeTitleTextAttributes equals actual font-size(+margin) multiplied by number of lines.

Description image

Because, default specification of navigationBar attributes may be able to display only 1 line largeTitle.

Although, somehow, I did notice that in case of label-settings(the label which subview of subview of navigationBar) on direct, it can display any number of lines in 1 line of in case of navigationBar attributes.

So, we should do set big font in navigationbar attributes, and set small font in the label(subview of subview of navigationBar), and take into consideration the margins.

Do label settings direct in viewDidAppear like this:

//Find label
navigationController?.navigationBar.subviews.forEach({ subview in
        subview.subviews.forEach { subsubview in
        guard let label: UILabel = subsubview as? UILabel else { return }
        //Label settings on direct.
        label.text = title
        label.font = UIFont.systemFont(ofSize: fontSize)
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        label.sizeToFit()
    }
})

Therefore, in short, the solution at minimum code is given like this:

import UIKit

class ViewController: UIViewController {
    
    private let fontSize: CGFloat = 26, margin: CGFloat = 5
    private let numberOfLines: CGFloat = 2

    override func viewDidLoad() {
        super.viewDidLoad()

        setUpNavigation()
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        setMultipleLargeTitle()
    }
    private func setUpNavigation() {
        //Set largeTitle
        navigationItem.largeTitleDisplayMode = .automatic
        navigationController?.navigationBar.prefersLargeTitles = true
        navigationController?.navigationBar.largeTitleTextAttributes = [.font: UIFont.systemFont(ofSize: (fontSize + margin) * numberOfLines)]
        
        //Set title
        title = "multiple large\ntitle is working!"
    }
    private func setMultipleLargeTitle() {
        //Find label
        navigationController?.navigationBar.subviews.forEach({ subview in
            subview.subviews.forEach { subsubview in
                guard let label: UILabel = subsubview as? UILabel else { return }
                //Label settings on direct.
                label.text = title
                label.font = UIFont.systemFont(ofSize: fontSize)
                label.numberOfLines = 0
                label.lineBreakMode = .byWordWrapping
                label.sizeToFit()
            }
        })
    }
}

thank you for reading :)

Kaiv Mata
  • 11
  • 2
  • I couldnt make it work, my Title just got Bigger (because of the font size) instead of separating into multiple lines, are you missing some implementation, or any news on your investigation on scrollview?? – MXNMike Nov 16 '21 at 19:56
0

If anyone looking for Title Lable Not Large Title, then below code is working.

Swift 5.X

func setMultilineNavigationBar(topText:  String, bottomText : String) {
     let topTxt = NSLocalizedString(topText, comment: "")
     let bottomTxt = NSLocalizedString(bottomText, comment: "")
        
     let titleParameters = [NSAttributedString.Key.foregroundColor : UIColor.white,
                               NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .semibold)]
     let subtitleParameters = [NSAttributedString.Key.foregroundColor : UIColor.white,
                                  NSAttributedString.Key.font : UIFont.systemFont(ofSize: 13, weight: .regular)]
        
     let title:NSMutableAttributedString = NSMutableAttributedString(string: topTxt, attributes: titleParameters)
     let subtitle:NSAttributedString = NSAttributedString(string: bottomTxt, attributes: subtitleParameters)
        
     title.append(NSAttributedString(string: "\n"))
     title.append(subtitle)
        
     let size = title.size()
        
     let width = size.width
     guard let height = navigationController?.navigationBar.frame.size.height else {return}
        
      let titleLabel = UILabel(frame: CGRect.init(x: 0, y: 0, width: width, height: height))
      titleLabel.attributedText = title
      titleLabel.numberOfLines = 0
      titleLabel.textAlignment = .center
      self.navigationItem.titleView = titleLabel 
    }
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Ashu
  • 3,373
  • 38
  • 34
  • 1
    This just puts a multi-line label in the default title view. The question is how to have a multi-line *large* title view. – Sherwin Zadeh Jan 16 '20 at 05:22
0

Swift 4 : Multi line even though the sentence is only short

title = "You're \nWelcome"

for navItem in(self.navigationController?.navigationBar.subviews)! {
     for itemSubView in navItem.subviews { 
         if let largeLabel = itemSubView as? UILabel {
             largeLabel.text = self.title
             largeLabel.numberOfLines = 0
             largeLabel.lineBreakMode = .byWordWrapping
         }
     }
}

Proof

Pengguna
  • 4,636
  • 1
  • 27
  • 32
0

SWIFT 5 This UIViewController extension helped me. Scenario that I have is mixed with enabling and disabling large titles so FIRST ENABLE large title and then call this method. Call it in viewDidLoad, I have found bug with peeking back with swipe and then releasing touch, for some reason current navigation title become previous navigation title

    extension UIViewController {

/// Sets two lines for navigation title if needed
/// - Parameter animated: used for changing titles on one controller,in that case animation is off
func multilineNavTitle(_ animated:Bool = true) {
    
    if animated {
        // setting initial state for animation of title to look more native
        self.navigationController?.navigationBar.transform = CGAffineTransform.init(translationX: .screenWidth/2, y: 0)
        self.navigationController?.navigationBar.alpha = 0
    }
    
    //Checks if two lines is needed
    if self.navigationItem.title?.forTwoLines() ?? false {
        
        // enabling multiline
        navigationItem.setValue(true,
                                forKey: "__largeTitleTwoLineMode")
    } else {
        
        // disabling multiline
        navigationItem.setValue(false,
                                forKey: "__largeTitleTwoLineMode")
    }
    
    // laying out title without animation
    UIView.performWithoutAnimation {
        self.navigationController?.navigationBar.layoutSubviews()
        self.navigationController?.view.setNeedsLayout()
        self.navigationController?.view.layoutIfNeeded()
    }
    
    if animated {
        //animating title
        UIView.animate(withDuration: 0.3) {
            self.navigationController?.navigationBar.transform = CGAffineTransform.identity
            self.navigationController?.navigationBar.alpha = 1
        }
    }

}


}


fileprivate extension String {

/// Checks if navigation title is wider than label frame
/// - Returns: `TRUE` if title cannot fit in one line of navigation title label
func forTwoLines() -> Bool {
    
    let fontAttributes = [NSAttributedString.Key.font: SomeFont]
    let size = self.size(withAttributes: fontAttributes)
    return size.width > CGFloat.screenWidth - 40 //in my case
    
}


}
Sakule
  • 61
  • 1
  • 5
0

Just create a custom navigation controller. Rest will be handled by the OS itself

class MyNavigationViewController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationBar.delegate = self 
    }

}

extension MyNavigationViewController: UINavigationBarDelegate {
    func navigationBar(_ navigationBar: UINavigationBar, shouldPush item: UINavigationItem) -> Bool {
        item.setValuesForKeys([
            "__largeTitleTwoLineMode": true
        ])
        return true
    }
}

Ankur Lahiry
  • 2,253
  • 1
  • 15
  • 25
0
viewController.navigationItem
    .setValuesForKeys(["__largeTitleTwoLineMode": true])

WARNING: This method does not work on older OS versions

errorka
  • 29
  • 9