How do I dynamically adjust the height of a UIView when a member stack view is no longer shown? - swift

I am super new iOS development and StackViews in general and need help calculating dynamic height in instances where a stack view will not be shown. There are cases where certain elements will not be shown depending on what I get back from the server.
However, when I call removeArrangedSubview the element is removed but the height isn't adjusted dynamically. How can I fix this?
I would like to avoid the Interface Builder all together and just do this programmatically. I have been using layout anchors for constraints.
Here's my code. You can put in a playground to see it.
//: Playground - noun: a place where people can play
import UIKit
import Foundation
let view = UIView(frame: CGRect(x: 0, y: 0, width: 800, height: 140))
let firstStackView = UIStackView(frame: CGRectZero)
//firstStackView.heightAnchor.constraintGreaterThanOrEqualToConstant(40).active = true
firstStackView.axis = .Vertical
firstStackView.alignment = .Fill
firstStackView.distribution = .EqualSpacing
let titleStackView = UIStackView(frame: CGRectZero)
titleStackView.axis = .Horizontal
titleStackView.alignment = .Fill
titleStackView.distribution = .Fill
titleStackView.spacing = 3
firstStackView.addArrangedSubview(titleStackView)
let productStackView = UIStackView(frame: .zero)
productStackView.axis = .Horizontal
productStackView.alignment = .Leading
productStackView.distribution = .Fill
productStackView.spacing = 3
firstStackView.addArrangedSubview(productStackView)
//firstStackView.removeArrangedSubview(productStackView)
let secondStackView = UIStackView(frame: CGRectZero)
//secondStackView.heightAnchor.constraintEqualToConstant(30).active = true
secondStackView.axis = .Horizontal
secondStackView.distribution = .EqualSpacing
let title = UILabel(frame: CGRectZero)
title.text = "test1"
title.textColor = .blackColor()
//labelOne.backgroundColor = .blueColor()
let size = title.sizeThatFits(CGSizeZero)
print("\(size)")
title.widthAnchor.constraintEqualToConstant(size.width).active = true
//labelOne.heightAnchor.constraintEqualToConstant(30).active = true
titleStackView.addArrangedSubview(title)
let assigneeLabel = UILabel(frame: CGRectZero)
assigneeLabel.text = "test2"
assigneeLabel.textColor = .blackColor()
//labelTest.backgroundColor = .redColor()
assigneeLabel.textAlignment = .Left
//labelTest.heightAnchor.constraintEqualToConstant(30).active = true
titleStackView.addArrangedSubview(assigneeLabel)
let actions = UIButton(type: .Custom)
//buttonOne.backgroundColor = .redColor()
actions.setTitle("some button", forState: .Normal)
actions.setTitleColor(.blackColor(), forState: .Normal)
titleStackView.addArrangedSubview(actions)
let productOne = UILabel(frame: CGRectZero)
productOne.text = "something1"
productOne.numberOfLines = 0
let productLabelSize = productOne.sizeThatFits(CGSizeZero)
productOne.widthAnchor.constraintEqualToConstant(productLabelSize.width).active = true
productOne.textColor = .blackColor()
//labelTwo.backgroundColor = .blueColor()
productStackView.removeArrangedSubview(productOne)
//productStackView.addArrangedSubview(productOne)
let productTwo = UILabel(frame: CGRectZero)
productTwo.text = "something2"
productTwo.numberOfLines = 0
//productTwo.heightAnchor.constraintEqualToConstant(30).active = true
productTwo.textColor = .blackColor()
//labelTwo.backgroundColor = .blueColor()
productStackView.removeArrangedSubview(productTwo)
//productStackView.addArrangedSubview(productTwo)
let labelThree = UILabel(frame: CGRectZero)
labelThree.text = "sometime"
//labelThree.heightAnchor.constraintEqualToConstant(30).active = true
labelThree.textColor = .blackColor()
//labelThree.backgroundColor = .blueColor()
firstStackView.addArrangedSubview(labelThree)
let descriptionView = UILabel(frame: CGRectZero)
descriptionView.text = "some description about something"
descriptionView.textColor = .blackColor()
//descriptionView.backgroundColor = .redColor()
secondStackView.addArrangedSubview(descriptionView)
let tagsView = UILabel(frame: CGRectZero)
tagsView.text = "some more things"
tagsView.textColor = .blackColor()
secondStackView.addArrangedSubview(tagsView)
secondStackView.trailingAnchor.constraintEqualToAnchor(tagsView.trailingAnchor).active = true
let stackView = UIStackView(arrangedSubviews: [firstStackView, secondStackView])
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
stackView.layoutMarginsRelativeArrangement = true
stackView.axis = .Vertical
stackView.frame = view.bounds
stackView.distribution = .FillProportionally
view.addSubview(stackView)
Before element is removed:
After element is removed:
I would like that gap to be gone and the height adjusted dynamically.

