The text doesn't get wrapped in swift UI - ios

Even After setting the .lineLimit(nil) the text doesn't get wrapped.
var body: some View {
VStack(alignment: .center) {
Text("SwiftUI is a modern way to declare user interfaces for any Apple platform. ")
.font(.title)
.color(.red)
.lineLimit(nil)
Text("Create beautiful, dynamic apps faster than ever before.")
.font(.system(size: 20))
.lineLimit(nil)
}.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}

After spending a lot of time with an error like this, I can't be 100% certain that this is a lineLimit issue. As of writing this post, the solution I found with more complex views is the following snippet of code to prevent wrapping:
.fixedSize(horizontal: false, vertical: true)
This should prevent the interpreter from collapsing text vertically.

The full solution as of Xcode 11.3 / Swift 5.1 is found across multiple answers here.
The explanation for why this is happening is found in Matteo Pacini's answer: using the predefined .font(.title), .font(.headline), etc. seems to bring the behavior that these Text views will size themselves to always ellipsize rather than wrap. However, simply switching to .body doesn't seem like the best work around.
The best workaround is found in Sharpienero's answer: add .fixedSize(horizontal: false, vertical: true) to your Text view. This tells the Text view to NOT do its custom re-sizing horizontal logic of NOT ellipsizing which causes it to follow the standard rules that we're all use to.
Thanks to both of them!

Try changing the second Text's lineLimit to a number instead of nil:
VStack(alignment: .leading) {
Text("SwiftUI is a modern way to declare user interfaces for any Apple platform. ")
.font(.title)
.color(.red)
.lineLimit(nil)
Text("Create beautiful, dynamic apps faster than ever before.")
.font(.system(size: 20))
.lineLimit(2)
}.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
Result:

It looks like the font holds line wrapping attributes.
If you change it to body, then it wraps correctly!

For my problem, I had a setup like this:
public var body: some View {
Form {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.font(.title)
Spacer()
.fixedSize()
Text("""
Integer ut orci odio. Proin cursus ut elit eget rutrum. Nunc ante sem, euismod sed purus sed, tempus elementum elit. Phasellus lobortis at arcu quis porta. Cras accumsan leo eu tempus molestie. Suspendisse vulputate diam ipsum, et tristique lorem porta et. Pellentesque sodales est id arcu luctus venenatis.
Vestibulum non magna lorem. In tincidunt aliquet nunc, sit amet pharetra neque hendrerit id.
Cras sed!
""")
NativeButton("OK", keyEquivalent: .return) { self.screen = .game }
}
.frame(maxWidth: 480)
.fixedSize()
.padding()
}
For some reason, all I had to do was add minWidth: 480, idealWidth: 480 to the frame and everything rendered correctly. I didn't expect this because I already applied .fixedSize(), so I figured one of these three should've been enough.
public var body: some View {
Form {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.font(.title)
Spacer()
.fixedSize()
Text("""
Integer ut orci odio. Proin cursus ut elit eget rutrum. Nunc ante sem, euismod sed purus sed, tempus elementum elit. Phasellus lobortis at arcu quis porta. Cras accumsan leo eu tempus molestie. Suspendisse vulputate diam ipsum, et tristique lorem porta et. Pellentesque sodales est id arcu luctus venenatis.
Vestibulum non magna lorem. In tincidunt aliquet nunc, sit amet pharetra neque hendrerit id.
Cras sed!
""")
NativeButton("OK", keyEquivalent: .return) { self.screen = .game }
}
.frame(minWidth: 480, idealWidth: 480, maxWidth: 480)
.fixedSize()
.padding()
}

SwiftUI
.lineLimit(nil) VS .lineLimit(any number)
VStack(alignment: .leading, spacing: 16.0) {
// Sets the maximum number of lines that text can occupy in the view.
Text("SwiftUI is a user interface toolkit that lets us design apps in a declarative way. ")
.font(.title)
.lineLimit(3)
// But if you don't know about the the text size then Sets nil in the lineLimit.
Text("SwiftUI is a user interface toolkit that lets us design apps in a declarative way. That's a fancy way of saying that we tell SwiftUI how we want our UI to look and work, and it figures out how to make that happen as the user interacts with it.... ")
.font(.body)
.lineLimit(nil)
}.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8))

