How to convert Navbar Large title to Multi-line, centre aligned - ios

I'm trying to design view controller with Multi-lined centred Large title text exactly like Ask Siri by apple (Settings->General->Keyboards->About Ask Siri, Dictation and Privacy...).
I can able to achieve centred text using:
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
navigationController?.navigationBar.largeTitleTextAttributes = [.paragraphStyle: paragraph]
I did set Navigation title from Storyboard and tried these to achieve multi-lined large title:
https://stackoverflow.com/a/51295457/4061501
https://stackoverflow.com/a/48388588/4061501
But none of them are worked on iOS 13.

You can achieve this by adding a multiline label to your scrollView and then show/hide your navigation item title in the scrollViewDidScroll delegate method of the scrollView depending on the vertical scrollView offset.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > myLabelHeight && navigationItem.title == "" {
setTitle(hidden: false)
} else if scrollView.contentOffset.y <= myLabelHeight && navigationItem.title == "MyTitleString" {
setTitle(hidden: true)
}
}
I've added a layer transition to achieve the fade effect.
func setTitle(hidden: Bool) {
let animation = CATransition()
animation.duration = 0.25
animation.type = .fade
navigationController?.navigationBar.layer.add(animation, forKey: "fadeText")
if hidden {
navigationItem.title = ""
} else {
navigationItem.title = "MyTitleString"
}
}
Don't forget to set the navigation item title to an empty string in viewDidLoad.

