iOS + Autolayout: Adding constraints to text of UILabel/Button, not frame - ios

I have a UILabel and a UIButton, and in both cases the frame is a lot bigger than the text presented. I'd rather not change this if I can.
Now, I would like to add a spinner (UIActivityIndicatorView) right after the text, how do I do it? What I have right now is a constraint from the spinner's trailing edge to the button/label's trailing edge and a constant of -40. This constant if ok for english, but might be wrong for other languages.
Any ideas? Thanks!

Follow these steps :
Take leading constraint for UIActivityIndicator.
Calculate width of the text using this (create extension) :
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedStringKey.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
Set leading constraint of UIActivityIndicator
to width + 5 (+ 5 for leaving some gap).

Related

Word Wrap Occurs Inconsistently in UILabel

I have a UILabel that is designed to expand in height when the width of the text's CGSize is greater than the width of the label. I accomplish that with this code:
func viewHeight(_ locationName: String) -> CGFloat {
let locationName = tappedLocation[0].name
var size = CGSize()
if let font = UIFont(name: ".SFUIText", size: 17.0) {
let fontAttributes = [NSAttributedStringKey.font: font]
size = (locationName as NSString).size(withAttributes: fontAttributes)
}
let normalCellHeight = horizontalStackViewHeightConstraint.constant
let extraLargeCellHeight = horizontalStackViewHeightConstraint.constant + 20.33
let textWidth = ceil(size.width)
let cellWidth = ceil(nameLabel.frame.width)
if textWidth > cellWidth {
return extraLargeCellHeight
} else {
return normalCellHeight
}
}
Lines = 0 and line break style = Word Wrap:
The label lives inside a vertical stackView, and is constrained to its top, leading and trailing edges and a stackView beneath it. The height of the label and the UIView properly expand in height when the CGSize width of the text is longer than the width of the label. All well and good.
However, the words do not wrap consistently. This behavior is intentional:
Bobby Mao's Chinese Kitchen & Bar:
XL cell. Width: 184.0,
Text width: 287.0
This behavior is not (why isn't "steak" on the prior line?):
Ruth's Chris Steak House:
XL cell. Width: 184.0,
Text width: 204.0
And neither is this (why didn't Gina wrap if it's over the label width parameter?):
Ristorante Mamma Gina:
XL cell. Width: 184.0,
Text width: 191.0
I have also set a temporary background color on my label to ensure that it does, in fact correspond to the intended width. The label in this example creates another line when the label's width is exceeded, but the text does not wrap:
I have read the other entries on Stack Overflow about word wrapping. I don't believe this is a duplicate. I do not have trouble creating two lines for my text. I don't have trouble with word wrapping occurring. I have trouble with how and when it is occurring.
I think the intent is clear... what am I missing?

Layout for different iPhone screens

I'm building my first real-world app after going through some tutorials and I've come across a layout issue. It is quite simple to adjust UI layout to different screen size classes, but I haven't found any information on how to adjust layout within same size class.
For example, I have a label whose Top Space constraint is set to 40 pt form top of view. It looks neat on a large iPhone 8 Plus screen:
But on a smaller iPhone SE screen (which is confusingly of same size class) this constraint of 40 pt pushes the label halfway through to the center, leaving reasonably less useful space below it:
So I was wondering if there's a way to set different constraints for different iPhones: say, 40 pt for iPhone 8 Plus, 30 pt for iPhone 8 and 20 pt for iPhone SE. Same goes about positioning other views below the label: I want them more compact vertical-wise on a small iPhone screen, and having more space between them on large screen. I know this last part can be solved with a stack view, but it's not always convenient to use.
UPD. Here is a full layout of the view on 8 Plus screen:
It has 3 fixed constraints:
1. From 'Title' label to top of the view - 50 pt
2. From 'Percent' label to bottom of 'Title' label - 60 pt
3. From 'Details' label to bottom of the view - 80 pt.
I've used text autoshrink in all labels + height of each label is proportional to view's height. This made layout a bit more flexible, but still there's a noticible issue on small SE screen:
As you can see, 'Details' is squeezed to 'Percent' label. At this point it would be great to move 'Percent' label higher up and closer to 'Title', but unlike heights constraints cannot be set in proportion (not in IB at least) to Superview height.
One of the options I see is to put a blank view between top and mid labels, making its height proportional and setting 'Percent' label top constraint at 0 to this blank view. Not sure though using such a "crutch" is a good practice.
You may get your most satisfactory results by using a single UILabel and setting the Attributed Text, instead of trying to get multiple labels and font sizes to cooperate.
Try this:
Create a new View Controller
add a normal UILabel
set constraints to 85% of width and 80% of height, and centered both ways
connect the label to an IBOutlet
then:
class ScalingViewController: UIViewController {
#IBOutlet weak var theLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let titleFont = UIFont.systemFont(ofSize: 80.0, weight: UIFontWeightThin)
let pctFont = UIFont.systemFont(ofSize: 100.0, weight: UIFontWeightThin)
let paraFont = UIFont.systemFont(ofSize: 30.0, weight: UIFontWeightLight)
// for blank lines between Title and Percent and between Percent and Body
let blankLineFont = UIFont.systemFont(ofSize: 36.0, weight: UIFontWeightLight)
let sTitle = "Title"
let sPct = "78%"
let sBody = "A detailed text explaining the meaning of percentage above and what a person should do to make it lower or higher."
// create the Attributed String by combining Title, Percent and Body, plus blank lines
let attText = NSMutableAttributedString()
attText.append(NSMutableAttributedString(string: sTitle, attributes: [NSFontAttributeName: titleFont]))
attText.append(NSMutableAttributedString(string: "\n\n", attributes: [NSFontAttributeName: blankLineFont]))
attText.append(NSMutableAttributedString(string: sPct, attributes: [NSFontAttributeName: pctFont]))
attText.append(NSMutableAttributedString(string: "\n\n", attributes: [NSFontAttributeName: blankLineFont]))
attText.append(NSMutableAttributedString(string: sBody, attributes: [NSFontAttributeName: paraFont]))
// these properties can be set in Interface Builder... or set them here to make sure.
theLabel.textAlignment = .center
theLabel.numberOfLines = 0
theLabel.adjustsFontSizeToFitWidth = true
theLabel.minimumScaleFactor = 0.05
// set the label content
theLabel.attributedText = attText
}
}
This gives me these results for 7+, 6s and SE:
And, just for demonstration's sake, how it looks with additional text in the "body" paragraph:

