I've been trying to emulate the Ray Wenderlich Swift UI graph tutorial for my own problem and it mostly works well, but I'm repeatedly hitting the 'The compiler is unable to type-check this expression in reasonable time' error due to the complexity of the view.
I've also tried to break the various sections into their own views but then I can't reference my coordinate transformation helper methods that convert from engineering units to window units residing in the parent view from the subviews. Here is the code I've adapted from the tutorial:
protocol GraphView {
var xAxisMaxRange: CGFloat { get }
var xAxisMinRange: CGFloat { get }
var xAxisOffset: CGFloat { get }
var yAxisMaxRange: CGFloat { get }
var yAxisMinRange: CGFloat { get }
var yAxisOffset: CGFloat { get }
var numVerticalGridLines: Int { get }
var numVerticalGridLinesPlusOne: Int { get }
var numHorizontalGridLines: Int { get }
var numHorizontalGridLinesPlusOne: Int { get }
}
extension GraphView {
func xAxisTransform(_ value: CGFloat, plotScreenWidth: CGFloat) -> CGFloat {
return (value / (xAxisMaxRange - xAxisMinRange)) * plotScreenWidth
}
func yAxisTransform(_ value: CGFloat, plotScreenHeight: CGFloat) -> CGFloat {
return plotScreenHeight - (value / (yAxisMaxRange - yAxisMinRange)) * plotScreenHeight
}
func plotAreaWidth(_ readerWidth: CGFloat) -> CGFloat {
return readerWidth - 2 * yAxisOffset
}
func plotAreaHeight(_ readerHeight: CGFloat) -> CGFloat {
return readerHeight - 2 * xAxisOffset
}
func plotAreaSize(_ readerSize: CGSize) -> CGSize {
return CGSize(width: plotAreaWidth(readerSize.width), height: plotAreaHeight(readerSize.height))
}
func point(x: Double, y: Double, readerSize: CGSize) -> CGPoint {
return CGPoint(x: yAxisOffset + xAxisTransform(CGFloat(x), plotScreenWidth: plotAreaSize(readerSize).width),
y: xAxisOffset + yAxisTransform(CGFloat(y), plotScreenHeight: plotAreaSize(readerSize).height))
}
func point(x: Double, y: CGFloat, readerSize: CGSize) -> CGPoint {
return point(x: x, y: Double(y), readerSize: readerSize)
}
func point (x: CGFloat, y: Double, readerSize: CGSize) -> CGPoint {
return point(x: Double(x), y: y, readerSize: readerSize)
}
func point(x: CGFloat, y: CGFloat, readerSize: CGSize) -> CGPoint {
return point(x: Double(x), y: Double(y), readerSize: readerSize)
}
func xAxisLabel(labelIndex: Int) -> String {
let labelValueSeconds = CGFloat(labelIndex) * (xAxisMaxRange - xAxisMinRange) / CGFloat(numVerticalGridLines)
let labelValueHours = Int(labelValueSeconds / 3600.0)
var labelValueHour = labelValueHours % 12
if labelValueHour == 0 { labelValueHour = 12}
return String(format: "%d", labelValueHour)+(labelValueHours<12 || labelValueHours == 24 ? " AM" : " PM")
}
func yAxisLabel(labelIndex: Int) -> String {
let labelValue = CGFloat(labelIndex) * (yAxisMaxRange - xAxisMinRange) / CGFloat(numHorizontalGridLines)
return String(format: "%.1f", labelValue)+(labelIndex == numHorizontalGridLines ? " ms" : "")
}
func xAxisLabelOffset(labelIndex: Int, readerSize: CGSize) -> CGFloat {
let offsetBetweenGrids = plotAreaWidth(readerSize.width) / CGFloat(numVerticalGridLines)
return yAxisOffset + CGFloat(labelIndex) * offsetBetweenGrids
}
func yAxisLabelOffset(labelIndex: Int, readerSize: CGSize) -> CGFloat {
let offsetBetweenGrids = plotAreaHeight(readerSize.height) / CGFloat(numHorizontalGridLines)
return xAxisOffset + plotAreaHeight(readerSize.height) - CGFloat(labelIndex) * offsetBetweenGrids - offsetBetweenGrids / 2.0
}
func xAxisGridWidth(readerSize: CGSize) -> CGFloat {
return plotAreaWidth(readerSize.width) / CGFloat(numVerticalGridLines)
}
func yAxisGridHeight(readerSize: CGSize) -> CGFloat {
return plotAreaHeight(readerSize.height) / CGFloat(numHorizontalGridLines)
}
}
struct DailyGraph: View, GraphView {
let xAxisMaxRange: CGFloat = 24 * 3600
let xAxisMinRange: CGFloat = 0
let xAxisOffset: CGFloat = 50
let yAxisMaxRange: CGFloat = 60 // ms
let yAxisMinRange: CGFloat = 0
let yAxisOffset: CGFloat = 60
let numVerticalGridLines: Int = 24
let numVerticalGridLinesPlusOne: Int
let numHorizontalGridLines: Int = 6
let numHorizontalGridLinesPlusOne: Int
@ObservedObject var historicalDataManager: HistoricalDataManager
init(historicalDataManager: HistoricalDataManager) {
self.historicalDataManager = historicalDataManager
numVerticalGridLinesPlusOne = numVerticalGridLines + 1
numHorizontalGridLinesPlusOne = numHorizontalGridLines + 1
}
var body: some View {
GeometryReader { reader in
Text("Response (ms) vs Time of Day")
.frame(width: reader.size.width, height: self.xAxisOffset, alignment: .center)
.font(.largeTitle)
// draw the vertical grid lines
ForEach(0..<self.numVerticalGridLinesPlusOne) { line in
Group {
Path { path in
let gridBottom = self.point(x: CGFloat(line) * (self.xAxisMaxRange - self.xAxisMinRange) / CGFloat(self.numVerticalGridLines),
y: self.yAxisMinRange,
readerSize: reader.size)
let gridTop = self.point(x: CGFloat(line) * (self.xAxisMaxRange - self.xAxisMinRange) / CGFloat(self.numVerticalGridLines),
y: self.yAxisMaxRange,
readerSize: reader.size)
path.move(to: gridBottom)
path.addLine(to: gridTop)
}.stroke(line == 0 || line == self.numVerticalGridLines ? Color.black : Color.gray)
Text(self.xAxisLabel(labelIndex: line))
.frame(width: self.xAxisGridWidth(readerSize: reader.size), height: self.xAxisOffset, alignment: .center)
.offset(x: self.xAxisLabelOffset(labelIndex: line, readerSize: reader.size) - self.xAxisGridWidth(readerSize: reader.size) / 2.0,
y: reader.size.height - self.xAxisOffset)
}
}
// draw the horizontal grid lines
ForEach(0..<self.numHorizontalGridLinesPlusOne) { line in
Group {
Path { path in
let gridLeft = self.point(x: self.xAxisMinRange,
y: CGFloat(line) * (self.yAxisMaxRange - self.yAxisMinRange) / CGFloat(self.numHorizontalGridLines),
readerSize: reader.size)
let gridRight = self.point(x: self.xAxisMaxRange,
y: CGFloat(line) * (self.yAxisMaxRange - self.yAxisMinRange) / CGFloat(self.numHorizontalGridLines),
readerSize: reader.size)
path.move(to: gridLeft)
path.addLine(to: gridRight)
}.stroke(line == 0 || line == self.numHorizontalGridLines ? Color.black : Color.gray)
Text(self.yAxisLabel(labelIndex: line))
.frame(width: self.yAxisOffset, height: self.yAxisGridHeight(readerSize: reader.size), alignment: .center)
.offset(x: 0, y: self.yAxisLabelOffset(labelIndex: line, readerSize: reader.size))
}
}
// draw the graph signal
ForEach(0..<$historicalDataManager.todaysData.count) { dataIndex in
Group {
Text("\(dataIndex)")
}
}
}
}
}
class HistoricalDataManager : ObservableObject {
// minimal class only for compilation..
@Published var todaysData: [(Date,Double)] = []
}
When I try to move each of the ForEach sections to their own view I lose reference to the helper methods. Is there an appropriate pattern to break these apart and still successfully reference the helper functions to complete the coordinate transformations? I thought I might be hitting the 10 views per group limit but refactoring the views into smaller groups didn't help. An example would be greatly appreciated.