How can justify text in Swift UI? - user-interface

I am new to Swift UI. I have been using storyboard all this while. I am stuck at a place in swiftUI. Could one of you please explain how to justify Text in swiftUI? I referred to few links but they were of not much help.
SwiftUI: Justify Text
Kindly help! I am stuck at this very badly

An important thing to remember about swift is that views are typically arranged in stacks, mostly V and H stacks. You can use those stacks to move text around as you want. Now a common issue you might run into would be a stack that is not as wide as you'd expect, which you can fix with modifiers such as minimumFontScale and others. For the sake of this explanation I'll show you common structures for aligning text as you want.
Horizontal Alignment, Center of View
VStack {
Spacer() // Disable for Top Left/Right
HStack {
Spacer() //Disable for Left Align
Text("Test")
Spacer() //Disable for Right Align
}
Spacer() // Disable for Bottom Left/Right
}
Notice the VStack has two spacers to center the nested HStack. Then the HStack has spacers to align to the left of the view or the right.
Vertical Alignment, Center of View
HStack {
Spacer() //Disable for Left, Top/Bottom
VStack {
Spacer() //Disable for Top Align
Text("Test")
Spacer() //Disable for Bottom Align
}
Spacer() //Disable for Right, Top/Bottom
}
This one places the text in the middle and allows you to move it up and down. You should be able to look at these structures and see how you can manipulate it to move your views around.
Other Ways of Manipulating Alignment
VStack(alignment: .leading) {} //Leading and Trailing alignments.
HStack(alignment: .top) {} //Top and Bottom alignments.
//A Modifier directly on the view, where you could get the width
//and height from anything you want. Alignments could be.
// .leading, .trailing, .bottom, .top, .topleading, .topTrailing
// .bottomLeading, .bottomTrailing
Text("YourText").frame(width: 300, height: 300, alignment: .topLeading)
Using these types of modifiers can set your views up so that they can be cleaner than the above solutions, and become particularly useful when dealing with complex UI's that might have groups/sections. Nested stacks can have their modifiers in ADDITION to their parent stacks modifiers. The best way to find a solution for you might be different for someone else's UI. Play around with all the different ways of modifying alignment. It will come naturally over time, I'd say in a week or so.
TIP
Text views only take up the required amount of space in a stack. Stacks will only be as WIDE or as HIGH as their largest element on that axis. For example a Text("A") and Text("ABCDEFG") inside of a VStack will have the width of the second one, and a height of both. When working with views you should keep that in mind. There are ways to set stacks to different widths/heights using GeometryReader and .frame(...) modifier. I'll omit those for this answer.

Related

Bad performance of large SwiftUI lists on macOS

