How can I prevent orphans in a label in swift? - ios

I have a label that can have one or two lines. If it has two lines, I want the second line to have at least two (or maybe three) words, never just one. Any ideas about how I can accomplish that using swift?
Thanks in advance!
Daniel
Edit: I edited out my silly first thoughts that didn't really help.

Ok, after looking around a lot I came up with what I think is the best solution.
I wrote this function:
func numberOfLinesInLabelForText(text: String) -> Int {
let attributes = [NSFontAttributeName : UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)]
let screenSize: CGRect = UIScreen.mainScreen().bounds
let labelSize = text!.boundingRectWithSize(CGSizeMake((screenSize.width - 30), CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: attributes, context: nil)
let lines = floor(CGFloat(labelSize.height) / bookTitleLabel.font.lineHeight)
return Int(lines)
}
You put in the string that will be displayed in the label and it gives you how many lines the label will have. I'm using dynamic type and the Headline style for this particular label, hence the preferredFontForTextStyle(UIFontTextStyleHeadline) part, but you can change that to the font and size your label uses.
Then I use (screenSize.width - 30) for my label's width because it's width is not fixed, so I'm using the screen size minus leading and trailing. This is probably not the most elegant solution, I'm open to better suggestions.
The rest is pretty straightforward.
After I have the number of lines I can do this:
func splittedString(text: String) -> String {
if numberOfLinesInLabel(text) == 2 {
var chars = Array(text.characters)
var i = chars.count / 2
var x = chars.count / 2
while chars[i] != " " && chars[x] != " " {
i--
x++
}
if chars[i] == " " {
chars.insert("\n", atIndex: i+1)
} else {
chars.insert("\n", atIndex: x+1)
}
return String(chars)
}
}
Instead of just avoiding orphans I decided to split the string in two at the breaking point nearest to its half, so that's what this last function does, but it wouldn't be hard to tweak it to suit your needs.
And there you have it!

Related

Getting string from label visible at runtime

I want to get the string which is visible at my label and having line break mode .byClipping .
My real string is :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants that are used in lots of classes all over the app then it makes sense to have a separate .h and .m file just for defining these constants."
but at run time i am getting :-
"This way your API interface will be nice and pretty, you’re not exposing the internals of your class to the world. If you have constants"
i want to get the below string in a variable at runtime.
I am not sure if this is natively doable. The closest idea I have is to test the string size and removing/adding characters. Check if the following works for you:
func findTextActuallyVisibleInLabel(_ label: UILabel) -> String? {
guard let originalText = label.text else { return nil }
var text = originalText
let labelSize = label.bounds
guard labelSize.height > 0 else { return text }
let bounds: CGRect = CGRect(x: 0.0, y: 0.0, width: labelSize.width, height: CGFloat.infinity)
while !text.isEmpty && label.textRect(forBounds: bounds, limitedToNumberOfLines: label.numberOfLines).height > labelSize.height {
text.removeLast()
label.text = text
}
label.text = originalText
return text
}
This seems to do its job in my case more or less. If I use wrapping that ends text with ... the result is incorrect.
Also some optimization may be nice; currently I just use the full string and start subtracting character by character. Maybe a bisection would be nicer here.
I do still use original label so it should take all possible parameters into account like wrapping, font size, lines... I modify the actual text on the label which is then reset to original.
Possibly view animations should be disabled while this operation is in progress.

getting consistent sizing for glyph backgrounds