There is not any such kind of property that you can set and title became multiline. You need to manipulate it.
This is a code example of how you can create a multiline navigationBar title:
label.backgroundColor = .clear
label.numberOfLines = 2
label.font = UIFont.boldSystemFont(ofSize: 16.0)
label.textAlignment = .center
label.textColor = .white
label.text = "This is a\nmultiline string for the navBar"
self.navigationItem.titleView = label```

Related

Can't change UINavigationBar prompt color and font

I can change title appearance and its work very well but I can't change prompt font and color
Try this code in viewWillAppear(_ animated: Bool):
for view in self.navigationController?.navigationBar.subviews ?? [] {
let subviews = view.subviews
if subviews.count > 0, let label = subviews[0] as? UILabel {
label.textColor = UIColor.white
label.font = UIFont.systemFont(ofSize: 30)
}
}

Custom navigation bar title gets clipped after user leaves view controller

I need to extend my navigation bar height but since Apple made it very hard to change the navigation bar height in iOS 11 I decided I needed to use a custom view which extended the navigation bar without the user noticing.
I've created a custom view to add to the bottom of the navigation bar. I made it red just for the sake of making this question more clear. When the user leaves the view controller and then comes back, the title view custom view is "clipped" by the red view. Why?
I've tried to set clipsToBounds false on the custom title view, but that didn't help. How can I make sure the custom title view always stays on top of everything? Why is it being clipped and overlapped by the little red view (whose main purpose is to "extend" the navigation bar)?
Note: "Monthly Spending" label is part of the title view being clipped.
class ViewController: UIViewController {
let customTitleView = CustomTitleView()
let navigationBarExtensionView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
setupAdditionalGradientView()
navigationItem.titleView = customTitleView
}
internal func setupAdditionalGradientView() {
view.addSubview(navigationBarExtensionView)
navigationBarExtensionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
navigationBarExtensionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
navigationBarExtensionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
navigationBarExtensionView.heightAnchor.constraint(equalToConstant: 18).isActive = true
// Hide pixel shadow between nav bar and red bar
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.navigationBar.layer.shadowRadius = 0
navigationController?.navigationBar.layer.shadowOffset = CGSize(width: 0, height: 0)
}
}
Custom title view:
import UIKit
class CustomTitleView: UIView {
let primaryLabel: UILabel = {
let label = UILabel()
label.text = "$10,675.00"
label.font = UIFont.systemFont(ofSize: 27.99, weight: .medium)
label.textColor = .white
label.textAlignment = .center
return label
}()
let secondaryLabel: UILabel = {
let label = UILabel()
label.text = "Monthly Spending"
label.textColor = .white
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 10, weight: .medium)
return label
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupStackView()
}
internal func setupStackView() {
addSubview(stackView)
stackView.addArrangedSubview(primaryLabel)
stackView.addArrangedSubview(secondaryLabel)
stackView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 10).isActive = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
In iOS 11, a custom bar button item view such as your titleView is sized from the inside out using constraints. Thus, you need constraints to size the view correctly. You are not providing any constraints, so the runtime doesn't know how to size the title view.
However, I would suggest that you just give up on the dubious idea of extending your UINavigationItem's custom view downward below the outside of the navigation bar, and instead, just show the words Monthly Spending in your view controller's view.

How to set multi line Large title in navigation bar? ( New feature of iOS 11)

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
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)]
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:
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()
}
}
You could try:
Create a custom UINavigationController
Add the protocol UINavigationBarDelegate to the class definition
Override the function navigationBar(_:shouldPush:)
Activate two lines mode using hidden variable item.setValue(true, forKey: "__largeTitleTwoLineMode")
Make navigationController.navigationBar.prefersLargeTitles = true
(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 Japanese🇯🇵Student.
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 :)
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
}
}
}
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
}
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
}
}
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
}
}
viewController.navigationItem
.setValuesForKeys(["__largeTitleTwoLineMode": true])
WARNING: This method does not work on older OS versions

Two Line Prompt - Swift

Is there a way to make a two line prompt for a swift navigation bar? I currently cannot find a property to modify. The text I am currently displaying in the prompt comes from an external data model, so sometimes there is more text than fits on the screen. Thanks.
Image Showing Text Outside of Frame
You can try like this:
Swift 3.0
let label = UILabel(frame: CGRect(x:0, y:0, width:350, height:50)) //width is subject to change, Defined as per your screen
label.backgroundColor =.clear
label.numberOfLines = 2
label.font = UIFont.boldSystemFont(ofSize: 16.0)
label.textAlignment = .center
label.textColor = UIColor.white
label.text = "Your Text here"
self.navigationItem.titleView = label
Navigation bar has a title and a prompt
navigationItem.title = "Title, large"
navigationItem.prompt = "One line prompt, small text, auto-shrink"
Having a prompt could be better that having a custom title view, because it gives you more height and it works great with searchbars. But make sure this is really what you want, since the code bellow is not tested on all devices iOS versions. This will just give you an idea how you can control almost anything regarding layout in the navigation bar
class MyNavigationBar: UINavigationBar {
func allSubViews(views: [UIView]) {
for view in views {
if let label = view as? UILabel, label.adjustsFontSizeToFitWidth {
if label.numberOfLines != 2 { //this is the promp label
label.numberOfLines = 2
let parent = label.superview
parent?.frame = CGRect(x: 0, y: 0, width: parent!.bounds.width, height: 44)
parent!.removeConstraints(parent!.constraints)
label.removeConstraints(label.constraints)
label.leadingAnchor.constraint(equalTo: parent!.leadingAnchor, constant: 20).isActive = true
label.trailingAnchor.constraint(equalTo: parent!.trailingAnchor, constant: -20).isActive = true
label.topAnchor.constraint(equalTo: parent!.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: parent!.bottomAnchor).isActive = true
}
return
}
self.allSubViews(views: view.subviews)
}
}
override func layoutSubviews() {
super.layoutSubviews()
allSubViews(views: self.subviews)
}
}
To use your navigation bar use:
let navVc = UINavigationController(navigationBarClass: MyNavigationBar.self, toolbarClass: nil)

Swift 3 UILabel, how to word wrap after changing text

I have a UILabel which has been created programmatically and want to set the text property dynamically after its creation. I can word wrap it during init but not subsequently. After the title label text is changed it keeps one line, regardless of whether I set the number of lines to 2 or 0 and word wrap or not. After I change the title I want it to occupy 2 lines and the text is easily long enough for this to happen but it does not.
let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.textColor = .white
return label
}()
var titleLabelText: String? {
didSet {
titleLabel.text = titleLabelText
titleLabel.layoutIfNeeded()
}
}
then later...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
titleLabelText = "some string which needs word wrapping"
}
titleLabel.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
titleLabel.widthAnchor.constraint(equalToConstant: screenWidth - 90).isActive = true
Font size is UIFont(name: "Muli", size: 12)
Here is how I load my label in viewDidLoad() but the label.text is applied in viewDidAppear()
let headerStackView: UIStackView = {
let headerArray = [welcomeLabel]
let stackView = UIStackView(arrangedSubviews: headerArray)
When you set your label to display multiple line..Remember your label must have enough height that can hold your text to be display multiple line.
Try to increase height of your label, Check width is proper and then check with your code.
If your text gets increases decrease the font size, it will automatically show complete text in your label.
and set the label line space to 0 and allow word wrap property true
select your label and try this one
AFAIK You need to add it as attributedText, then create an NSAttributedString, with your string and attributes.
let paragraphStyleWithWordWrapping = NSMutableParagraphStyle()
paragraphStyleWithWordWrapping.lineBreakMode = .byWordWrapping
let attributedString = NSAttributedString(myString, [NSParagraphStyleAttributeName: paragraphStyleWithWordWrapping])
label.attributedText = attributedString

Resources