I had a situation involving a UIViewRepresentable wrapped UITextField which was presented as:
VStack {
Text("Long enough to wrap....")
Spacer().frame(height: 40)
CustomTextField()
}
The Text stopped wrapping when I added the CustomTextField. Nothing I did to the Text helped. If I removed the Spacer() it wrapped just fine!!
I wound up removing the Spacer and adding a bottom padding to the Text. As far as I can tell, my CustomTextField is just fine and I cannot see why it would affect the SwiftUI layout algorithms.

The only answer that kinda worked for me was applying .fixedSize(horizontal: false, vertical: true) on the Text.
I say "kinda" because, by using .fixedSize, SwiftUI will completely ignore the size given by the parent and overflow it. If the parent has a background, the layout is ruined. If the parent has a sibling node, the text is rendered on top of it.
I really believe Apple should give us the option to have the ideal size of a Text be its multiline version instead of the one-line counterpart.
While this doesn't happen, I'm using the following monstrosity, but it works:
struct AdaptiveText: View {
#State private var height: CGFloat?
var text: String
private func updateHeight(height: CGFloat) -> some View {
DispatchQueue.main.async {
self.height = height
}
return Color.clear
}
var body: some View {
ZStack {
Text(text)
.fixedSize(horizontal: false, vertical: true)
.background() {
GeometryReader { geo in
updateHeight(height: geo.size.height)
}
}
.opacity(0)
Text(text)
}.frame(width: nil, height: height)
}
}

I just added
.padding()
to my Text instance and it worked

Related

How to get rid of vertical text padding in SwiftUI?

I want to make a rectangle with text inside. the rectangle should be pinned to the right and left side of the display (or superview). It's height should be determined by the text.
I tried the following code:
struct DescriptionView: View {
var description =
"""
02.11.2021
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Illud urgueam, non intellegere eum quid sibi dicendum sit, cum dolorem summum malum esse dixerit. Omnis enim est natura diligens sui. Quamquam haec quidem praeposita recte et reiecta dicere licebit. Duo Reges: constructio interrete. Idem iste, inquam, de voluptate quid sentit? Ergo instituto veterum, quo etiam Stoici utuntur, hinc capiamus exordium. Tum ille: Tu autem cum ipse tantum librorum habeas, quos hic tandem requiris? Bona autem corporis huic sunt, quod posterius posui, similiora..
Bild © Lorem Lipsum
"""
var body: some View {
ZStack(alignment: .bottom) {
Color.red
.edgesIgnoringSafeArea(.all)
Text(description)
.foregroundColor(.white)
.font(.headline)
.background(
Rectangle()
)
.edgesIgnoringSafeArea(.horizontal)
}
}
}
This results looks like this:
As you can see, the text has some padding on the left and right side. How can I get rid of this? The rectangle should always be as wide as possible, while the text determines the height of the rectangle.
Update:
I am using Xcode Version 13.1 (13A1030d).
When I embed DescriptionView() in A TabView, the padding goes away in Xcode Preview. However, when I launch the app in the simulator, the padding appears again.
struct ContentView: View {
var body: some View {
ZStack {
Color.black
.edgesIgnoringSafeArea(.all)
TabView {
DescriptionView()
DescriptionView()
DescriptionView()
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .interactive))
}
}
}
Xcode Preview:
Simulator:
Update 2:
As one of the answers suggested, I tried the geometry reader. I got rid of the padding, but the alignment is now wrong. Also I don't think it should be that complicated (with a geometry reader and a stack):
var body: some View {
ZStack(alignment: .bottom) {
Color.red
.edgesIgnoringSafeArea(.all)
GeometryReader { geometry in
VStack() {
Text(description)
.foregroundColor(.white)
.font(.headline)
.frame(maxWidth: .infinity)
.background(
Color.black
)
}
}
}
}
Your Text view is calculating its size using the max width (in this case screen width) and the result is a width that is smaller than the screen width because there isn't a line of text that fits the screen perfectly.
If you want the Text view to expand completely you should use the frame modifier:
var body: some View {
ZStack(alignment: .bottom) {
Color.red
.edgesIgnoringSafeArea(.all)
Text(description)
.foregroundColor(.white)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
Rectangle()
)
}
}
I don't know this is right for your needs, but this way can be answer.
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack() {
Text("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to mak")
.font(.title)
.background(Color.blue)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Adding unlimited lines in a Text (SwiftUI)

Just figuring out how I can achieve multiple lines of text in a Text. It seems like the Text has the same default as UILabel (one line), but I can't find any function which meets this criteria.
struct ContentView : View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Avocado Toast").font(.system(size: 24))
}
// This Text does cut, and I wonder how I can achieve multiple rows
Text("Ingredients: Avocado, Almond Butter, Bread")
.font(.system(size: 20))
}
}
}
Edit
.lineLimit(X), did the trick. But is it possible to not set a specific amount, for instance. With just a 0?
For wrapping Text in a Form .lineLimit(Int.max) did not work for me. It seems there needs to be some width for it to know when to wrap. I believe the official way is with .fixedSize:
Text(message)
.fixedSize(horizontal: false, vertical: true)
The documentation states:
This example shows the effect of fixedSize(horizontal:vertical:) on a text view that is wider than its parent, preserving the ideal, untruncated width of the text view.
https://developer.apple.com/documentation/swiftui/view/fixedsize(horizontal:vertical:)
Use .lineLimit() to limit the amount of lines of text. It takes an optional Int (Int?) as an argument, and .lineLimit(nil) allows unlimited lines.
Edit: As of SwiftUI Beta 5, Text has a default line limit of nil, so text in Text will wrap by default.
My text kept truncating even with no line limit applied. Wrapping my content in a ScrollView {} solved it.
See more info here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-horizontal-and-vertical-scrolling-using-scrollview
Text("Professional Burnout and how to overcome it")
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.frame(width: 220)
I had to add 2 different modifiers for it to work:
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam pharetra neque ex, at consectetur leo semper eu. Ut finibus maximus ex et maximus. Proin vel sagittis sapien, sed tincidunt orci.")
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
If you don't add the frame, it will have your text out of the bounds of the screen, and fixedSize allows it to have multiple lines visible
lineLimit and fixedSize work together.
Setting .fixedSize with (horizontal: false, vertical: true) will allow the Text to grow.
Setting lineLimit determines up to how many lines the Text will grow to
a nil value is no limit
lineLimit(nil) doesn't seem to be necessary in conjunction with .fixedSize as .fixedSize alone will allow the label to grow as far as it can.
Example:
Text("Using `___VARIABLE_\(identifier): identifier___` in your template will replace that macro with `\(defaultValue)`")
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
lineLimit(1)
If lineLimit(nil) is not working for you, try setting layoutPriority manually to whatever it suits you .layoutPriority(0.5)
None of the previous answers worked for me, but .lineLimit(Int.max) did the trick.

