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

SwiftUI Chart LineMark异常:如何实现两条独立折线?

问题描述

我是iOS开发新手,目前从Firebase获取数据并处理为两个元组数组(后续将扩展为3个),每个数组对应一条折线,元组首个值对应X轴,每个元组代表折线的一个数据点。我使用SwiftUI Chart展示数据,但目前两条折线被错误连接为一条连续线,且数据已按X轴从小到大排序,希望实现两条独立折线。

当前折线显示效果如图:
折线异常效果

图表展示代码

Chart {
    ForEach(listOfEv.indices, id: \.self) { index in
        LineMark(
            x: .value("Time", Float(listOfEv[index].time)),
            y: .value("EV", Float(listOfEv[index].ev))
            
        ).foregroundStyle(
            .linearGradient(
                // You can have more colours here depends on your data.
                colors: [.blue, .red],
                startPoint: .bottom,
                endPoint: .top)
        )
        
        PointMark(
            x: .value("Time", Float(listOfEv[index].time)),
            y: .value("EV", Float(listOfEv[index].ev))
        )
        
    }
    
    ForEach(listOfAv.indices, id: \.self) { index1 in
        LineMark(
            x: .value("Time", Float(listOfAv[index1].time)),
            y: .value("EV", Float(listOfAv[index1].ev))
        ).foregroundStyle(.green)
        PointMark(
            x: .value("Time", Float(listOfAv[index1].time)),
            y: .value("EV", Float(listOfAv[index1].ev))
        )
    }
    
}.foregroundStyle(.green)
.frame(width: .infinity,height: 300)
.onAppear(perform: {
    firebaseData.BetTracker(username: "rozzaroo", completion: { EvData, AvData, SpData in
        
        listOfAv = AvData
        listOfSp = SpData
        listOfEv = EvData
        
    })
})

Firebase数据获取代码

func BetTracker(username: String, completion: @escaping ([(Int, Int)], [(Int, Int)], [(Int, Int)]) -> Void) {
    
    let ref = Database.database().reference().child("users").child(username.lowercased()).child("bets")
    
    ref.observeSingleEvent(of: .value) { snapshot, error in
        if let error = error {
            print("Error fetching odds list: \(error)")
            return
        }
        
        if let data = snapshot.value as? [String: Any] {
            let keysCount = data.keys.count
            var processedKeys = 0
            var EvCounter = 0
            var AvCounter = 0
            var time = 0
            
            let sortedKeys = data.keys.sorted {
                guard let first = data[$0] as? [Any], let second = data[$1] as? [Any],
                      let firstInt = first[1] as? Int, let secondInt = second[1] as? Int else { return false }
                return firstInt < secondInt
            }
            
            for key in sortedKeys {
                if let usersBetTrack = data[key] as? [Any] {
                    let refMaster = Database.database().reference().child("Master").child(key)
                    
                    refMaster.observeSingleEvent(of: .value) { caughtBet, error in
                        processedKeys += 1
                        
                        if let error = error {
                            print("Error fetching data: \(error)")
                            return
                        }
                        
                        if caughtBet.exists() {
                            if let array = caughtBet.value as? [AnyObject], let evMaster = array.first as? String,
                               let userBetAmtTrack = usersBetTrack[2] as? String,
                                
                                let betTimePlace1 = usersBetTrack[1] as? Int {
                                if let userBookOdd = usersBetTrack[3] as? String {
                                    var userBookOdds = Float(userBookOdd)
                                    
                                    
                                    var updatedEvMaster = Float(evMaster.dropLast())! / 100
                                    var dollarAmountBet = Float(userBetAmtTrack.dropFirst())!
                                    dollarAmountBet *= updatedEvMaster
                                    let currentTimeZone = TimeZone.current
                                    let gmtOffsetInSeconds = currentTimeZone.secondsFromGMT()
                                    var betTimePlace2 = betTimePlace1 + gmtOffsetInSeconds
                                    
                                    EvCounter = Int(dollarAmountBet) + EvCounter
                                    EvCounter = Int(EvCounter)
                                    
                                    time = 1 + time
                                    
                                    if let gameResult = array[12] as? String {
                                        
                                        switch (gameResult) {
                                        case "win":
                                            var usersStake = Float(userBetAmtTrack.dropFirst())
                                            usersStake = (userBookOdds! - 1) * usersStake!
                                            AvCounter = AvCounter + Int(Float(usersStake!))
                                            
                                        case "lose":
                                            AvCounter = AvCounter - Int(Float(userBetAmtTrack.dropFirst())!)
                                        case "pending":
                                            AvCounter = AvCounter + 0
                                        default:
                                            AvCounter = AvCounter + 0
                                        }
                                    }
                                    
                                    time = time + 1
                                    self.AvList.append((time: Int(time), ev: AvCounter))
                                    self.EvList.append((time: Int(time), ev: EvCounter))
                                    
                                    // Check if all keys have been processed, then call the completion handler
                                    if processedKeys == keysCount {
                                        
                                        completion(self.EvList, self.AvList, self.SpList)
                                    }
                                }
                            }
                        } else {
                            print("Snapshot does not exist")
                        }
                    }
                }
            }
        }
    }
}
解决方案

问题根源是你给每个数据点单独创建了LineMark,SwiftUI Chart会默认把所有未区分系列的LineMark连接成一条线。要实现独立折线,需要把同一系列的所有数据点归为一组,用.series()修饰符标记不同折线。

修改后的图表代码

Chart {
    // EV系列折线
    ForEach(listOfEv, id: \.time) { item in
        LineMark(
            x: .value("Time", Float(item.time)),
            y: .value("EV", Float(item.ev))
        )
        PointMark(
            x: .value("Time", Float(item.time)),
            y: .value("EV", Float(item.ev))
        )
    }
    .foregroundStyle(
        .linearGradient(
            colors: [.blue, .red],
            startPoint: .bottom,
            endPoint: .top
        )
    )
    .series(name: "EV")

    // AV系列折线
    ForEach(listOfAv, id: \.time) { item in
        LineMark(
            x: .value("Time", Float(item.time)),
            y: .value("EV", Float(item.ev))
        )
        PointMark(
            x: .value("Time", Float(item.time)),
            y: .value("EV", Float(item.ev))
        )
    }
    .foregroundStyle(.green)
    .series(name: "AV")
}
.frame(width: .infinity, height: 300)
.onAppear {
    firebaseData.BetTracker(username: "rozzaroo") { EvData, AvData, SpData in
        listOfAv = AvData
        listOfSp = SpData
        listOfEv = EvData
    }
}

关键修改说明

  1. 将每个折线的LineMark和PointMark放在同一个ForEach循环后,用.series()标记系列,Chart会自动把同系列的点连接成独立折线。
  2. 直接遍历数据元素而非索引,代码更简洁,避免索引相关的潜在bug。
  3. 移除外层的.foregroundStyle(.green),防止覆盖系列自定义样式。

额外优化建议

建议把元组替换成自定义Identifiable结构体,简化ForEach的使用:

struct ChartDataPoint: Identifiable {
    let id = UUID()
    let time: Int
    let ev: Int
}

之后数据数组可以直接用[ChartDataPoint]类型,ForEach无需手动指定id参数,更符合SwiftUI最佳实践。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 15:17:36