0

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.

Scott
  • 1,034
  • 1
  • 9
  • 19
  • Here is a pattern "break apart & simplify"... if you want help with this then provide complete runnable code. – Asperi May 10 '20 at 04:06
  • added. yes. break apart and simplify is the intent here. Its the mechanics of such that is failing me. – Scott May 10 '20 at 04:40

1 Answers1

0

Here is a demo of possible way (eg. of first breaking iteration, next possible - to separate paths generation, and so on)

private func headerView(in reader: GeometryProxy) -> some View {
    Text("Response (ms) vs Time of Day")
        .frame(width: reader.size.width, height: self.xAxisOffset, alignment: .center)
        .font(.largeTitle)
}

private func footerView(in reader: GeometryProxy) -> some View {
    ForEach(0..<self.historicalDataManager.todaysData.count) { dataIndex in
        Group {
            Text("\(dataIndex)")
        }
    }
}

private func verticalGrid(for line: Int, in reader: GeometryProxy) -> some View {
    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)

    }
}

private func horizontalGrid(for line: Int, in reader: GeometryProxy) -> some View {
    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))
    }
}

// ... and now - elegant body
var body: some View {
    GeometryReader { reader in
        self.headerView(in: reader)

        // draw the vertical grid lines
        ForEach(0..<self.numVerticalGridLinesPlusOne) { line in
            self.verticalGrid(for: line, in: reader)
        }
        // draw the horizontal grid lines
        ForEach(0..<self.numHorizontalGridLinesPlusOne) { line in
            self.horizontalGrid(for: line, in: reader)
        }

        // draw the graph signal
        self.footerView(in: reader)
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks. I was trying to create new struct GridLineView: View { var body : some view {}} vs having them built simply in a method. – Scott May 10 '20 at 06:01