How to scroll an ExpansionTile / Listview when a tile is expanded?

We have a bunch of ExpansionTile within a ListView (so far not too crazy, right?). Our users would like us to "auto scroll" the list when they open up an ExpansionTile so the new expanded tile is now at the top (or as top as it can be) of the screen so they don't have to manually scroll to see what we just opened.
I think we need something like a ScrollController on the ListView -- and to register an onExpansionChanged .. to do .. something. It's the something that's more than a bit vague to me. :)
Any pointers or help on how to do this would be greatly appreciated!
TIA!
If I got right what your asking for, is basically a way to scroll automatically your ExpansionTile up so the user doesn't have to do it manually to see its content, right?
Then, you can actually use a ScrollController in your ListView and take advantage of the onExpansionChanged callback of the ExpansionTile to scroll it up/down accordingly to the size of the tile vs. its current position.
But now you are wondering: how do I know how much to scroll?
For that, either you know previously how exactly is the height of your widget when constructing it by giving it some constraints (like building a Container(heigh: 100.0)) or, in this particular scenario, you don't know exactly how many pixels the ExpansionTile height will take. So, we can use a GlobalKey and then check its rendering height by checking its RenderBox at runtime to know exactly how many pixels it's taking when collapsed and multiply it by its index.
ExpansionTile _buildExpansionTile(int index) {
final GlobalKey expansionTileKey = GlobalKey();
double previousOffset;
return ExpansionTile(
key: expansionTileKey,
onExpansionChanged: (isExpanded) {
if (isExpanded) previousOffset = _scrollController.offset;
_scrollToSelectedContent(isExpanded, previousOffset, index, expansionTileKey);
},
title: Text('My expansion tile $index'),
children: _buildExpansionTileChildren(),
);
}
Ok, now we have a ListView that will use this _buildExpansionTile in its generator with a GlobalKey. We are also saving the scroll offset of the ListView when we expand the tile. But, how do we actually scroll up to show its content? Let's see the _scrollToSelectedContent method.
void _scrollToSelectedContent(bool isExpanded, double previousOffset, int index, GlobalKey myKey) {
final keyContext = myKey.currentContext;
if (keyContext != null) {
// make sure that your widget is visible
final box = keyContext.findRenderObject() as RenderBox;
_scrollController.animateTo(isExpanded ? (box.size.height * index) : previousOffset,
duration: Duration(milliseconds: 500), curve: Curves.linear);
}
}
So, we are accessing the render object by its key and then use the ListView scroll controller to scroll up when expanded and down back to its initial position when collapsed by multiplying its height with its index. You don't have to scroll back, it was just my personal preference for this example. This will result in something like this:
And here you have the full example in a StatefulWidget
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final ScrollController _scrollController = ScrollController();
void _scrollToSelectedContent(bool isExpanded, double previousOffset, int index, GlobalKey myKey) {
final keyContext = myKey.currentContext;
if (keyContext != null) {
// make sure that your widget is visible
final box = keyContext.findRenderObject() as RenderBox;
_scrollController.animateTo(isExpanded ? (box.size.height * index) : previousOffset,
duration: Duration(milliseconds: 500), curve: Curves.linear);
}
}
List<Widget> _buildExpansionTileChildren() => [
FlutterLogo(
size: 50.0,
),
Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse vulputate arcu interdum lacus pulvinar aliquam. Donec ut nunc eleifend, volutpat tellus vel, volutpat libero. Vestibulum et eros lorem. Nam ut lacus sagittis, varius risus faucibus, lobortis arcu. Nullam tempor vehicula nibh et ornare. Etiam interdum tellus ut metus faucibus semper. Aliquam quis ullamcorper urna, non semper purus. Mauris luctus quam enim, ut ornare magna vestibulum vel. Donec consectetur, quam a mattis tincidunt, augue nisi bibendum est, quis viverra risus odio ac ligula. Nullam vitae urna malesuada magna imperdiet faucibus non et nunc. Integer magna nisi, dictum a tempus in, bibendum quis nisi. Aliquam imperdiet metus id metus rutrum scelerisque. Morbi at nisi nec risus accumsan tempus. Curabitur non sem sit amet tellus eleifend tincidunt. Pellentesque sed lacus orci.',
textAlign: TextAlign.justify,
),
];
ExpansionTile _buildExpansionTile(int index) {
final GlobalKey expansionTileKey = GlobalKey();
double previousOffset;
return ExpansionTile(
key: expansionTileKey,
onExpansionChanged: (isExpanded) {
if (isExpanded) previousOffset = _scrollController.offset;
_scrollToSelectedContent(isExpanded, previousOffset, index, expansionTileKey);
},
title: Text('My expansion tile $index'),
children: _buildExpansionTileChildren(),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('MyScrollView'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 100,
itemBuilder: (BuildContext context, int index) => _buildExpansionTile(index),
),
);
}
}
Based on this answer you can calculate how much is needed to scroll by getting the current scroll offset of ListView from its controller:
_scrollController.offset
and add it to the current ExpansionTile Y position given from its key:
RenderBox _box = _ExpansiontileKey.currentContext.findRenderObject();
yPosition = _box.localToGlobal(Offset.zero).dy;
This position considers the status bar and AppBar heights, so the scroll point can be calculated by:
scrollPoint = _scrollController.offset + yPosition - MediaQueryData.fromWindow(window).padding.top - 56;
MediaQueryData.fromWindow(window).padding.top will give you the statusbar height and from constats.dart you can get the AppBar height:
/// The height of the toolbar component of the [AppBar].
const double kToolbarHeight = 56.0;
You can avoid miscalculations of the last items by checking if the scrollPoint is below the maximum extent of the scroll:
if(_scrollPoint <= _scrollController.position.maxScrollExtent)
_scrollController.animateTo(_scrollPoint, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
else
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn
);
Notice that ExpansionTiles own expansion animation takes 200ms like shown in its definition:
const Duration _kExpand = Duration(milliseconds: 200); therefore it is a good idea to wait for expansion animation to finish before starting scrolling so you can get the updated values.
One approach is to delay the the call using Future.delayed(duration):
Future.delayed(Duration(milliseconds: 250)).then((value){
RenderBox _box = _ExpansiontileKey.currentContext.findRenderObject();
yPosition = _box.localToGlobal(Offset.zero).dy;
scrollPoint = _scrollController.offset + yPosition -
MediaQueryData.fromWindow(window).padding.top - 56;
if(_scrollPoint <= _scrollController.position.maxScrollExtent)
_scrollController.animateTo(_scrollPoint, duration: Duration(milliseconds:
500), curve: Curves.fastOutSlowIn);
else
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
});
Other than that you can make your own ExpansionTile and give it whatever duration you want for expanding.
If you are still facing this issue, I found another solution using the Scrollable Widget. We have to pass the current context fo the ExpansionTile to the ensureVisible method of the Scrollable class and it'll do the work. You can check my article How to scroll an ExpansionTile when it is expanded? for more info.