I have a SwiftUI app which displays large lists of 1000 to 5000 items.
I noticed that on macOS displaying such a long list has very bad performance. It takes several seconds for SwiftUI to render the list. This is independent of the complexity of the row views. Even if the rows are only Text() views.
On iOS, however, the same list would render almost instantaneously.
My code for the list view looks like this:
struct WordList: View {
#EnvironmentObject var store: Store
#State var selectedWord: RankedWord? = nil
var body: some View {
List(selection: $selectedWord) {
ForEach(store.words) { word in
HStack {
Text("\(word.rank)")
Text(word.word)
}
.tag(word)
}
}
}
}
Does anybody know some tricks to speed this up? Or is this a general problem on macOS 12 and we need to hope Apple improves this in the next major OS update?
I have also created a very simple sample app to test and demonstrate the list performance and of which the code above is taken from. You can browse / download it on GitHub
Update for Ventura
List performance on Ventura has significantly improved over Monterey. So no additional optimization might be necessary.
If you want to stay with List because you need all this nice features like selection, reordering, easy drag & drop... you will have to help SwiftUI estimate the total height of your list by having a fixed size for your rows. (I think this is the same in UIKit where performance will significantly improve if you are able to estimate the row height for each entry.)
So in your example, modify your row code as follows:
HStack {
Text("\(word.rank)")
Text(word.word)
}
.frame(width: 500, height: 15, alignment: .leading)
.tag(word)
I know it is an ugly solution because it doesn't dynamically adjust to the font size but it reduces the rendering time on my M1 Max based Mac from 2s down to 0.3s for a list of 10,000 words.
List seems to be not lazy on macOS. But you can use Table which is lazy, and supports single or multiple selection:
struct WordList_mac: View {
#EnvironmentObject var store: Store
#State var selectedWord: RankedWord.ID? = nil
var body: some View {
Table(store.words, selection: $selectedWord) {
TableColumn("Rank") { Text("\($0.rank)") }
TableColumn("Word", value: \.word)
}
}
}
What you wrote is fully generic code with say 5,000 user interface elements. A good old UITableView will handle this easily - it is one UI element instead of 5,000, and it creates reusable cells just for rows of the table that are visible on the screen (say 30 on an iPad, instead of 5,000.

SwiftUI align SF Symbol as text with a text

I need to display a SF Symbol (image) and a text in a view (as a title). As the text could be quite long, I want that it passes under the image for second line.
Expected result (almost!):
I first tried a HStack with image and text. But text doesn't go under image: Second line starts with a left 'padding' (corresponding to left image space).
So I did this code (to get previous screenshot):
HStack(alignment:.top) {
Text(Image(systemName: "circle.hexagongrid.circle"))
.font(.caption)
.foregroundColor(.pink)
+
Text("A long text as example")
.font(.title2).bold()
.foregroundColor(.pink)
}
.frame(width:140)
But I want to align image with center of first line. Center of image should be vertically in middle of top of A and bottom of g.
It is important that my second (and more) line passes under my image. And I don't want a bigger font for image.
I don’t know if you can force the image to automatically be centred in all cases, but you can add a .baselineOffset() modifier to the image-in-text to shift the image upwards by a fixed amount. For example:
Text(Image(systemName: "circle.hexagongrid.circle"))
.font(.caption)
.foregroundColor(.pink)
.baselineOffset(3.0)
+
Text("A long text as example")
.font(.title2).bold()
.foregroundColor(.pink)
Once you have hit upon the right offset amount for the default text size, you might be able to accommodate accessibility variations by making the offset size relative to a base size, e.g.
Struct MyView: View {
#ScaledMetric(relativeTo: .title2) var baselineOffset: CGFloat = 3.0
// and in your body…
.baselineOffset(baselineOffset)
Firstly, you can remove the HStack because it is redundant. You can just replace it with brackets () to encapsulate the new Text, so you can use more modifiers like for setting the width.
The main solution is to use baselineOffset(_:) which means you can offset your image from the baseline of Text. Below, we only offset by 1, but this can be any value you want (including decimal numbers, e.g. 0.99).
Code:
struct ContentView: View {
var body: some View {
(
Text(Image(systemName: "circle.hexagongrid.circle"))
.font(.caption)
.foregroundColor(.pink)
.baselineOffset(1)
+
Text("A long text as example")
.font(.title2)
.bold()
.foregroundColor(.pink)
)
.frame(width: 140)
}
}
Result:
Bonus: you can move your foregroundColor(.pink) modifier after the Texts have been combined, so you don't have to duplicate that.
Have you tried embedding Image in Text?
Text("Super star \(Image(systemName: "star"))")

SwiftUI: fit SF Icon into line height

When I try to display an icon next to a text, the Image adds unwanted space above, increasing the line height and I don't understand how I can control it. I tried frame, padding, resizable without success. I also tried the Label element, which my XCode does not recognize ("unresolved identifier Label").
The image shows the difference between an Image and Text, where the Image adds unwanted space.
Where is that extra space coming from and how can I control it?
VStack {
Text("User Name").bold()
HStack {
Text("hello#contact.com")
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 14))
.foregroundColor(.blue)
}
}
The image shows the difference between an Image and Text, where the Image adds unwanted space.
Where is that extra space coming from and how can I control it?
It is all about default spacing... it is, yes, strange, use explicit, eg
VStack(spacing: 0) {
// ... other your code
}

I am able to draw on each ViewController to edit Original ImageView. Is there any way to fix this?