Hey folks – I'm new to TextKit and trying to draw backgrounds & borders around specific attributes. I've gotten fairly close, but haven't yet found methods that don't generate very inconsistent sizing, which tends to look bad. Here's my first crack at it:
class MyLayout: NSLayoutManager {
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
guard let storage = textStorage else {
return
}
guard let context = UIGraphicsGetCurrentContext() else {
return
}
var codeBlockRect: CGRect? = nil
enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, container, subRange, stop) in
var effectiveRange = NSRange()
let attributes = storage.attributes(at: subRange.location, effectiveRange: &effectiveRange)
storage.enumerateAttribute(.inlineCodeBlock, in: subRange) { (value, attributeRange, stop) in
guard value != nil else {
return
}
var background = self.boundingRect(forGlyphRange: attributeRange, in: container)
background.origin.x += origin.x
background.origin.y += origin.y
context.setFillColor(UIColor.lightGrey.cgColor)
context.setStrokeColor(UIColor.mediumGrey.cgColor)
context.stroke(background)
context.fill(background)
}
}
}
}
That produces these results:
Single line of text:
Multiple lines of text:
As you can see, there's about 3 pixels of difference between the sizes there. I imagine it's because boundingRect, as the documentation says:
Returns the smallest bounding rect which completely encloses the glyphs in the given glyphRange
But I haven't found a method that gives me a number closer to what I'm looking for. My ideal scenario is that every rectangle will have the exact same height.
Let me know if any more information is needed.
Update
It crossed my mind that this could be related to the proprietary font we're using, so I changed everything to use UIFont.systemFont, which didn't make any difference.
I found a workaround here – instead of defining my inlineCodeBlock as a background, i defined it as a custom underline style.
Doing that let me override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:).
Once I had that, I was able to use baselineOffset to get a consistent positioning.

Swift: Display (LaTeX) math expressions inline

I would like to display math terms inside a text, in particular in an inline mode, i.e. inside a sentence.
Using LaTeX, this would for example look like:
"Given a right triangle having catheti of length \(a\) resp. \(b\) and a hypotenuse of length \(c\), we have
\[a^2 + b^2 = c^2.\]
This fact is known as the Pythagorean theorem."
Does anybody know how this can be achieved in Swift?
(I know that this example may be achieved in Swift without LaTeX-like tools. However, the expressions in my mind are in fact more complex than in this example, I do need the power of LaTeX.)
The optimal way would be a UITextView-like class which recognizes the math delimiters \(,\) resp. \[,\], recognizes LaTeX code inside these delimiters, and formats the text accordingly.
In the Khan Academy app, this problem seems to be solved as the screenshots in the Apple App Store/Google Play Store show inline (LaTeX) math.
I’ve found the package iosMath which provides a UILabel-like class MTMathUILabel. As this class can display solely formulas, this seems to be not good enough for my purpose, except if there was a method which takes a LaTeX source text such as in the example above, formats expressions such as \(a\) into tiny MTMathUILabels and sets these labels between the other text components. As I am new to Swift, I do not know whether and how this can be achieved. Moreover, this seems to be very difficult from a typographical point of view as there will surely occur difficulties with line breaks. And there might occur performance issues if there are a large number of such labels on the screen at the same time?
It is possible to achieve what I want using a WKWebView and MathJax or KaTeX, which is also a hack, of course. This leads to other difficulties, e.g. if one wants to set several of these WKWebViews on a screen, e.g. inside UITableViewCells.
Using iosMath, my solution on how to get a UILabel to have inline LaTeX is to include LATEX and ENDLATEX markers with no space. I replaced all ranges with an image of the MTMathUILabel, going from last range to first range so the positions don't get screwed up (This solution allows for multiple markers). The image returned from my function is flipped so i used .downMirrored orientation, and i sized it to fit my text, so you might need to fix the numbers a little for the flip scale of 2.5 and the y value for the attachment.bounds.
import UIKit
import iosMath
let question = UILabel()
let currentQuestion = "Given a right triangle having catheti of length LATEX(a)ENDLATEX resp. LATEX(b)ENDLATEX and a hypotenuse of length LATEX(c)ENDLATEX, we have LATEX[a^2 + b^2 = c^2]ENDLATEX. This fact is known as the Pythagorean theorem."
question.text = currentQuestion
if (question.text?.contains("LATEX"))! {
let tempString = question.text!
let tempMutableString = NSMutableAttributedString(string: tempString)
let pattern = NSRegularExpression.escapedPattern(for: "LATEX")
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let matches = regex?.matches(in: tempString, options: [], range: NSRange(location: 0, length: tempString.count)) {
var i = 0
while i < matches.count {
let range1 = matches.reversed()[i+1].range
let range2 = matches.reversed()[i].range
let finalDistance = range2.location - range1.location + 5
let finalRange = NSRange(location: range1.location, length: finalDistance)
let startIndex = String.Index(utf16Offset: range1.location + 5, in: tempString)
let endIndex = String.Index(utf16Offset: range2.location - 3, in: tempString)
let substring = String(tempString[startIndex..<endIndex])
var image = UIImage()
image = imageWithLabel(string: substring)
let flip = UIImage(cgImage: image.cgImage!, scale: 2.5, orientation: .downMirrored)
let attachment = NSTextAttachment()
attachment.image = flip
attachment.bounds = CGRect(x: 0, y: -flip.size.height/2 + 10, width: flip.size.width, height: flip.size.height)
let replacement = NSAttributedString(attachment: attachment)
tempMutableString.replaceCharacters(in: finalRange, with: replacement)
question.attributedText = tempMutableString
i += 2
}
}
}
func imageWithLabel(string: String) -> UIImage {
let label = MTMathUILabel()
label.latex = string
label.sizeToFit()
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
label.layer.render(in: UIGraphicsGetCurrentContext()!)
return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}