You can add a constraint for the height of the view if you are using auto layout.
So like for initial setup, you could do something like:
class YourClass : UIViewController() {
var heightConstraint = NSLayoutConstraint()
func someMethod () {
// load your View
// get the height of view.
heightConstraint = yourView.heightAnchor.constraintEqualToConstant(height)
self.view.addConstraint(heightConstraint)
}
func deleteMemberStackView() {
/// After deleting the member, get the new height of the view and do this
self.view.removeConstraint(heightConstraint)
heightConstraint = yourView.heightAnchor.constraintEqualToConstant(height)
self.view.addConstraint(heightConstraint)
UIView.animateViewDuration(0.3, completion: {
self.view.layoutIfNeeded()
})
}
}

Related

How to make vertical labels bar in Swift?

I would like to make such layout via swift:
I tried to make it in such way:
var buttonArray = [UILabel]()
for (myKey,myValue) in colorDictionary{
buttonArray += [colorButton(withColor: myValue, title: myKey)]
}
let horizontalStack = UIStackView(arrangedSubviews: buttonArray)
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fillEqually
horizontalStack.alignment = .fill
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
horizontalStack.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
let label2 = UILabel()
label2.text = "Label"
label2.backgroundColor = .red
label2.textColor = .white
label2.textAlignment = .center
label2.lineBreakMode = .byCharWrapping
label2.numberOfLines = 0
label2.translatesAutoresizingMaskIntoConstraints = false
label2.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
let mainStackView = UIStackView()
mainStackView.axis = .horizontal
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(label2)
mainStackView.addArrangedSubview(horizontalStack)
mainContainer.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.topAnchor.constraint(equalTo: mainContainer.topAnchor, constant: 5),
mainStackView.leftAnchor.constraint(equalTo: mainContainer.leftAnchor,
constant: 20),
mainStackView.rightAnchor.constraint(equalTo: mainContainer.rightAnchor,
constant: -20),
mainStackView.heightAnchor.constraint(equalToConstant: 270),
])
where colorButton is:
func colorButton(withColor color:UIColor, title:String) -> UILabel{
let newButton = UILabel()
newButton.backgroundColor = color
newButton.text = title
newButton.textAlignment = .center
newButton.textColor = UIColor.white
return newButton
}
and here is the result which I got:
How I can make all these labels look like the desired image? And also I'm not sure whether label rotation can be done by my method.
What I would do is first get it working without any transformations so it appears as a normal, not rotated setup. Once that is working, you only need to apply a single transformation to the main stack view to get the whole thing rotated. Then finally you can tweak the constraints to get it positioned correctly.
Here is code that works:
let horizontalStack = UIStackView(arrangedSubviews: buttonArray)
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fillEqually
horizontalStack.alignment = .fill
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
let label2 = UILabel()
label2.text = "Label"
label2.backgroundColor = .red
label2.textColor = .white
label2.textAlignment = .center
label2.lineBreakMode = .byCharWrapping
label2.numberOfLines = 0
label2.translatesAutoresizingMaskIntoConstraints = false
let mainStackView = UIStackView()
mainStackView.axis = .vertical
mainStackView.distribution = .equalSpacing
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(label2)
mainStackView.addArrangedSubview(horizontalStack)
mainStackView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
mainContainer.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.centerYAnchor.constraint(equalTo: mainContainer.centerYAnchor),
mainStackView.centerXAnchor.constraint(equalTo: mainContainer.leftAnchor, constant: 40),
// Set the "width", not the "height"
mainStackView.widthAnchor.constraint(equalToConstant: 270),
])
Changes:
I removed the transformation you had for the horizontal stack view and the big label.
I added a single transformation to the main stack view.
I used a vertical, not horizontal, stack view for the main stack.
Updated the properties of the main stack view so the main label fills the area above the other labels.
Updated the constraints. Adjust those to suit your needs.
Note that you need to set the width of the main stack view since the constraint is relative to the untransformed main stack view.

UIStackView stretching out sub view aligning it

