2

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.

Woodstock
  • 22,184
  • 15
  • 80
  • 118
  • Would you provide reproducible example? It is not clear for me what is "jumping around" and in what cases... – Asperi Jul 08 '20 at 17:40
  • @Asperi sure just a sec.. – Woodstock Jul 08 '20 at 17:43
  • Updated @Asperi – Woodstock Jul 08 '20 at 17:52
  • `.font(.monospacedDigit())` (I know that doesn't answer the deeper question, and I know there are a lot of cases it doesn't resolve, which is why I'm not offering it as an answer, but it really is the answer for a surprising number of cases in my experience.) – Rob Napier Jul 08 '20 at 18:05
  • Thanks @RobNapier, I've also updated with my hack of a solution, is this really the type of approach we should be using? Seems a long way from the elegance of auto layout, yet, declarative type UI is clearly the future. I just can't get this "Spacer + 20" vibe, doesn't feel right. – Woodstock Jul 08 '20 at 18:11
  • Nah; give me a sec and I'll write something up (though Asperi will probably beat me to it). It's just that `.monospacedDigit()` is really useful and overlooked. – Rob Napier Jul 08 '20 at 18:12
  • 1
    thanks Rob! appreciate it. `.monospacedDigit()` seems to now be `.font(.system(.body, design: .monospaced))` – Woodstock Jul 08 '20 at 18:13
  • BTW, what AutoLayout constraints are you trying to reproduce? If you just centered it (as SwiftUI does), it would jump around exactly like this. – Rob Napier Jul 08 '20 at 18:22
  • @RobNapier, I guess I'm trying to elegantly reproduce: Static Text Frame anchored to left of Screen but inset by some normal amount, then the dynamic text that follows centred on screen. - Thank you! Just looking to better understand SwiftUI layout, seems `Spacer()` is a big part of that, updated with an approach I have currently which is pretty good when using your monospace idea. – Woodstock Jul 08 '20 at 18:28
  • "the dynamic text that follows centred on screen." That's going to jump around, though, since the dynamic text changes size. That's true in AutoLayout just as much as in SwiftUI. To not have it jump around, you'd have to add some fixed width to it, or you'd have to left-align it against something. You can't center it if you want its left edge not to move. (I've got some solutions here, but they have the same problems as you'd have in AL, so I just wanted to nail down what "centered" means here. What should happen if the view is too narrow for example?) – Rob Napier Jul 08 '20 at 18:31
  • @RobNapier Gotcha! If it's too narrow I'd be happy for it to truncate, if that makes sense. So I guess I'm looking for fixed width... Good points re: auto layout having similar issues, I guess in AL, I would make the centre view fixed size, or say calc in from the screen width... – Woodstock Jul 08 '20 at 18:36

1 Answers1

7

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") }
        }
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Super, thanks Rob. Really appreciate it. I see a lot of it is smart use of spacers :) Great tips in this answer. – Woodstock Jul 08 '20 at 18:55