Swift 3 how to anchor bottom when width is exceeded

I am coding an app and I want to know how I can anchor a view to the bottom instead of right side of the previous view when total width in the same row exceeds device width.
Basically like this:
View 1: width=150 anchor to superview's leftAnchor
View 2: width=500 anchor to view1's rightAnchor
View 3: width=1000 anchor to view1's bottomAnchor because total width in same row
exceeds device width
#device width=1200
Any help is appreciated. Sorry for bad English, not a native speaker.
Ps. all heights are same, width is dynamic
The main challenge was I couldn't know the width of my views until they are constrained.
So I used swift's estimation.
let size = CGSize(width: UIScreen.main.bounds.width, height: 50)
let attributeHeight = [NSFontAttributeName: UIFont.preferredFont(forTextStyle: .subheadline)]
let estimatedHeight = NSString(string: _title).boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributeHeight, context: nil)
self._estimatedWidth = estimatedHeight.width + 32
after that it was easy to anchor them according to my needs with a for loop

NSLabel Autoshrink with top constraints

I try to figure out autoshrink option in UILabel with regarding top constraint.
I have UIView and three labels. One of them has autoshrink option on. It has constraint to be centered, and has Trailing and Top constraint which should shrink label when changing size of UIView. If I make UIVIew thinner, font size is decreased, but if I change height of UIView font is not changed.
Constraints on UILabels :
Align Center X to Superview
Align Center Y to Superview
Trailing Space to Superview >= 50
Top Space to Superview >= 40
Align Center X to label2
Top Space to label1 equals :15
Bottom space to label2 equals :3
Label 1 constraints :
Align Center x to superview
Trailing Space to superview >=10
Leading Space to superview >=10
Bottom Space to Shrink Label equal 15
Label 2 constraint :
Align Center X to Shrink label
Top Space to Shrink label equals 3
How to change this?
What I want is, on last image that label will be nice autoshrink. So if I change width or height of the UIView label should shrink.
plz select your Shrink label set
Number of lines is 0
Line Breaks: Clip
Autoshrink: Minimum Font Scale 0.25
I hope below code will help you in some way,
extension Double {
/// Returns propotional width according to device width
var propotional: CGFloat {
if UIDevice.current.userInterfaceIdiom == .pad {
return CGFloat(414.0) / CGFloat(375.0) * CGFloat(self)
}
return CGFloat(Screen.width) / CGFloat(375.0) * CGFloat(self)
}
}
extension UILabel {
/// This property is change font-size of Label's text propotionaly, if assigned `On` from Interface builder
#IBInspectable
var isPropotional: Bool{
get {
return true
}
set {
if newValue == true {
let fontSize = Double((self.font!.pointSize))
self.font = UIFont(name: self.font!.fontName, size: fontSize.propotional)
}
}
}
}
In extension of Double, propotional value is calculated by considering current device set in storyboard as iPhone 6 size.
After adding this code set on from interface builder for Label where you can see isPropotional attribute in attribute inspector tab. Please refer image.

