3

I have a horizontal ScrollView in SwiftUI that I would like to 'dynamically' center. the content within the scrollview will be a dynamic chart, and the user can change the width of it by changing years (5, 10, 15 year chart grows horizontally).

Anyway I have two problems:

  1. My content in the horizontal scrollview seems to be aligned left, and any attempt at centering it with padding is just leaving it off centered and makes it difficult to account for various screen sizes.

  2. When I try using a GeometryReader, my chart's 'legend' at the bottom gets shoved to the bottom of the page, Spacer() doesn't fix it.

struct ChartView: View {   
    var body: some View {
        VStack(alignment: .center) {
            GeometryReader { geo in
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                       ForEach(0..<self.chartSettings.getChartYears(), id:\.self) { index in
                           ColumnView()
                       }
                    }
                }
                .background(Color.gray)
                .frame(maxHeight: geo.size.height/2)
            }
            
            
            //Chart Legend
            HStack{
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
                Text("First Value")
                    .font(.system(size: 12))
                
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.2263821661, green: 0.4659538441, blue: 0.08977641062, alpha: 1)))
                Text("Second Value")
                    .font(.system(size: 12))
            }
        }
    }
}

Left is what it looks like with current code and right is what I'm hoping it looks like regardless of screen size/iPhone model.

enter image description here

Note This is the reason I'm using a scrollview:

<iframe src='https://gfycat.com/ifr/DeficientNiceInchworm' frameborder='0' scrolling='no' allowfullscreen width='400' height='210'></iframe>
steverngallo
  • 121
  • 1
  • 10

2 Answers2

3

Move GeometryReader outside of VStack:

struct ChartView: View {
    var body: some View {
        GeometryReader { geo in
            VStack(alignment: .center) {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                       ForEach(0..<self.chartSettings.getChartYears(), id:\.self) { index in
                           ColumnView()
                       }
                    }
                }
                .background(Color.gray)
                .frame(maxHeight: geo.size.height/2)

                //Chart Legend
                HStack{
                    Rectangle()
                        .frame(width: 10, height: 10)
                        .foregroundColor(Color.init(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
                    Text("First Value")
                        .font(.system(size: 12))

                    Rectangle()
                        .frame(width: 10, height: 10)
                        .foregroundColor(Color.init(#colorLiteral(red: 0.2263821661, green: 0.4659538441, blue: 0.08977641062, alpha: 1)))
                    Text("Second Value")
                        .font(.system(size: 12))
                }
            }
        }
    }
}

Note: horizontal scroll view content always start from left (it is not alignment). If you want just center HStack on screen - remove ScrollView.

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • hmm, you may have a good point there and appreciate the help! This is the behavior I have currently that necessitates using a ScrollView: https://gfycat.com/deficientniceinchworm – steverngallo Jul 22 '20 at 14:28
  • 1
    then you have to adapt for width [the approach I made here](https://stackoverflow.com/a/61185571/12299030) for height. – Asperi Jul 22 '20 at 14:50
3

In order to correctly center your chart in the ScrollView, set the minimum width of the content to be the same as the width of the ScrollView itself. To do this, call .frame(minWidth: geo.size.width) on the HStack inside of your ScrollView. A full explanation of this solution can be found here.

The reason the legend gets pushed to the bottom of the screen is that GeometryReader is itself a view that expands to fill all the available space. There are two solutions that I can think of that will solve this issue.

Solution 1 - Static Chart Height

If the height of the chart is static, or if you know what the height will be before you render your views, you can use that known dimension to set the height of the GeometryReader.

struct ChartView: View {   

    private let chartHeight: CGFloat = 300 // declare a height parameter

    var body: some View {

        VStack(alignment: .center) {
            GeometryReader { geo in
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                       ForEach(0..<self.chartSettings.getChartYears(), id:\.self) { index in
                           ColumnView()
                       }
                    }
                    .frame(minWidth: geo.size.width)
                }
                .background(Color.gray)
            }
            .frame(height: chartHeight) // set the height of the GeometryReader
            
            //Chart Legend
            HStack{
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
                Text("First Value")
                    .font(.system(size: 12))
                
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.2263821661, green: 0.4659538441, blue: 0.08977641062, alpha: 1)))
                Text("Second Value")
                    .font(.system(size: 12))
            }
        }

    }

}

Solution 2 - Dynamic Chart Height

If you want the height of the chart to be dynamic and allow the ScrollView to determine its height based on the content, you can use Spacers() and call the .layoutPriority(0) method on them so that the GeometryReader and spacers are given equal priority when laying out their views. With this solution, the GeometryReader will only place a small default padding around the ScrollView.

Optionally, you can use a negative padding value if you want the legend to be closer to the ScrollView.

struct ChartView: View {   

    var body: some View {

        VStack(alignment: .center) {

            Spacer().layoutPriority(0) // add a spacer above

            GeometryReader { geo in
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                       ForEach(0..<self.chartSettings.getChartYears(), id:\.self) { index in
                           ColumnView()
                       }
                    }
                    .frame(minWidth: geo.size.width)
                }
                .background(Color.gray)
            }
            
            //Chart Legend
            HStack{
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
                Text("First Value")
                    .font(.system(size: 12))
                
                Rectangle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(Color.init(#colorLiteral(red: 0.2263821661, green: 0.4659538441, blue: 0.08977641062, alpha: 1)))
                Text("Second Value")
                    .font(.system(size: 12))
            }
            .padding(.top, -20) // optionally move the legend closer to the ScrollView

            Spacer().layoutPriority(0) // add a spacer below

        }

    }

}
Jon
  • 159
  • 1
  • 6
  • thanks for the response - since I posted this, apple also released ScrollViewReader: https://developer.apple.com/documentation/swiftui/scrollviewreader I've since removed most of my scrollviews to avoid messing with this – steverngallo Oct 21 '20 at 01:20