I'm trying to lay out a StackView programatically, the effect I would like to achieve is
but instead I am getting
I do not understand why the loadingDotView is stretching to fill up all the space?
let loadingDotView: UIView = {
let ldv = UIView()
ldv.backgroundColor = .white
ldv.alpha = 0
ldv.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
ldv.layer.cornerRadius = 10
ldv.layer.masksToBounds = true
ldv.translatesAutoresizingMaskIntoConstraints = false
return ldv
}()
let dotsStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
Setup code...
view.addSubview(dotsStackView)
NSLayoutConstraint.activate([
dotsStackView.heightAnchor.constraint(equalToConstant: 20),
dotsStackView.widthAnchor.constraint(equalToConstant: 100),
dotsStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
dotsStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
dotsStackView.addArrangedSubview(loadingDotView)
dotsStackView.addArrangedSubview(loadingDotView)
dotsStackView.addArrangedSubview(loadingDotView)
This ( closure )
let loadingDotView: UIView = {
let ldv = UIView()
ldv.backgroundColor = .white
ldv.alpha = 0
ldv.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
ldv.layer.cornerRadius = 10
ldv.layer.masksToBounds = true
ldv.translatesAutoresizingMaskIntoConstraints = false
return ldv
}()
returns same object every access so only one appears , make it ( computed property ) to create a new one every access
var loadingDotView: UIView {
let ldv = UIView()
ldv.backgroundColor = .white
ldv.alpha = 0
ldv.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
ldv.layer.cornerRadius = 10
ldv.layer.masksToBounds = true
ldv.translatesAutoresizingMaskIntoConstraints = false
return ldv
}
And add
stackView.spacing = 20
stackView.distribution = .fillEqually

Images not being rounded in UIStackView

I'm trying to round several images and add them in a stack view, for some reason it's not working. Basically, I have an array with all UIImage names and assign them to a UIImageView. When I try to make a circle out of the image, it's not working :
var navStackView : UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fillEqually
// stack.spacing = -10
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
var images = ["1", "2", "3", "4"]
override func viewDidLoad() {
super.viewDidLoad()
let navController = navigationController!
navController.navigationBar.addSubview(navStackView)
// x, y, w, h
navStackView.leadingAnchor.constraint(equalTo: navController.navigationBar.leadingAnchor).isActive = true
navStackView.trailingAnchor.constraint(equalTo: navController.navigationBar.trailingAnchor).isActive = true
navStackView.topAnchor.constraint(equalTo: navController.navigationBar.topAnchor).isActive = true
navStackView.bottomAnchor.constraint(equalTo: navController.navigationBar.bottomAnchor).isActive = true
for image in images {
let imageView = UIImageView()
imageView.image = UIImage(named: image)
imageView.layer.cornerRadius = imageView.frame.height / 2
imageView.clipsToBounds = true
imageView.layer.masksToBounds = false
imageView.contentMode = .scaleAspectFit
// imageView.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
navStackView.addArrangedSubview(imageView)
navStackView.layoutIfNeeded()
}
navigationItem.titleView = navStackView
}
You can try
var once = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if once {
navStackView.subviews.forEach { $0.layer.cornerRadius = $0.frame.height / 2 }
once = false
}
}

Stackview in navigation bar

Is it possible to place UIStackView in NavigationBar programmaticaly using swift? I want to place there StackView with two arranged stackviews. But when i do that , it shows nothing in navigation bar. If it is possible, please provide example. Thanks
Finally I found solution
let btnSort = UIButton(type: .system)
btnSort.frame = CGRect(x: 0, y: 0, width: 120, height: 40)
btnSort.tintColor = UIColor.white
btnSort.setImage(UIImage(named:"ic_controls_icon.png"), for: .normal)
btnSort.imageEdgeInsets = UIEdgeInsets(top: 6,left: -10,bottom: 6,right: 34)
btnSort.titleEdgeInsets = UIEdgeInsets(top: 0,left: 0,bottom: 0,right: 14)
btnSort.setTitle("SORT", for: .normal)
btnSort.layer.borderWidth = 1.0
btnSort.backgroundColor = UIColor.red //--> set the background color and check
btnSort.layer.borderColor = UIColor.white.cgColor
let btnControl = UIButton(type: .system)
btnControl.frame = CGRect(x: 0, y: 0, width: 120, height: 40)
btnControl.tintColor = UIColor.white
btnControl.setImage(UIImage(named:"ic_controls_icon.png"), for: .normal)
btnControl.imageEdgeInsets = UIEdgeInsets(top: 6,left: -10,bottom: 6,right: 34)
btnControl.titleEdgeInsets = UIEdgeInsets(top: 0,left: 0,bottom: 0,right: 14)
btnControl.setTitle("SORT", for: .normal)
btnControl.layer.borderWidth = 1.0
btnControl.backgroundColor = UIColor.red //--> set the background color and check
btnControl.layer.borderColor = UIColor.white.cgColor
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
view.axis = .horizontal
view.distribution = .fillEqually
view.spacing = 5
view.addArrangedSubview(btnSort)
view.addArrangedSubview(btnControl)
let mainTitleView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
mainTitleView.addSubview(view)
navigationItem.titleView = mainTitleView
You can make any UIView subclass (of which UIStackView is one) the navigation bar's title using your view controller's navigationItem.titleView property.
You can test this out in a playground…
import UIKit
import PlaygroundSupport
let vc = UIViewController()
vc.view.backgroundColor = .white
let nav = UINavigationController(rootViewController: vc)
let topLabel = UILabel()
topLabel.font = UIFont.boldSystemFont(ofSize: 20)
topLabel.text = "Hello"
let bottomLabel = UILabel()
bottomLabel.font = UIFont.systemFont(ofSize: 16)
bottomLabel.text = "World!"
let stackView = UIStackView(arrangedSubviews: [topLabel, bottomLabel])
stackView.axis = .vertical
vc.navigationItem.titleView = stackView
PlaygroundPage.current.liveView = nav.view
PlaygroundPage.current.needsIndefiniteExecution = true
var navTitle: String? = "Preview Checklist"
var navSubTitle: String? = "Edit Checklist >"
lazy var titleStackView: UIStackView = {
let titleLabel = UILabel()
titleLabel.textAlignment = .left
titleLabel.text = navTitle
//titleLabel.font = UIFont(name: "RawlineMedium-Regular", size:CGFloat(15))
titleLabel.textColor = .white
let subtitleLabel = UILabel()
subtitleLabel.textAlignment = .left
subtitleLabel.text = navSubTitle
//subtitleLabel.font = UIFont(name: "RawlineMedium-Regular", size:CGFloat(11))
subtitleLabel.textColor = .white
let stackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel])
stackView.axis = .vertical
stackView.backgroundColor = .blue
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.titleView = titleStackView
}

