NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

SOLVED: WatchOs: Best way to create some kind of table with static header and footer and scrollable content and most important how to center header with content and with footer

Forums > SwiftUI

Hi all,

On the below screenshot you can see my layout on the watch. It's a simple table to show some scores. I don't think I have found an optimal way to code it, because I'm using a lot of .padding to center the name with the scores and with the total score at the bottom. On the smaller watch like version 6 it will not look as good as here. There must be a better way I can't see right now! The question is about the optimal way to center each of the 3 columns, where the header with names and footer with total scores are always visible, but the middle part with scores is scrollable. The 3 visible columns at the end must be centered.

My code:

VStack {
    HStack {
        Text("Tomasz")
            .font(.footnote)
            .padding(.leading, 10)
        Spacer()
        Text("Adam")
            .font(.footnote)
        Spacer()
        Text("Pawel")
            .font(.footnote)
            .padding(.trailing, 10)
    }
    Divider()
    ScrollView (showsIndicators: false) {
        HStack {
            VStack {
                ForEach (game.rounds) { round in
                    Text(String(round.scores[0]))
                        .padding(.leading, 30)
                }
            }
            Spacer()
            VStack {
                ForEach (game.rounds) { round in
                    Text(String(round.scores[1]))
                        .padding(.leading, 5)
                }
            }
            Spacer()
            VStack {
                ForEach (game.rounds) { round in
                    Text(String(round.scores[2]))
                        .padding(.trailing, 23)
                }
            }
        }
    }
    Divider()
    HStack {
        Text(game.sumPoints(forUser: 0))
            .font(.title3)
            .bold()
            .padding(.leading, 29)
        Spacer()
        Text(game.sumPoints(forUser: 1))
            .font(.title3)
            .bold()
            .padding(.leading, 5)
        Spacer()
        Text(game.sumPoints(forUser: 2))
            .font(.title3)
            .bold()
            .padding(.trailing, 22)
    }
}

Screenshot

   

I don't know much about WatchOS but it seems like the exercises in Project 18 of Hacking with SwiftUI might be able to help you. It shows examples of how to create your own layout guides to align views with custom guide lines that you create.

So you could create your own guides named .column1Center, .column2Center, etc, and use those instead of .leading, .trailing, .top, etc.

   

What about the new Grid Layout? https://sarunw.com/posts/swiftui-grid/

   

@Hatsushira I think Grid will not help me, because I need the header and footer to be always visible and only the middle part should scroll. I can't do this with Grid.

@Fly0strich Nice suggestion and I had a lot of hope until I started to play with this... I made to get it work with a single guideline for middle player. The problem is that I would need 3 guidelines to center all the views for each of 3 players (3 columns). I thought I need to create 3 different extensions and name it differently to create 3 guidelines, but only a single guideline could be used in the top VStack... How to make 3 different guidelines is now the question. Another problem is, that to make the middle part scrollable I need to put the middle HStacks with rounds into ScrollView and the guidelines are not working in this case. I have the player name and the total score aligned, but the points in the middle (which are in the ScrollView) are not alaigned anymore...

Anybody any more ideas?

My code:

extension HorizontalAlignment {
    enum CenterMiddlePlayer: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[.trailing]
        }
    }

    static let centerMiddlePlayer = HorizontalAlignment(CenterMiddlePlayer.self)
}

struct ContentView: View {
    var body: some View {
        VStack (alignment: .centerMiddlePlayer) {
            // player names
            HStack  {
                Text("Tomasz")
                Text("Adam")
                    .alignmentGuide(.centerMiddlePlayer) { d in d[HorizontalAlignment.center]}
                Text("Pawel")
            }
            // multiple rounds
            HStack {
                Text("1")
                Text("0")
                    .alignmentGuide(.centerMiddlePlayer) { d in d[HorizontalAlignment.center]}
                Text("0")
            }
            HStack {
                Text("1")
                Text("0")
                    .alignmentGuide(.centerMiddlePlayer) { d in d[HorizontalAlignment.center]}
                Text("0")
            }
            // total points
            HStack {
                Text("2")
                    .bold()
                Text("0")
                    .bold()
                    .alignmentGuide(.centerMiddlePlayer) { d in d[HorizontalAlignment.center]}
                Text("0")
                    .bold()
            }
        }
    }
}