I am working on an animation app and in each other ViewController I can draw on the image that's being currently shown on the original ImageView. Is there any way to fix this?
This is what exactly is happening. I don't really know where in the code the problem exists.
https://drive.google.com/file/d/1M7qWKMugaqeDjGls3zvVitoRmwpOUJFY/view?usp=sharing
Expected to be able to draw only on the DrawingFrame ViewController. However, I can draw on every single ViewController in my app
The problem would appear to be that your gesture recognizer is still operational, even though you’ve presented another view on top of the current one.
This is a bit unusual. Usually when you present a view like that, the old one (and its gesture recognizers) are removed from the view hierarchy. I’m guessing that you’re just sliding this second view on top of the other. There are a few solutions:
One solution would be to make sure to define this new view such that (a) it accepts user interaction; and (b) write code so that it handles those gestures. That will avoid having the view behind it picking up those gestures.
Another solution is to disable your gesture recognizer recognizer when the menu view is presented, and re-enable it when the menu is dismissed.
The third solution is to change how you present that menu view, making sure you remove the current view from the view hierarchy when you do so. A standard show/present transition generally does this, though, so we may need to see how you’re presenting this menu view to comment further.
That having been said, a few unrelated observations:
you should use UIGraphicsBeginImageContextWithOptions instead of UIGraphicsBeginImageContext;
rather than
if pencil.eraser == true { ... }
you can
if pencil.eraser { ... }
I’d suggest giving the pencil a computed property:
var color: UIColor { return UIColor(red: red, green: green, blue: blue, alpha: opacity) }
Then you can just refer to pencil.color;
property names should start with lowercase letter; and
drawingFrame is a confusing name, IMHO, because it’s not a “frame”, but rather likely a UIImageView. I’d call it drawingImageView or something like that.
Yielding:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
guard let pencil = pencil else { return }
//begins current context (and defer the ending of the context)
UIGraphicsBeginImageContextWithOptions(drawingImageView.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
//where to draw
drawingImageView.image?.draw(in: drawingImageView.bounds)
//saves context
guard let context = UIGraphicsGetCurrentContext() else { return }
//drawing the line
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.setLineCap(.round)
if pencil.eraser {
//Eraser
context.setBlendMode(.clear)
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
} else {
//opacity, brush width, etc.
context.setBlendMode(.normal)
context.setLineWidth(pencil.pencilWidth)
context.setStrokeColor(pencil.color.cgColor)
}
context.strokePath()
//storing context back into the imageView
drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
Or, even better, retire UIGraphicsBeginImageContext altogether and use the modern UIGraphicsImageRenderer:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
guard let pencil = pencil else { return }
drawingImageView.image = UIGraphicsImageRenderer(size: drawingImageView.bounds.size).image { _ in
drawingImageView.image?.draw(in: drawingImageView.bounds)
let path = UIBezierPath()
path.move(to: fromPoint)
path.addLine(to: toPoint)
path.lineCapStyle = .round
if pencil.eraser {
path.lineWidth = 10
UIColor.white.setStroke()
} else {
path.lineWidth = pencil.pencilWidth
pencil.color.setStroke()
}
path.stroke()
}
}
For more information on UIGraphicsImageRenderer, see the “Drawing off-screen” section of WWDC 2018 Image and Graphics Best Practices.
As an aside, once you get this problem behind you, you might want to revisit this “stroke from point a to point b and re-snapshot” logic to capture an array of points and build a path from a whole series, and don’t re-snapshot ever point, but only after a whole bunch have been added. This snapshotting process is slow and you’re going to find that the UX stutters a bit more than it needs. I personally re-snapshot after 100 points or so (at which point the amount of time to restroke the whole path is slow enough that it’s not much faster than the snapshot process, so if I snapshot and restart the path from where I left off, it then speeds up again).
But you say:
Expected to be able to draw only on the DrawingFrame ViewController. However, I can draw on every single ViewController in my app.
The above should draw only the image of drawingImageView and the stroke from fromPoint to toPoint. Your problem about drawing on “every single ViewController” rests elsewhere. We’d really need to see how precisely you are presenting this menu scene.

Horizontal scrollView/UIImageView layout issue

The goal: Have a scroll view that displays an array of uiimageviews (photos) that you can horizontally scroll through them
How I understand to do this: Make the frame (CGRect) of each uiimageview the height and width of the scroll view, the y value to 0 on each, and set the first imgViews x value to 0. For every imgView after that, add the width of the scrollview to the x value. In theory, this would line the imgViews (Photos) up next to each other horizontally and not allow for any vertical scrolling or zooming, purely a horizontal photo viewer.
The storyboard setup: I am creating my scrollview in a xib file (It’s a custom uiCollectionViewCell), with these constraints:
— Top space to cell (0)
— Trailing space to cell (0)
— Leading space to cell (0)
— Height of 400
— Bottom space to a view (0)
—— (See Below for img)
Laying out the UIImgViews:
func layoutScrollView() {
for (index, img) in currentImages.enumerate() {
let imgView = UIImageView(frame: CGRect(x: CGFloat(index) * scrollView.bounds.width, y: CGFloat(0), width: scrollView.bounds.width, height: scrollView.bounds.height))
imgView.contentMode = .ScaleAspectFill
imgView.image = img
scrollView.addSubview(imgView)
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(index), height: scrollView.bounds.height)
scrollView.setNeedsLayout()
}
}
My suspicion: I suspect the issue is stemming from the auto layout constraints i’ve specified, but (considering Im asking a SO question) not sure
If there is a better way to do this (really the correct way) please let me know! I have been trying to wrap my head around this for a few days now.
I appreciate all responses! Thanks for reading
EDIT #1
I tried paulvs approach of setting setNeedsLayout & layoutIfNeeded before the "for" loop, and still no luck. Here is (out of three images selected) the second photo displaying. It seems that both the first and second photos are way longer than the content view and that would move the middle view over (Squished).
Your code looks fine except for a few details (that may be causing the problem):
Add:
view.setNeedsLayout()
view.layoutIfNeeded()
before accessing the scrollView's frame (a good place would be before the for-loop).
This is because when using Autolayout, if you access a view's frame before the layout engine has performed a pass, you will get incorrect frames sizes/positions.
Remove these lines from inside the for-loop:
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(index), height: scrollView.bounds.height)
scrollView.setNeedsLayout()
and place this line after (outside) the for loop:
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(currentImages.count), height: scrollView.bounds.height)

Resources