Search code examples
iosswiftviewautolayoutswiftui

Correct way to layout SwiftUI (similar to Autolayout)


Question:

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:

Issue

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.

Potential Solution:

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.


Solution

  • 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") }
            }
        }
    }