How to let TextView be able to scroll horizontally

I know the TextView is embedded in a ScrollView. And if there is a fairly long String(Which contains no "\n")
The TextView will automatically do the line-wrap, according to the width of the TextView.
If TextView's height is short, then we are able to scroll it vertically.
How do you disable the auto line-wrap? Such that, if there are no "\n" encounters, it does not line wrap. Rather, it lets the user scroll horizontally to view the text.
How can I implement this?
I figure out how to do this with many helps of you guys :D, thanks, and here we go!
1. So, firstly, We need a longlonglong String, right?
let displayStr = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 "
2. Assume we have a UIScrollView which is linked by #IBOutlet or got by calling the .viewWithTag(xxx) .We will let it be named as scroll :
3. It's time to get the size of our string, here is a key function we'll use it:
Oh! I almost forget to define what kind of Font( this's a crucial parameter ) we will use, and What is the max-size of our string's size
let maxSize = CGSizeMake(9999, 9999)
let font = UIFont(name: "Menlo", size: 16)!
//key function is coming!!!
let strSize = (displayStr as NSString).boundingRectWithSize(maxSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : font], context: nil)
4. OK, now we can put the string into a textView, you need to programmatically create a new UITextView, so that we can define the frame which is identical to the string's size(Oh, maybe a little bigger :D) :
let frame = CGRectMake(0, 0, size.width+50, size.height+10)
let textView = UITextView(frame: frame)
textView.editable = false
textView.scrollEnabled = false//let textView becomes unScrollable
textView.font = font
textView.text = displayStr
5. Back to our scroll, we will set its contentSize:
scroll.contentSize = CGSizeMake(size.width, size.height)
6. Finally, addSubview:
scroll.addSubview(textView)
You can see, textView is embed in a scrollView, which allow it to
scroll with 2 directions.
B.T.W. My implement is just a demo for
static String. if you want user to use a textView which will not line
wrap if he doesn't input any "\n", you may need dynamically calculate
the string size. :D
[I hope this will help]
[myTextView setContentSize:CGSizeMake(width, myTextView.frame.size.height)];
The width of the content extends past the width of the textView's frame or else it won't scroll.
Turn off all the scroll options on the UITextView, then embed it in another UIScrollView. Reference:- DualScrollTextView
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var textView: UITextView!
var yourText: String = "Whatever your text is going to be."
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.layoutIfNeeded()
self.scrollView.contentSize = self.textView.bounds.size
}
override func viewDidLoad() {
super.viewDidLoad()
textView.text = yourText
textView.sizeThatFits(CGSizeMake(textView.frame.size.width, 80)) //height to equal the actual size of your text view.
}
I think this should plug and play, but I know for sure that textView.sizeThatFits works. You have to make sure that you constrain your text view to it's parent scrollview for it to work. (Leading, Trailing, Top, Bottom and the Height.)
The accepted answer didn't work with AutoLayout so I'll share my approach:
1. Add a UIScrollView with a UITextView inside it and pin all the edges for both of them
2. Add width and height constraints for your UITextView (doesn't matter what you set them to)
3. Create IBOutlets for the UITextView, the height constraint, and the width constraint
4. Uncheck 'Scrolling Enabled' on the TextView
5. When you update the text, calculate the bounding size of the text and update the height and width constraints with the bounding size
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var textViewWidthConstraint: NSLayoutConstraint!
let displayText = "Your text here."
override func viewDidLoad() {
super.viewDidLoad()
let maxSize = CGSize(width: 10000, height: 30000)
let textRect = displayText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font : self.textView.font!], context: nil)
self.textView.text = displayText
self.textViewHeightConstraint.constant = textRect.height
self.textViewWidthConstraint.constant = textRect.width
}
UPDATE: I discovered that this approach uses a lot of memory for large amounts of text (I was seeing over 1GB used). Here's how I reduced the memory impact from 1GB to 100MB:
1. Set the edge constraints just like step 1 above.
2. Add a width constraint to the TextView for the scrollable content width (I used 2000 but you can adjust it to your liking, the wider you go the more memory you'll use though).
3. Add a constraint to make the TextView and ScrollView have equal heights
That's it! The scrollable width will be constant and once you set the text for the TextView the scrollable height will automatically adjust.
Note: Some caveats of this approach:
The scrollable width will always be the width you set but you can use a hybrid approach with the first solution if you want to make it sized based on the text
You can't see the vertical scroll bar (unless you're scrolled all the way to the right) but it's possible to get it back by adjusting the scroll bar inset of the TextView.
Reference: https://www.ralfebert.de/ios-examples/auto-layout/uiscrollview-storyboard/

Resources