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
}
}