How to word wrap on UILabel that has infinite lines?

I have a UILabel in which the number of lines need to be set to 0 (infinite lines). But when it displays a single long word, it breaks the line by character, to form a second line. I tried to set Line breaking mode manually as such
cell.nameLbl.adjustsFontSizeToFitWidth = true
cell.nameLbl.lineBreakMode = .byWordWrapping
But the app still breaks by character. How can I fix this?
Edit- I want the word font size to shrink.
Adjusting font size is unfortunately done per label, not per line. Wrapping does not effect it. So the result you are seeing is expected. You could split your string manually into multiple labels maybe putting them into stack view to get desired result.
What I tried was:
private func layoutText(text: String, onStackView stackView: UIStackView) {
var words: [String] = text.components(separatedBy: .whitespaces)
var currentLineWords: [String] = [String]()
var currentLabel: UILabel!
func newLabel() -> UILabel {
let label = UILabel(frame: stackView.bounds)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
return label
}
while words.count > 0 {
if currentLineWords.count == 0 {
currentLabel = newLabel()
currentLineWords.append(words.removeFirst())
} else {
let newText = currentLineWords.joined(separator: " ") + " " + words[0]
currentLabel.text = newText
currentLabel.sizeToFit()
if currentLabel.bounds.width > stackView.bounds.width {
// Break line
currentLabel.text = currentLineWords.joined(separator: " ")
stackView.addArrangedSubview(currentLabel)
currentLabel = nil
currentLineWords = [String]()
} else {
// All good
currentLineWords.append(words.removeFirst())
}
}
}
if currentLineWords.count > 0 {
currentLabel.text = currentLineWords.joined(separator: " ")
stackView.addArrangedSubview(currentLabel)
}
}
This works pretty neatly but:
When word is too long to fit in a line even when shrunk it will show "..." at the end
Shrunk line still has the same height instead of having it reduced
We are only handling whitespaces and inserting back spaces
The first 2 would probably both be solved by having manual adjustsFontSizeToFitWidth. Basically keep reducing font size until it fits or until a certain size. If "certain size" scenario is hit then simply use multiline on a label (will break by characters).
The last one just need some extra effort.
I would still not implement this for multiple reasons one of them being that it looks ugly but is still interesting.
Custom font size adjustment:
To have custom font adjustment and all the logic there is only a change when a new lien is created:
if currentLineWords.count == 0 {
currentLabel = newLabel()
currentLineWords.append(words.removeFirst())
currentLabel.text = currentLineWords.joined(separator: " ")
currentLabel.sizeToFit()
if currentLabel.bounds.width > stackView.bounds.width {
currentLabel.adjustsFontSizeToFitWidth = false
let originalFontSize: CGFloat = currentLabel.font.pointSize
var currentFontSize: CGFloat = originalFontSize
let minimalFontSize: CGFloat = currentLabel.minimumScaleFactor > 0.0 ? currentLabel.font.pointSize * currentLabel.minimumScaleFactor : originalFontSize
while currentLabel.bounds.width > stackView.bounds.width {
currentFontSize -= 1.0
if currentFontSize < minimalFontSize {
currentLabel.numberOfLines = 0
currentLabel.font = UIFont(name: currentLabel.font.fontName, size: originalFontSize)
break // Could not shrink it even further. USe multiline
}
currentLabel.font = UIFont(name: currentLabel.font.fontName, size: currentFontSize)
currentLabel.sizeToFit()
}
// Break line
stackView.addArrangedSubview(currentLabel)
currentLabel = nil
currentLineWords = [String]()
}
}
This looks usable now. My result:

