SwiftUI Text scaledToFit and wrap text - ios

I have a button with dynamic text. I'm looking to scale the text down to fit it (as much as possible), and wrap the text to some number of lines (probably 2?).
By default, SwiftUI appears to wrap the text on words. But it seems like when I use the scaledToFit modifier on my Text view, it prevents the words from wrapping.
Here is some playground code to illustrate:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
VStack {
// Here is the default text - see in the screenshot that it properly wraps the text
HStack {
Text(lorem)
}
.frame(width: 100, height: 100)
.background(Color.blue)
// Here is text that is scaled down, but prevented from wrapping, even though
// we have allowed it to use 2 lines.
HStack {
Text(lorem)
.allowsTightening(true)
.scaledToFit()
.lineLimit(2)
.minimumScaleFactor(0.7)
}
.frame(width: 100, height: 100)
.background(Color.green)
// Here is the text with the same scaling modifiers as above, but it
// is still set to the original font size
HStack {
Text("Short")
.allowsTightening(true)
.scaledToFit()
.lineLimit(2)
.minimumScaleFactor(0.7)
}
.frame(width: 100, height: 100)
.background(Color.pink)
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
So, ideally, I'd like a combination of the first and second views - I would like the text to wrap, but also shrink to some minimum size if the text doesn't immediately fit in the parent view.

try this, and similarly for the pink:
HStack {
Text(lorem)
.frame(width: 100, height: 100) // <--- here
.allowsTightening(true)
.lineLimit(2)
.scaledToFit()
.minimumScaleFactor(0.7)
}
.frame(width: 100, height: 100)
.background(Color.green)

Related

SwiftUI ZStack alignment bug

Here is a simple code that produces a strange top alignment and shrunk text of the second element inside ZStack.
BUT if you change the second text a bit (replace text2 by text2Alt1 or by text2Alt2) making it longer or shorter everything becomes correct.
What is the reason for this behavior?
struct VCardView: View {
let text: String
let color: UIColor
var body: some View {
VStack {
Rectangle()
.fill(Color(self.color))
.frame(idealWidth: 800, idealHeight: 500)
.aspectRatio(contentMode: .fit)
Text(self.text)
}
}
}
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
VCardView(text: text1, color: .blue)
.frame(width: 180, height: nil)
.alignmentGuide(.leading) { _ in 180 }
.alignmentGuide(.top) { _ in 0 }
//Replace text2 by text2Alt1 or text2Alt2 here:
VCardView(text: text2, color: .green)
.frame(width: 180, height: nil)
.alignmentGuide(.leading) { _ in 0 }
.alignmentGuide(.top) { _ in 0 }
}
}
let text1 = "1Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidu!"
let text2 = "2Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis nat!"
let text2Alt1 = "2Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. C"
let text2Alt2 = "2Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis nat! Cum sociis nat!"
}
Also, you could replace Rectangle() by Image() with appropriate proportions because it was the initial state. Rectangle with the ideal size is just for demonstration.
Here I use ZStack with explicit alignment guides (not HStack) because it's a part of another library. It is essential.
XCode 11.5
iOS 13.5 iPhone SE 2020
Bug looks like this:
Expected layout:
I've seen such Text truncation problems often in SwiftUI. The workaround which most of the time helps: ensure that Text is allowed to adjust it's font size automatically using the minimumScaleFactor modifier.
By modifying VCardView
Text(self.text)
.minimumScaleFactor(0.5)
The misalignment problem disappears for all possible text values (text1, text2, text2Alt1 and text2Alt2).
The resulting text does not look scaled at all. Everything fits nicely.
I do not have a good explanation, but I think if you tell SwiftUI that your Text is not 100% rigid/stiff, it performs better in calculating the elements extents.
Perhaps it is a SwiftUI bug, but I was always OK with this minimumScaleFactor workaround as it hadn't negatively impacted my results.
it doesn't look like a ZStack problem. Try to remove .aspectRatio(contentMode: .fit) from Rectangle() in VCardView.
For rectangle proportion check something like this.

Splitting a text into chunks smaller than a specific size, at a newline, in F#