Screenshot with ScrollView: Screenshot with ScrollView

Screenshot without ScrollView: Screenshot without ScrollView

   

@Hatsushira I think Grid will not help me, because I need the header and footer to be always visible and only the middle part should scroll. I can't do this with Grid.

Give it a try:

struct GridDemo: View {
    var body: some View {
        Grid() {
            GridRow {
                Text("Header")
            }
            ScrollView {
                ForEach([0,1,2,3,4,5,6,7,8,9], id: \.self) { i in
                    GridRow {
                        Text("Row \(i)")
                    }
                }
            }
            GridRow {
                Text("Footer")
            }
        }
    }
}

   

Sorry, I just had some time to mess around with your code a bit, and I think you're right. You would only be able to use 1 alignment guide for each Stack, and then choose a View in each stack that you used it on to align with a View in the other stack. So creating one guide for each column doesn't really help you in this case. My bad, I didn't understand how those guides worked very well when I suggested it.

But something else that you could do is use GeometryReader.

struct ContentView: View {
    var body: some View {
        GeometryReader { geo in
            VStack () {
                // player names
                HStack  {
                    Text("Tomasz")
                        .frame(width: geo.size.width / 3)
                    Text("Adam")
                        .frame(width: geo.size.width / 3)
                    Text("Pawel")
                        .frame(width: geo.size.width / 3)
                }
                // multiple rounds
                HStack {
                    Text("1")
                        .frame(width: geo.size.width / 3)

                    Text("0")
                        .frame(width: geo.size.width / 3)

                    Text("0")
                        .frame(width: geo.size.width / 3)

                }
                HStack {
                    Text("1")
                        .frame(width: geo.size.width / 3)

                    Text("0")
                        .frame(width: geo.size.width / 3)

                    Text("0")
                        .frame(width: geo.size.width / 3)

                }
                // total points
                HStack {
                    Text("2")
                        .bold()
                        .frame(width: geo.size.width / 3)

                    Text("0")
                        .bold()
                        .frame(width: geo.size.width / 3)
                    Text("0")
                        .bold()
                        .frame(width: geo.size.width / 3)
                }
            }
        }
    }
}

GeometryReader will basically just measure the maximum space available to whatever view you wrap inside of it, and allow you to use those measurements as a reference for where you want to place things, or how big you want them to be in reference to those measurements.

This is repeating the same .frame(width:) modifier a lot. But you wouldn't need to add it so many times if you were using ForEach to create the text fields in your actual app. But this will ensure that each Text view takes up exactly 1/3 of the available screen width. So the text views are all aligned perfectly.

1      

@Hatsushira I've tried it, but forgot to write about the problem. The issue is that the GridRow inside a ScrollView behaves odd. If I have multiple Text views, in my case 3, each with point for an user, the row isn't created properly, but the Text views are below each other like in the VStack. Please check the screenshot. I've tried to use Group inside GridRow and other views, but it is not helping.

var body: some View {
        Grid() {
            GridRow {
                Text("Tomasz")
                Text("Adam")
                Text("Pawel")
            }
            ScrollView {
                ForEach([0,1,2,3,4,5,6,7,8,9], id: \.self) { i in
                    GridRow {
                        Text("\(i)")
                        Text("\(i)")
                        Text("\(i)")
                    }
                }
            }
            GridRow {
                Text("3")
                Text("2")
                Text("5")
            }
        }
    }

Example

   

Ah, I see.

I stumbled across a session from WWDC 2022 Compose custom layouts with SwiftUI perhaps there you find some useful tips.

1      

@Fly0strich Your solution is working and I don't need to play with padding guessing appropriate value anymore. It will also automatically adapt to available space on watches with smaller screen sizes. After watching Compose custom layouts with SwiftUI proposed by Hatsushira there could be some other solution, but to be honest this video is too complicated for me at the stage where I currently am with SwiftUI and programming, so I will try to implement this GeometryReader stuff :). Thanks to all!

1      

Hacking with Swift is sponsored by Play

SPONSORED Play is the first native iOS design tool created for designers and engineers. You can install Play for iOS and iPad today and sign up to check out the Beta of our macOS app with SwiftUI code export. We're also hiring engineers!

Click to learn more about Play!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.