Best approach getting subviews to layout vertically in SwiftUI?

285 views Asked by At

I'm having a hard time getting subviews to lay out correctly, especially the heights. I hate to hard code the subview heights but that is the only thing I can get to work. If I remove these frame(heights) from the subviews, the subviews overlap in the parent view. Any suggestions for a better way to architect this without the hard coded heights?

struct SkateDetailView: View {
    
    @EnvironmentObject var combineWorkoutManager: CombineWorkoutManager
    @Environment(\.colorScheme) var colorScheme
    @State var subscriptionIsActive = false
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                combineWorkoutManager.workout.map { SkateDetailHeaderView(workout: $0)}
                combineWorkoutManager.heartRateSamplesFromWorkout.map { HeartRateRecoveryCard(heartRateSamples: $0) }
                combineWorkoutManager.vo2MaxFromWorkout.map { vo2MaxCard(vo2MaxValue: $0) }
            }
            .padding(.top)
            .background(colorScheme == .dark ? Color.black : Color.offWhite)
            .onAppear {
                combineWorkoutManager.loadHeartRatesFromWorkout()
                combineWorkoutManager.loadVo2MaxFromWorkout()
                combineWorkoutManager.getWorkout()
                self.subscriptionIsActive = UserDefaults.standard.bool(forKey: subscriptionIsActiveKey)
            }
            //Code for conditional view modifer from: https://fivestars.blog/swiftui/conditional-modifiers.html
            .if(subscriptionIsActive) { $0.redacted(reason: .placeholder)}
        }
        
        
    }
}

struct SkateDetailHeaderView: View {
    
    var workout: HKWorkout
    @Environment(\.colorScheme) var colorScheme
    @State var sessionTypeImage = Image("Game_playerhelmet")
    
    var body: some View {
        ZStack(alignment: .leading) {
            (colorScheme == .dark ? Color.black : Color.white)
                .cornerRadius(10)
            VStack(alignment: .leading) {
                HStack() {
                    sessionTypeImage
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 35.0, height: 35.0)
                        .unredacted()
                    Text(WorkoutManager.ReadOutHockeyTrackerMetadata(workout: workout)?.sessionType ?? "")
                        .font(.custom(futuraLTPro, size: largeTitleTextSize))
                        .unredacted()
                }
                .padding(.leading)
                .onAppear {
                    setSessionTypeImage()
                }
                HStack {
                    Text("Goals")
                    Text("Assists")
                    Text("+/-")
                }
                .font(.custom(futuraMedium, size: captionTextSize))
                .unredacted()
            }
             .padding(.all)
        }
       .padding(.horizontal)
    }

struct HeartRateRecoveryCard: View {
    
    //Don't download HR samples for this view the parent view should download HR samples and pass them into it's children views
    @State var heartRateSamples: [HKQuantitySample]
    @State var HRRAnchor = 0
    @State var HRRTwoMinutesLater = 0
    @State var heartRateRecoveryValue = 0
     @Environment(\.colorScheme) var colorScheme
   
    var body: some View {
    ZStack {
      (colorScheme == .dark ? Color.black : Color.white)
          .cornerRadius(10)
        VStack(alignment: .leading) {
            VStack(alignment: .leading) {
                Text("Heart Rate Recovery")
                    .font(.custom(futuraLTPro, size: titleTextSize))
                    .unredacted()
                Text("\(heartRateRecoveryValue) bpm")
                    .font(.custom(futuraMedium, size: titleTextSize))
                    .font(Font.body.monospacedDigit())
                Text("\(HRRAnchor) bpm - \(HRRTwoMinutesLater) bpm")
                    .font(.custom(futuraMedium, size: captionTextSize))
                    .font(Font.body.monospacedDigit())
                    .foregroundColor(Color(.secondaryLabel))
            }
            .padding(.leading)
            
            SwiftUILineChart(chartLabelName: "Heart Rate Recovery", entries: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).0, chartLineAndGradientColor: ColorCompatibility.label, xAxisFormatter: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).1, showGradient: false)
                .frame(width: 350, height: 180, alignment: .center)
                .cornerRadius(12)
                .onAppear {
                    
                    if let unwrappedHRRObject =  HeartRateRecoveryManager.calculateHeartRateRecoveryForHRRGraph(heartRateSamples: heartRateSamples) {
                        HRRAnchor = unwrappedHRRObject.anchorHR
                        HRRTwoMinutesLater = unwrappedHRRObject.twoMinLaterHR
                        heartRateRecoveryValue = unwrappedHRRObject.hrr
                        print("HRR = \(heartRateRecoveryValue)")
                    }
                }
                
                BarScaleView(valueToBeScaled: heartRateRecoveryValue, scaleMax: 60, numberOfBlocks: 5, colors: [logoAquaShade1Color, logoAquaShade2Color, logoAquaShade3Color, logoAquaShade4Color, logoAquaShade5Color], blockValues: [0, 12, 24, 36, 48])
                
        }
        .padding(.top)
    }
     .frame(height: 375)
    .padding(.all)
    }

