You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI Charts添加RuleMark实现十字线时严重卡顿问题求助

问题:SwiftUI Charts中RuleMark十字线导致实时更新图表卡顿

我在SwiftUI Charts里用RuleMark实现了十字线功能,图表每秒更新一次数据,但开启十字线后出现严重卡顿。试过几种临时方案都没解决,注释掉RuleMark后图表就能回到流畅的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)
    }
}

尝试官方手势方案后的问题

我改用了官方的手势方案,但调用proxy.value方法将手势坐标转换为图表对应值时,耗时高达30-50ms,依然存在性能问题:

chartOverlay { proxy in
    Rectangle().fill(.clear).contentShape(Rectangle()).gesture(
        // 省略官方示例中的模板代码
        
        // 将坐标转换为图表的日期和数值,此步骤耗时30-50ms
        if let (date, value) = proxy.value(at: location, as: (Date, Float64).self) {
            xPosition = date
            yPosition = value
        } else {
            xPosition = nil
            yPosition = nil
        }
    )
}

最小复现代码

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("显示十字线", 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: "帧率: %.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
    }
}

内容的提问来源于stack exchange,提问作者Avba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 22:12:11