Let's say I have some text:
Lorem ipsum dolor sit amet, consectetur adipiscing elit,\n
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris\n
nisi ut aliquip ex ea commodo consequat.\n
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore\n
eu fugiat nulla pariatur.\n
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia\n
deserunt mollit anim id est laborum.\n
What is the most efficient way to cut it into chunks of x bytes, where the cut can only happen at the carriage return?
Two methods come to mind:
split the text into lines, add lines to a buffer until the buffer is full, roll back the last line that caused the overflow, and repeat.
find the offset in the text at the buffer length and walk back to the previous carriage return, with proper handling of the beginning and ending of the text
I couldn't find a solution online, but I can't believe that this problem hasn't already been solved many times, and there may be a common implementation of this.
Edit:
more information about my use case:
The code is for a Telegram bot which is used as a communication tool with an internal system.
Telegram allows up to 4kb per message and throttles the number of calls.
Right now I collect all messages, put them in a concurrent queue and then a tasks flushes the queue every second.
Messages can be a single line, can be a collection of lines and can sometimes be larger than 4kb.
I take all the messages (some being multiple lines in one block), aggregate them into a single string, then split the string by carriage return and then I can compose blocks of up to 4kb.
One additional problem I haven't tackled yet, but that's for later, is that Telegram will reject incomplete markup, so I will also need to cut the text based on that at some point.
Not very efficient, and also laboring under the assumptions
that you may want to preserve the newline separators, and
that we can assume that the end of the string is equivalent
to a single newline;
then, an implementation along the lines of your first approach is both functional and straightforward. Just split into lines and combine them unless their combined length exceeds the threshold.
// Comma-separated output of the string lengths
// (plus 1 to compensate for the absence of the EOL)
let printLengths =
Array.map (String.length >> (+) 1 >> string)
>> String.concat ", "
>> printfn "%s"
let text =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
"
text.Split '\n' |> printLengths
// prints 57, 67, 67, 41, 77, 26, 74, 37, 1, 1
let foo n (text : string) =
(text.Split '\n', [])
||> Array.foldBack (fun t -> function
| x::xs when String.length x + t.Length + 1 < n -> x+"\n"+t::xs
| xs -> t::xs )
text |> foo 108 |> List.toArray |> printLengths
// prints 57, 67, 108, 77, 100, 39
Most common stream related tasks are already implemented very efficiently in the BCL.
It's probably a good idea to stick with tried-and-tested Stream classes.
let lipsum =
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
"""
use stream = new MemoryStream(Encoding.UTF8.GetBytes(lipsum))
use reader = new StreamReader(stream)
let readBlock blockSize =
let writer = new StringBuilder(capacity = blockSize)
let rec readNextline () =
if (not reader.EndOfStream) then do
let line = reader.ReadLine()
if writer.Capacity < line.Length + writer.Length then do
stream.Seek(int64 -line.Length, SeekOrigin.Current) |> ignore
else
writer.AppendLine(line) |> ignore
readNextline ()
readNextline ()
writer.ToString()
readBlock 300 |> printfn "%s"
You can just flush the queue, writing to the same MemoryStream. And call readBlock to keep getting new blocks of at-most your specified size.

How to update progressView when downloading something in realm

I am trying to figure out how to use a progressView to show the progress of a something being downloaded using Realm. Through reading it seems that a Float called progress needs to be found, but I don't know how to find that in Realm. I have looked at the following questions, but I don't think that they are too helpful in this situation:
Swift: Realm - Update UI (Progress) while adding Data to DB
iOS-Swift How to update progressView
This is what I have at the moment:
ViewController:
import UIKit
import Realm
import RealmSwift
class ViewController: UIViewController {
var data = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
var realm: Realm!
#IBOutlet weak var saveBtn: UIButton!
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
print("Hello")
print(Realm.Configuration.defaultConfiguration.fileURL!)
realm = try! Realm()
}
#IBAction func saveBtnPressed(_ sender: Any) {
saveToRealm(id: 1, name: "One", data: data)
let progress: Float = 0
progressView.setProgress(progress, animated: true)
}
func saveToRealm(id: Int, name: String, data: String) {
let realmSave = RealmSave()
realmSave.id = id
realmSave.name = name
realmSave.data = data
try? realm!.write {
realm.add(realmSave, update: true)
}
}
}
If there is anything that I can help with, please ask. Thank you
First: defining a let constant with a value of 0 will always stay 0. You can't dynamically change that to reflect the state of your download.
As a Realm engineer pointed out in this post "Realm has no way to know the total amount of data."
Estimating the progress should be done in your code.
You could try something like this:
func estimateProgress() {
let dataString = "Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda."
let data = dataString.data(using: .utf8)
let count = Float((data?.count)!)
let percentage: Float = count / 100
var progress: Float = 0
if progress < count {
progress += percentage
} else {
progress = count
}
progressView.setProgress(progress, animated: true)
print(progress)
}
Please note that this solution is for updating the UI and letting the user know that the saving to Realm is in progress. Since it uses the amount of bytes in the data, the larger the data, the longer it will take for the progress bar to fill. So it is good for UI, but it is not the actual state of progress.
And if you feel the progress bar is filling to slow/fast you can always tweak the percentage.

How to remove orphans and widow in multi-line UILabel having justified text alignment?

I am using UILabel to display text. Sometimes, it creates orphan and widow problem in the copy of text, the alignment of the text is set to 'justified', also the hyphenation factor is set to 1.0.
How can I solve the widow & orphan problem?
I want justified text with hyphens for word-wrap and want to remove orphan and widow.
I had set attributed text to label and the size of label is set depending on the device width.
Here is the code for attributed text:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = NSTextAlignment.Justified
paragraphStyle.hyphenationFactor = 1.0;
let strTwo = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
let attrlblTwo = NSAttributedString(string: strTwo,
attributes: [
NSParagraphStyleAttributeName: paragraphStyle,
NSBaselineOffsetAttributeName: NSNumber(float: 0),NSKernAttributeName: 0, NSFontAttributeName : lblTwo.font
])
self.lblTwo.attributedText = attrlblTwo
To get rid of orphans, replace the last space in the text with a non-breaking space. The non-breaking space character is U+00A0. You can find it by selecting "Emoji & Symbols" from the edit menu, click on the widget in the upper right to expand the Character Viewer to full size and then type space in the search field. You'll see a few "missing" characters, these are the space characters. Click on them until you find the "No-Break Space", then double click it to insert it into your code. Character Viewer with Non Breaking Space highlighted
Have you ever tried doing this using storyboards? I've experienced this from a project before, having a UILabel on storyboard, setting Text to Atrributed and setting hyphenation to 1 works for me. Did test on different paragraph lengths and iOS devices.

Highlight part of text in UILabel

in my application I have a UILabel with a lot of text inside. When the user performs a research I want to highlight the background under the text searched by the user.
Here an example from "Preview" in MacOSX.
The user searches silence and this word is highlighted in the text.
Any ideas ?
Thanks.
You'll need to enable the 'attributed' text of the UILabel (or UITextView/custom view). Then you'll need to find/make a nice/fast algorithm to change the color (bg/text) color of some parts of the text. You should be able to find quite a lot on 'attributed string' algorithms to mark some words/matches.
Please check also http://iphonedevelopment.blogspot.be/2011/03/attributed-strings-in-ios.html
In case of, it can maybe be easier/quicker to use a webview by making a custom HTML with the matched-words in another color/bg-color. A webview can almost look like a normal label, and offers even the option to use links/images/..., which can improve the user-experiences in some cases
Maybe this is an old question. But I think this will help to someone.
To highlight a part of text in UILabel, I have written an open source named HighlightLabel.
You can simply specify the hightlightText and highlightColor to highlight it.
_highlightLabel.text = #"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.";
_highlightLabel.highlightText = #"Lorem ipsum dolor sit er elit lamet";
_highlightLabel.highlightColor = [UIColor yellowColor];
You'll have to use NSAttributedString for this.
This might also help. It is a subclass of UILabel that draws an NSAttributedString and also provides convenience methods for setting the attributes of an NSAttributedString from UIKit classes.
Once desired letters/Characters are found, highlight that part of UIView or anything you are using there.

Resources