Reputation: 22926
I'm struggling to layout views effectively with SwiftUI. I am very familiar with UIKit and Autolayout and always found it intuitive.
I know SwiftUI is young and only beginning so maybe I expect too much, but taking a simple example:
Say I have a HStack
of Text()
views.
|--------------------------------|
| Text("static") Text("Dynamic") |
|________________________________|
When I have dynamic content, the static Text strings jump all over the place as the size of the HStack
changes, when Text("Dynamic") changes...
I've tried lot's of things, Spacers()
, Dividers()
, looked at approaches using PreferenceKeys
(link), Alignment Guides
(link)
Closest to an answer seems alignment guides, but they are convoluted.
What's the canonical approach to replicate Autolayout's ability to basically anchor views to near the edge of the screen, and layout correctly without jumping around?
I'd like to anchor the static text "Latitude" so it doesn't jump around.
There are other examples, so a more general answer on how best to layout would be appreciated...
With Autolayout it felt I chose were things went. With SwiftUI it's a lottery.
Example, showing the word "Latitude" jump around as co-ordinates change:
Example, code:
HStack {
Text("Latitude:")
Text(verbatim: "\(self.viewModelContext.lastRecordedLocation().coordinate.latitude)")
}
I'm really struggling when my views have changing/dynamic context. All works OK for static content as shown in all of the WWDC videos.
Using a HStack
like this:
HStack(alignment: .center, spacing: 20) {
Text("Latitude:")
Text(verbatim: "\(self.viewModelContext.lastRecordedLocation().coordinate.latitude)")
Spacer()
}
.padding(90)
The result is nicely anchored, but I hate magic numbers.
Upvotes: 4
Views: 5802
Reputation: 5117
From iOS 16 Grid
and GridRow
can simplify the task a lot.
The columns width is calculated based on the cell sizes of row elements. You can get to decide how many columns each row element should occupy with .gridCellColumns(n)
.
Consider such sample:
struct GridTextView: View {
var rowData: [RowData] = RowData.sample
var body: some View {
Grid(alignment: .leading) {
Text("Some sort of title")
ForEach(RowData.sample) { row in
GridRow {
Text(row.id)
Text(row.name)
.gridCellColumns(2)
}
}
GridRow {
let row = RowData.sample.first!
Text(row.id)
Text(row.name)
Text("Third extra column")
}
}
.padding()
}
}
struct RowData: Identifiable {
var id: String
var name: String
static var sample: [Self] = [.init(id: "1", name: "Joe Doe"), .init(id: "10000", name: "Diana Snow")]
}
Upvotes: 1
Reputation: 299265
As you've somewhat discovered, the first piece is that you need to decide what you want. In this case, you seem to want left-alignment (based on your padding solution). So that's good:
HStack {
Text("Latitude:")
Text(verbatim: "\(randomNumber)")
Spacer()
}
That's going to make the HStack as wide as its containing view and push the text to the left.
But from you later comments, you seem to not want it to be on the far left. You have to decide exactly what you want in that case. Adding .padding
will let you move it in from the left (perhaps by adding .leading
only), but maybe you want to match it to the screen size.
Here's one way to do that. The important thing is to remember the basic algorithm for HStack, which is to give everyone their minimum, and then split up the remaining space among flexible views.
HStack {
HStack {
Spacer()
Text("Latitude:")
}
HStack {
Text(verbatim: "\(randomNumber)")
Spacer()
}
}
The outer HStack has 2 children, all of whom are flexible down to some minimum, so it offers each an equal amount of space (1/2 of the total width) if it can fit that.
(I originally did this with 2 extra Spacers, but I forgot the Spacers seem to have special handling to get their space last.)
The question is what happens if randomNumber
is too long? As written, it'll wrap. Alternatively, you could add .fixedSize()
which would stop it from wrapping (and push Latitude to the left to make it fit). Or you could add .lineLimit(1)
to force it to truncate. It's up to you.
But the important thing is the addition of flexible HStacks. If every child is flexible, then they all get the same space.
If you want to force things into thirds or quarters, I find you need to add something other than a Spacer. For example, this will give Latitude and the number 1/4 of the available space rather than 1/2 (note the addition of Text("")
):
HStack {
HStack {
Text("")
Spacer()
}
HStack {
Spacer()
Text("Latitude:")
}
HStack {
Text(verbatim: "\(randomNumber)")//.lineLimit(1)
Spacer()
}
HStack {
Text("")
Spacer()
}
}
In my own code, I do this kind of thing so much I have things like
struct RowView: View {
// A centered column
func Column<V: View>(@ViewBuilder content: () -> V) -> some View {
HStack {
Spacer()
content()
Spacer()
}
}
var body: some View {
HStack {
Column { Text("Name") }
Column { Text("Street") }
Column { Text("City") }
}
}
}
Upvotes: 8