Layout for different iPhone screens - ios

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:

Related

Find width of string (swift)

How do I find the width of a string (CGFloat) given the font name and font size?
(The goal is to set the width of a UIView to be just wide enough to hold the string.)
I have two strings: one with "1" repeated 36 times, the other with "M" repeated 36 times. These both fill the width (359.0) of the screen (give or take a little for margins).
I am using using Courier 16, which is monospaced, so I expect the width of both strings to be equal (as they in fact do appear on the screen).
However, using https://stackoverflow.com/a/58782429/8635708 :
the width of the string with the "1"s is 257.34375
the width of the string with the "M"s is 492.1875.
The first is does not fill the screen, the other is way too long.
And using https://stackoverflow.com/a/58795998/8635708 :
the width of each string is 249.640625.
At least here, they are the same, but that value clearly does not fill the screen.
I think you could create a label and call label.sizeToFit():
let label = UILabel()
label.font = UIFont.init(name: "Courier", size: 16)
label.text = "mmmmmmmmmmmmmmmm"//"1111111111111111"
label.sizeToFit()
print("Width: \(label.frame.size.width)") //153.66666666666666 -> For both strings

Make UILabel auto-adjust font size to screen width, but not to text length

In a very simple single screen app, I have a single-line UILabel going from left edge to right edge of the screen.
The text of the label is dynamically updated at runtime. The length of the text varies, as it contains a number in the range 0...100, and I am neither using a monospaced font nor leading zeroes.
Here is an illustration:
|<--------- Screen width --------------->|
|<----- UILabel "Some value = 0" ------->|
|<----- UILabel "Some value = 50" ------>|
|<----- UILabel "Some value = 100" ----->|
I would like the label to always use the maximum width for any device (i.e. screen size). This can be achieved by using auto-layout, suitable leading and trailing constraints, a large font size and the "auto-shrink" property for the label.
The problem is, that this approach will make the font size also vary depending on the value displayed, which is not what I want. It should only vary with the width of the screen, but not with the length of the label text.
In the example above, a large font size would be used for the value 0, a medium one for 50 and a small one for 100. I want it to adjust to the worst-case (100) and use the resulting size for any text afterwards.
Is it possible to achieve this using Interface-Builder properties and auto-layout constraints only?
I can think of ways how to calculate sizes in code, but I think there must be an easier way.
You cannot express font size as a proportion of view width in interface builder but you can do it very easily in code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let textSize = Constant.baseFontSize * bounds.size.width / Constant.baseViewWidth
label.font = .systemFont(ofSize: textSize)
}

How to align Right-Justify UILabel?

Remark:
Implementing:
myLabel.textAlignment = .right
does not solves my problem, that is not what I am asking for.
What I am trying to achieve is to let the alignment for the label to be Right-Justify.
To make it more clear:
That's how left alignment looks:
And that's how justify alignment looks:
if you are not seeing any difference between the tow screenshots, you could recognize it by the right (trailing) constraint of the label, you will notice in the second screenshot the whole width of the label has been filled by the text.
As shown in the second screenshot, letting the label to be justified will make it Left-Justify by default, i.e the last line alignment is left.
How can I make let the label to be justified to the right? In other words, I want the text to be just like the 2nd screenshot except that I want the last short line to be shifted to the right.
If you wondering what is the purpose of doing such a thing, it would be appropriate for right to left languages.
Thanks to #user6788419 for mentioning the appropriate answer.
For achieving it, you should work with NSMutableParagraphStyle:
An object that enables changing the values of the subattributes in a
paragraph style attribute.
It gives you the ability to the alignment and baseWritingDirection simultaneously, which leads to the desired output:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var lblDescription: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let text: NSMutableAttributedString = NSMutableAttributedString(string: "In vertebrates, it is composed of blood cells suspended in blood plasma. Plasma, which constitutes 55% of blood fluid, is mostly water (92% by volume), and contains dissipated proteins...")
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .justified
paragraphStyle.baseWritingDirection = .rightToLeft
paragraphStyle.lineBreakMode = .byWordWrapping
text.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, text.length))
lblDescription.attributedText = text
}
}
The output would be:
There is not ENUM available for right justified!
You have to choose from NSTextAlignmentLeft,NSTextAlignmentRight,NSTextAlignmentCenter,NSTextAlignmentJustified,NSTextAlignmentNatural.
You have added screenshot of xcode's interface builder! once check it in simulator or device.
If you want space at left or right side in label with justified text then you can play with edge insets.
For example Left padding in label.

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/

How to adjust font size of a label according to length of string and label size

I have a multi line label with a set size (300 x 300).
I want to adjust the label's font size programmatically according to how long the label's text is and how big the label is.
Here are 2 examples of the same sized labels with different length text strings
Refer to NSString.boundingRectWithSize:options:attributes:. Here you need to decrement the font size stepwise until the string will fit into the original frame.
var paragraph = NSMutableParagraphStyle()
let layout = [NSFontAttributeName:NSFont.systemFontOfSize(0), NSParagraphStyleAttributeName: paragraph, ]
paragraph.lineBreakMode = NSLineBreakMode.ByWordWrapping
var a = NSString(string: "My long text")
let rect = NSMakeSize(30, 20)
let bb = a.boundingRectWithSize(rect, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: layout)
Place the above in Playground and modify rect and the font to see what happens.
I know the Question is old but....
Just use the option AutoShrink and set the minimum font size/scale in the Attributes Inspector, it will scale the font as large as possible to fit the size of the label.

Resources