Add subtitle under the title in navigation bar controller in Xcode

So I'm wanting to add a "subtitle" under the title in the navigation bar in navigation controller.
Mostly everything I look up so far wants me to use CGRect. I don't know a whole lot what that is and it sounds like its wanting me to create an entire new view which is not what I am wanting to do.
My question is, is there a dot method to adding a subtitle view easily?
The closest thing I found was posted on stack overflow and here is the link:
Create a subtitle in navigationbar
Apparently last year this worked but now I am getting errors and it's in my viewDidLoad...
I tried this:
self.navigationController?.navigationItem.prompt = "Subtitle Here"
It's the only thing that won't show any errors but still doesn't work. It literally does nothing. At least nothing visible at run time.
On a side note, swift is preferred. Thanks!
Here is my version using a stack view on an extension.
extension UINavigationItem {
func setTitle(title:String, subtitle:String) {
let one = UILabel()
one.text = title
one.font = UIFont.systemFont(ofSize: 17)
one.sizeToFit()
let two = UILabel()
two.text = subtitle
two.font = UIFont.systemFont(ofSize: 12)
two.textAlignment = .center
two.sizeToFit()
let stackView = UIStackView(arrangedSubviews: [one, two])
stackView.distribution = .equalCentering
stackView.axis = .vertical
stackView.alignment = .center
let width = max(one.frame.size.width, two.frame.size.width)
stackView.frame = CGRect(x: 0, y: 0, width: width, height: 35)
one.sizeToFit()
two.sizeToFit()
self.titleView = stackView
}
}
Though there is a solution but it has some known issues
Solution is writing a function like this
func setTitle(title:String, subtitle:String) -> UIView {
let titleLabel = UILabel(frame: CGRectMake(0, -2, 0, 0))
titleLabel.backgroundColor = UIColor.clearColor()
titleLabel.textColor = UIColor.grayColor()
titleLabel.font = UIFont.boldSystemFontOfSize(17)
titleLabel.text = title
titleLabel.sizeToFit()
let subtitleLabel = UILabel(frame: CGRectMake(0, 18, 0, 0))
subtitleLabel.backgroundColor = UIColor.clearColor()
subtitleLabel.textColor = UIColor.blackColor()
subtitleLabel.font = UIFont.systemFontOfSize(12)
subtitleLabel.text = subtitle
subtitleLabel.sizeToFit()
let titleView = UIView(frame: CGRectMake(0, 0, max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), 30))
titleView.addSubview(titleLabel)
titleView.addSubview(subtitleLabel)
let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width
if widthDiff < 0 {
let newX = widthDiff / 2
subtitleLabel.frame.origin.x = abs(newX)
} else {
let newX = widthDiff / 2
titleLabel.frame.origin.x = newX
}
return titleView
}
Using this function for custom navigation title view in viewDidLoad
self.navigationItem.titleView = setTitle("Title", subtitle: "SubTitle")
Only known issue is that if subtitle becomes very large than the misplacement occurs.
Final Outcome
Source: https://gist.github.com/nazywamsiepawel/0166e8a71d74e96c7898
#iosjillian's Swift 4 extension works great, adding a bit more to honor the bar's appearance and user font preferences:
import UIKit
extension UINavigationItem {
func setTitle(_ title: String, subtitle: String) {
let appearance = UINavigationBar.appearance()
let textColor = appearance.titleTextAttributes?[NSAttributedString.Key.foregroundColor] as? UIColor ?? .black
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .preferredFont(forTextStyle: UIFont.TextStyle.headline)
titleLabel.textColor = textColor
let subtitleLabel = UILabel()
subtitleLabel.text = subtitle
subtitleLabel.font = .preferredFont(forTextStyle: UIFont.TextStyle.subheadline)
subtitleLabel.textColor = textColor.withAlphaComponent(0.75)
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
stackView.distribution = .equalCentering
stackView.alignment = .center
stackView.axis = .vertical
self.titleView = stackView
}
}
Thanks a lot for your answer! #RajanMaheshwari
Your coding worked perfectly except the if statement you made with the widthDiff..
I adjusted it a little bit and everything worked smoothly.
if widthDiff < 0 {
let newX = widthDiff / 2
subtitleLabel.frame.origin.x = abs(newX)
} else {
let newX = widthDiff / 2
titleLabel.frame.origin.x = newX
}
Thanks again for your response!
I really liked #user2325031's answer, but found that sizing the labels to fit and setting the frame wasn't needed. I also set the stackView's alignment to .center per #GerardoMR's suggestion.
extension UINavigationItem {
func setTitle(_ title: String, subtitle: String) {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 17.0)
titleLabel.textColor = .black
let subtitleLabel = UILabel()
subtitleLabel.text = subtitle
subtitleLabel.font = .systemFont(ofSize: 12.0)
subtitleLabel.textColor = .gray
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
stackView.distribution = .equalCentering
stackView.alignment = .center
stackView.axis = .vertical
self.titleView = stackView
}
}
In case anyone looking for Objective-C code of the above mentioned solution:
UILabel *title = [[UILabel alloc]init];
UILabel *subtitle = [[UILabel alloc]init];
[title setFont:[UIFont systemFontOfSize:12]];
[title setTextColor:[UIColor whiteColor]];
[title setFont:[UIFont systemFontOfSize:17]];
[title sizeToFit];
title.text = #"Title";
[subtitle setTextColor:[UIColor whiteColor]];
[subtitle setFont:[UIFont systemFontOfSize:12]];
[subtitle setTextAlignment:NSTextAlignmentCenter];
[subtitle sizeToFit];
subtitle.text = #"Subtitle Title";
UIStackView *stackVw = [[UIStackView alloc]initWithArrangedSubviews:#[title,subtitle]];
stackVw.distribution = UIStackViewDistributionEqualCentering;
stackVw.axis = UILayoutConstraintAxisVertical;
stackVw.alignment =UIStackViewAlignmentCenter;
[stackVw setFrame:CGRectMake(0, 0, MAX(title.frame.size.width, subtitle.frame.size.width), 35)];
self.navigationItem.titleView = stackVw;
Thanks for the answer #RajanMaheshwari
If anyone is having the issue where the title becomes misaligned when the subtitle text is longer than the title text, I added the following code to the Rajan's answer above just below where the subtitleLabel is instantiated:
// Fix incorrect width bug
if (subtitleLabel.frame.size.width > titleLabel.frame.size.width) {
var titleFrame = titleLabel.frame
titleFrame.size.width = subtitleLabel.frame.size.width
titleLabel.frame = titleFrame
titleLabel.textAlignment = .center
}
Hope this helps someone who encountered the same issue as me
Another solution, using only one label and NSAttributedString to differentiate between title and subtitle (with different font sizes, weights, colors, etc.) instead. Removes the problem of different label alignment.
extension UIViewController {
func setTitle(_ title: String, subtitle: String) {
let rect = CGRect(x: 0, y: 0, width: 400, height: 50)
let titleSize: CGFloat = 20 // adjust as needed
let subtitleSize: CGFloat = 15
let label = UILabel(frame: rect)
label.backgroundColor = .clear
label.numberOfLines = 2
label.textAlignment = .center
label.textColor = .black
let text = NSMutableAttributedString()
text.append(NSAttributedString(string: title, attributes: [.font : UIFont.boldSystemFont(ofSize: titleSize)]))
text.append(NSAttributedString(string: "\n\(subtitle)", attributes: [.font : UIFont.systemFont(ofSize: subtitleSize)]))
label.attributedText = text
self.navigationItem.titleView = label
}
}
Custom titleView based in part on https://stackoverflow.com/a/34298491/3918865
Swift 4:
import UIKit
class NavigationTitleView: UIView {
private var contentStackView = UIStackView()
private var titleLabel = UILabel()
private var subTitleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
viewConfig()
addViewsConfig()
layoutViewsConfig()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func set(title: String, subTitle: String){
self.titleLabel.text = title
self.subTitleLabel.text = subTitle
}
private func viewConfig() {
contentStackView.axis = .vertical
contentStackView.alignment = .center
contentStackView.distribution = .fill
contentStackView.spacing = 5
self.backgroundColor = .clear
self.titleLabel.textColor = .white
self.self.subTitleLabel.textColor = .white
}
private func addViewsConfig() {
contentStackView.addArrangedSubview(subTitleLabel)
contentStackView.addArrangedSubview(titleLabel)
self.addSubview(contentStackView)
}
private func layoutViewsConfig(){
contentStackView.translatesAutoresizingMaskIntoConstraints = false
contentStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0.0).isActive = true
contentStackView.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0.0).isActive = true
}
}
Use:
import UIKit
class ViewController: UIViewController {
private var navigationTitleView = NavigationTitleView()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.titleView = navigationTitleView
navigationTitleView.set(title: "title", subTitle: "subTitle")
}
}
Easy fix for iOS 16. Following the #iosjillian's / #Dan's approx, calling layoutSubviews() on stack view does the trick.
extension UINavigationItem {
func setTitle(_ title: String, subtitle: String) {
let textColor = getTextColor()
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .preferredFont(forTextStyle: UIFont.TextStyle.headline)
titleLabel.textColor = textColor
let subtitleLabel = UILabel()
subtitleLabel.text = subtitle
subtitleLabel.font = .preferredFont(forTextStyle: UIFont.TextStyle.subheadline)
subtitleLabel.textColor = textColor.withAlphaComponent(0.75)
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
stackView.distribution = .equalCentering
stackView.alignment = .center
stackView.axis = .vertical
stackView.layoutSubviews()
self.titleView = stackView
}
}
Working iOS 16 solution. Swift 5.7
With SnapKit library. If you are not using SnapKit lib, just make both views (titleLabel and subtitleLabel) translatesAutoresizingMaskIntoConstraints = false and replace SnapKit Constraints with native constraints.
func setTitle(title: String, subtitle: String, view: UIView) -> UIView {
let appearance = UINavigationBar.appearance()
let titleColor = appearance.titleTextAttributes?[NSAttributedString.Key.foregroundColor] as? UIColor ?? .black
let titleLabel = UILabel()
titleLabel.backgroundColor = UIColor.clear
titleLabel.textColor = titleColor
titleLabel.adjustsFontSizeToFitWidth = false
titleLabel.font = .preferredFont(forTextStyle: UIFont.TextStyle.headline)
titleLabel.lineBreakMode = .byTruncatingTail
titleLabel.textAlignment = .center
titleLabel.text = title
let subtitleLabel = UILabel()
subtitleLabel.backgroundColor = UIColor.clear
subtitleLabel.textColor = UIColor.init(hexString: "#808890")
subtitleLabel.adjustsFontSizeToFitWidth = false
subtitleLabel.lineBreakMode = .byTruncatingTail
subtitleLabel.textAlignment = .center
subtitleLabel.font = UIFont.systemFont(ofSize: 11)
subtitleLabel.text = subtitle
let titleView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 30))
titleView.addSubview(titleLabel)
titleView.addSubview(subtitleLabel)
titleLabel.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview()
make.top.equalToSuperview().offset(-20)
make.height.equalTo(20)
}
subtitleLabel.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.height.equalTo(10)
}
return titleView
}