swiftuiuiscrollviewoverlayhorizontalscrollview

SwiftUI - How can I create a horizontal scrolling View with some fixed elements?


I am trying to create a view like this in SwiftUI (sorry it's so huge): screen mockup

Specifically, I'm trying to build the scrolling row of labels/bar graph bars in the top quarter of the screen.

To me, it looks like a ScrollView with horizontal scrolling, containing some number of "barGraphItems" which are each a 270 degree-rotated VStack of a label and a colored rectangle.

Here's the code I have so far:

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack(alignment: .bottom, spacing: 0) {
            Spacer()
            VStack(alignment: .leading, spacing: 0) {
                Text("Demo")
                    .font(.caption)
                    .fontWeight(.bold)
                
                Rectangle().foregroundColor(.orange)
                    .frame(width: 84, height: 10)
            }
            .rotationEffect(Angle(degrees: 270.0))
            
            VStack(alignment: .leading, spacing: 0) {
                Text("Demo")
                    .font(.caption)
                    .fontWeight(.bold)
                
                Rectangle().foregroundColor(.orange)
                    .frame(width: 84, height: 10)
            }
            .rotationEffect(Angle(degrees: 270.0))
            
            VStack(alignment: .leading, spacing: 0) {
                Text("Demo")
                    .font(.caption)
                    .fontWeight(.bold)
                
                Rectangle().foregroundColor(.orange)
                    .frame(width: 84, height: 10)
            }
            .rotationEffect(Angle(degrees: 270.0))
            
            VStack(alignment: .leading, spacing: 0) {
                Text("Demo")
                    .font(.caption)
                    .fontWeight(.bold)
                
                Rectangle().foregroundColor(.orange)
                    .frame(width: 84, height: 10)
            }
            .rotationEffect(Angle(degrees: 270.0))
            
            VStack(alignment: .leading, spacing: 0) {
                Text("Demo")
                    .font(.caption)
                    .fontWeight(.bold)
                
                Rectangle().foregroundColor(.orange)
                    .frame(width: 84, height: 10)
            }
            .rotationEffect(Angle(degrees: 270.0))
            
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And here's what it produces (sorry it's so huge): swiftui preview

How can I get the barGraphItems closer together? How can I add the "Budget" line? How can I make the whole business scrollable WITHOUT scrolling the Budget line?

I think the other elements onscreen I can suss out, but I've been fiddling with this bar graph widget all afternoon and I'm stumped.


Solution

  • The problem with applying rotation effect to complex container having many views is that layout of container does not change, so all other views affected by rotated container

    It looks more appropriate different approach. The idea is to have plot bars as rectangles where height or rectangle show values, and label is a transformed overlay for such rectangle, thus rectangles form layout, and overlays do not affect anything (almost).

    Tested with Xcode 11.4 / iOS 13.4

    Note: the budget line can be added above plot scrollview by wrap both into ZStack.

    demo

    demo2

    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            VStack(alignment: .leading, spacing: 0) {
                Text("Title above").font(.largeTitle)
                ContentView()
                Text("comment below")
            }
        }
    }
    
    
    struct ContentView: View {
        var plotHeight: CGFloat = 120
    
        // test with different lenth labels
        let demos = ["Demo", "Demoos", "Demoooos", "Demooooooos"]
    
        var body: some View {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .bottom, spacing: 24) {
                    ForEach(0..<20) {_ in
                        Rectangle().foregroundColor(.orange)
                            .frame(width: 14, height: CGFloat.random(in: 0..<self.plotHeight)) // values for demo
                            .overlay(
                                Text(self.demos.randomElement()!) // values for demo
                                    .font(.caption)
                                    .fontWeight(.bold)
                                    .fixedSize()
                                    .rotationEffect(Angle(degrees: 270.0), anchor: .leading)
                                    .offset(x: -8, y: 6)  // align as/if needed here
                            , alignment: .bottomLeading)
                    }
                }
                .frame(height: plotHeight, alignment: .bottom)
                .padding(.leading) // required offset for first label in scrollview
            }
        }
    }