SwiftUI Charts are very laggy when I am conditionally adding a rule mark

57 Views Asked by At

I have a cross hair on the chart implemented with RuleMark.

This causes the chart to be very laggy (it is updated every second with data).

I've tried various hacks but still laggy. When commenting out the rule mark the chart is a snappy 60FPS.

struct DataPoint: Identifiable { 
  let x: Date, 
  let y: Int, 
  var id: Date = {x} 
} 


struct MyView: View {
@State var xPosition : Date?
@State var dataPoints: [DataPoint] 

var body: some View {
  if let xPosition {
            Text("xPosition \(xPosition.description)")
   }
   Chart {
      ForEach(datapoints) { point in 
         LineMark(...point...)
      }
      if let xPosition {
         Plot {
                    RuleMark(x: .value("x", xPosition))
                        .foregroundStyle(.blue)
                }
      }
   }
   .chartXSelection(value: $xPosition)
  }
}

UPDATE: Swapped to using a gesture according to the example found here https://developer.apple.com/documentation/swiftui/view/chartoverlay(alignment:content:)

After experimentation with this method the problematic issue occurs when translating the x,y position of the gesture to the corresponding value of the element in the chart:

chartOverlay { proxy {
Rectangle().fill(.clear).contentShape(Rectangle()).gesture(
{
// omitting boiler plate code from apple example in the link

// Get the x (date) and y (amount) value from the location.
// this takes 30 - 50 ms  from my experimentation
                            if let (date, value) = proxy.value(at: location, as: (Date, Float64).self) {
                                xPosition = date
                                yPosition = value
                            } else {
                                xPosition = nil
                                yPosition = nil
                            }

Here is a minimal reproducible example using the chartX/Yselection :

import SwiftUI
import Charts
import Combine
import QuartzCore
import Foundation
struct ContentView: View {
    @State var data: DataPump = DataPump()
    @State var selectedX: Date? = nil
    @State var selectedY: Int?  = nil
    @State var crossHairs = true
    var body: some View {
        FrameRateView()
        Toggle("cross hair on", isOn: $crossHairs)
        if crossHairs {
            chart
            .chartXSelection(value: $selectedX)
            .chartYSelection(value: $selectedY)
        } else {
            chart
        }
    }
    @ViewBuilder
    var chart: some View {
        Chart {
            ForEach(Array(data.datapoints.enumerated()), id: \.offset) { i, point in
                LineMark(x: .value("time", point.date), y: .value("amount", point.value))
                RectangleMark(xStart: .value("", data.datapoints2[i].date.addingTimeInterval(-0.2)),
                              xEnd: .value("", data.datapoints2[i].date.addingTimeInterval(0.2)),
                              yStart: .value("", data.datapoints2[i].value - 3),
                              yEnd: .value("", data.datapoints2[i].value + 3)).foregroundStyle(.red)
            }
            if crossHairs, let selectedX, let selectedY {
                RuleMark(x: .value("X", selectedX))
                RuleMark(y: .value("Y", selectedY))
            }
        }
    }
}
@Observable class DataPump {
    var cancelable : Cancellable? = nil
    var datapoints: [DataPoint] = {
        let now = Date()
        var arr = [DataPoint]()
        for i in 0...180 {
            arr.append(DataPoint(date: now.addingTimeInterval(-Double(180 - i))))
        }
        return arr
    }()
    var datapoints2: [DataPoint] = {
        let now = Date()
        var arr = [DataPoint]()
        for i in 0...180 {
            arr.append(DataPoint(date: now.addingTimeInterval(-Double(180 - i))))
        }
        return arr
    }()
    init() {
        cancelable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink(receiveValue: { value in
                self.datapoints.append(DataPoint(date: value))
                self.datapoints2.append(DataPoint(date: value))
                if self.datapoints.count > 180 {
                    self.datapoints.removeFirst(self.datapoints.count - 180)
                    self.datapoints2.removeFirst(self.datapoints2.count - 180)
                }
        })
    }
}
struct DataPoint: Identifiable {
    let date: Date
    let value: Int = Int.random(in: 7...15)
    var id: Date { date }
}
struct FrameRateView: View {
    @State private var displayLink = DisplayLink()
    
    var body: some View {
        Text(String(format: "Frame Rate: %.0f FPS", displayLink.frameRate))
    }
}
@Observable class DisplayLink {
    var frameRate: Double = 0
    private var displayLink: CADisplayLink?
    private var lastTimeStamp: CFTimeInterval = 0
    private var cancellable: Cancellable?
    init() {
        self.displayLink = NSScreen.main?.displayLink(target: self, selector: #selector(updateFrameRate))
        self.displayLink?.add(to: .main, forMode: .common)
        self.lastTimeStamp = self.displayLink!.timestamp
    }
    @objc func updateFrameRate() {
        let currentTimeStamp = displayLink!.timestamp
        let elapsedTime = currentTimeStamp - lastTimeStamp
        lastTimeStamp = currentTimeStamp
        frameRate = 1 / elapsedTime
    }
}
0

There are 0 best solutions below