struct vo2MaxCard: View {
    
    @State var vo2MaxValue: Int
      @Environment(\.colorScheme) var colorScheme
    
    var body: some View {
        ZStack {
         (colorScheme == .dark ? Color.black : Color.white)
          .cornerRadius(10)
            VStack(alignment: .leading) {
                VStack(alignment: .leading) {
                    Text("VO2 Max")
                        .font(.custom(futuraLTPro, size: titleTextSize))
                        .padding(.top)
                        .unredacted()
                    Text("\(vo2MaxValue) mL/(kg - min)")
                        .font(.custom(futuraMedium, size: titleTextSize))
                        .font(Font.body.monospacedDigit())
                }
                .padding(.leading)
                BarScaleView(valueToBeScaled: vo2MaxValue, scaleMax: 72, numberOfBlocks: 4, colors: [logoRedShade1Color, logoRedShade2Color, logoRedShade3Color, logoRedShade4Color, logoRedShade5Color], blockValues: [0, 18, 36, 54])
                
            }
        }
        .frame(height: 175)
        .padding(.all)
    }
}

struct BarScaleView: View {

    var valueToBeScaled: Int
    var scaleMax: Int
    var numberOfBlocks: Int
    var colors: [UIColor] 
    var blockValues: [Int] //e.g. 0, 12, 24, 36, 48
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                HStack(spacing: 0) {
                    ForEach(0..<numberOfBlocks) { index in
                        ScaleBarBlock(numberOfBlocks: numberOfBlocks, blockColor: Color(colors[index]), proxy: geometry, blockValue: blockValues[index])
                    }
                }
                Image(systemName: "triangle.fill")
                    .padding(.top)
                    .unredacted()
                    // .if(subscriptionIsActive) { $0.redacted(reason: .placeholder)}
                    .if(valueToBeScaled >= scaleMax) { $0.position(x: geometry.size.width - 5) }
                    .if(valueToBeScaled < scaleMax) { $0.position(x: geometry.size.width * CGFloat(Double(valueToBeScaled) / Double(scaleMax))) }
                
                
            }
        }
        .padding()
    }
}


struct SwiftUILineChart: UIViewRepresentable {
    
    var chartLabelName: String
    //Line Chart accepts data as array of BarChartDataEntry objects
    var entries: [ChartDataEntry]
    var chartLineAndGradientColor = logoRedShade1Color
    
    var xAxisFormatter: ChartXAxisFormatter?
    var showGradient = true
    
    // this func is required to conform to UIViewRepresentable protocol
    func makeUIView(context: Context) -> LineChartView {
        //crate new chart
        let chart = LineChartView()
        //it is convenient to form chart data in a separate func
        chart.data = addData()
        chart.backgroundColor = .clear
        chart.pinchZoomEnabled = false
        chart.dragEnabled = false
        chart.isUserInteractionEnabled = false
        
        //If available pass xAsisFormatter to chart
        if let unwrappedxAxisFormatter = xAxisFormatter {
            chart.xAxis.valueFormatter = unwrappedxAxisFormatter
            chart.xAxis.setLabelCount(5, force: true) //set number of labels at top of graph to avoid too many (Not using since setting granularity works)
            chart.xAxis.avoidFirstLastClippingEnabled = true
            chart.xAxis.labelPosition = .bottom
        }
        
        return chart
    }
    
    // this func is required to conform to UIViewRepresentable protocol
    func updateUIView(_ uiView: LineChartView, context: Context) {
        //when data changes chartd.data update is required
        uiView.data = addData()
    }
    
      func addData() -> LineChartData {
        let data = LineChartData() 
        //BarChartDataSet is an object that contains information about your data, styling and more
        let dataSet = LineChartDataSet(entries: entries)
        
        if showGradient == true {
            
            let gradientColors = [chartLineAndGradientColor.cgColor, UIColor.clear.cgColor]
            let colorLocations: [CGFloat] = [1.0, 0.0]  //positioning of gradient
            let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: colorLocations)!
            
            dataSet.fillAlpha = 1
            dataSet.fill = Fill(linearGradient: gradient, angle: 90)
            dataSet.drawFilledEnabled = true
        }
        
        // change bars color to green
        dataSet.colors = [chartLineAndGradientColor]
         dataSet.drawCirclesEnabled = false  //no circles
         dataSet.drawValuesEnabled = false //hide labels on the datapoints themselves
        //change data label
        dataSet.label = chartLabelName
        data.addDataSet(dataSet)
        return data
    }
    
    
}

How I would like this View to look but without hard coded heights

0

There are 0 answers