How to "list" text in columns in iOS?

Basically, I wish to list some text like so, in a column view:
Item Colour Quantity
Glasses Black 3
It's not for user-input, more towards the user reading it. I know this can be accomplished via the use of labels, however, this does tend to get tedious. I have the text written up in a word document, tried to copy and paste it into a UITextView and mess around with the attributes, I get 99% close however the last "column" seems to always give me an issue.
Can anyone potentially shed some light onto how I can do this with ease?
P.S- I will probably have 10 "lists" each different.
You have basically three good possibilities.
1) You give it a try using labels
2) You take a UICollectionView or
3) some UITableViews side by side
I had the same problem and I decided to go with option 1)
Create a method which needs a 2d String array as input.
Determine the ScreenSize of the device.
func createRows(textName: [[String]])
{
dispatch_async(dispatch_get_main_queue())
{
if self.columnArrayActive || self.columnArray1.isEmpty
{
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
self.ScrollViewMain.contentSize.height = CGFloat(30 * textName.count))
Now create two for loops to iterate through the array.
Create the labels and modify them.
for i1 in 0..<textName.count
{
var columnArray = [UILabel]()
for i2 in 0..<textName[i1].count
{
columnArray.append((UILabel(frame: CGRectMake(CGFloat((Int(screenWidth) / 9) * i2 - Int(screenWidth / 128)), (CGFloat(30 * i1) - modifyY), CGFloat(Int(screenWidth) / 8), 30))))
columnArray[i2].text = textName[i1][i2]
columnArray[i2].textAlignment = NSTextAlignment.Center
I have additionally structured the rows with two different colors for a better reading experience.
if i1 == 0
{
columnArray[i2].backgroundColor = UIColor(red: CGFloat(1), green: CGFloat(0.35), blue: CGFloat(0.35), alpha: CGFloat(1))
} else {
if self.rowSwitcher
{ columnArray[i2].backgroundColor = settings.vari.color1 as? UIColor}
else
{ columnArray[i2].backgroundColor = settings.vari.color2 as? UIColor}
}
self.scrollViewMain.addSubview(columnArray[i2])
}
if i1 != 0
{
if self.rowSwitcher
{ self.rowSwitcher = false }
else
{ self.rowSwitcher = true }
}
self.columnArray1.append(columnArray)
}
self.columnArrayActive = false
This part of the code is used when you already have created your list but you need to update the values of the labels.
} else {
for i3 in 0..<textName.count
{
for i4 in 0..<textName[i3].count
{
self.columnArray1[i3][i4].text = textName[i3][i4]
}
}
}
}
}
You need to define some global or class variables to get it work:
1)columnArray1 : [[UILabel]]
2)columnArrayActive : Bool
3)scrollViewMain : UIScrollView!
You create in the Interface Builder a UIScrollView filling you screen.
Then you create constraints.
Afterwards you create a reference to your class and in viewDidLoad you add:
scrollViewMain = self.delegate
This means that your class needs of course to inherit from UIScrollViewDelegate!
The result looks like this:
Don't forget you can draw text into a graphics context. For read-only text, this is definitely an option I would consider. Another option which might work for you is text containers in Text Kit.

Resources