boundingRectWithSize returning a wrong height value for a multiline label

I am facing a strange problem with boundingRectWithSize and a multiline UILabel using the Swift language. I am currently working in a Xcode playground so I can see the result and updates of UI in real-time.
What I am trying to do is to calculate a given UILabel height dynamically based on it's content, I know this has been discusses over an over in other StackOverflow questions but this seems too strange to me.
The multi line label has this text in NSAttributedString form. Text is a simple Lorem ipsum placeholder:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor.
Here's the code for the UILabel:
let valueString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor."
var dataValue = UILabel()
dataValue.numberOfLines = 0
dataValue.lineBreakMode = .ByWordWrapping
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .Justified
paragraphStyle.lineBreakMode = .ByWordWrapping
paragraphStyle.firstLineHeadIndent = 0.1
let attributedText = NSAttributedString.init(
string:valueString,
attributes:[NSParagraphStyleAttributeName:paragraphStyle, NSFontAttributeName:dataValue.font])
let textString = dataValue.text! as NSString
dataValue.attributedText = attributedText
dataValue.frame = CGRect(
x:CGFloat(kMargin),
y:dataType.frame.origin.y + CGFloat(kLabelHeight),
width:CGFloat((maxLabelWidth) - kMargin * 2),
height:textString.boundingRectWithSize(
CGSizeMake(CGFloat(kMaxWidth), CGFloat.max),
options:[.UsesLineFragmentOrigin, .UsesFontLeading],
attributes:[NSParagraphStyleAttributeName:paragraphStyle, NSFontAttributeName:dataValue.font],
context:nil).size.height)
The problem is that the UILabel only shows 3 lines of text, stopping at the mauris word of the text and truncating the last ones as you can see from the Playground preview feature:
One obvious problem is that valueString and textString are two different strings. So there is no reason to believe that string you're putting into the label is the same as the string you're measuring.
You use valueString here:
let attributedText = NSAttributedString.init(
string:valueString, // ...
But you use dataString here:
height:textString.boundingRectWithSize(
Also, do not try to combine label attributes with attributed text. This can cause all kinds of problems. The dataValue label should have no font. You should not be fetching this font value. Do everything using the attributed string. You should not be constructing two different style dictionaries; you should be assigning and measuring the exact same attributed string.
To put it another way, do not measure valueString or textString at all, with or without attributes. Measure attributedString.
Finally, make sure that the label has the same width as you are using during measurement. You are saying
CGSizeMake(CGFloat(kMaxWidth) // ...
but there is no evidence that the label itself is going to have this width.

Override first letter capitalization of sentences for UITextview

I would like to make lowercase every first string of a sentence, that user writes into a UITextView.
Example
lorem ipsum dolor sit amet. consectetur adipiscing elit. phasellus et
tincidunt eros at faucibus orci.
There are a lot of related question for UITextfield, but I couldn't find any useful answers that deals with UITextview. Is it possible with a UITextView property or I need to check every string and make them lowercase? I've tried to set the keyboard in Interface Builder without any success.
Check this
textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
there are following you can use -
typedef enum : NSInteger {
UITextAutocapitalizationTypeNone ,
UITextAutocapitalizationTypeWords ,
UITextAutocapitalizationTypeSentences ,
UITextAutocapitalizationTypeAllCharacters ,
} UITextAutocapitalizationType;
Default is The default value for this property is UITextAutocapitalizationTypeSentences.

Resources