iosswiftswiftui

Pulsing dot position gets changed by scroll view


I am making a stock chart graph and I want the very last data point to have a pulsing dot above it. Currently when the view appears, the pulsing dot is lined up with the last data point.

When I scroll through the scroll view and scroll back up the dots appear somewhere else on the screen or nowhere at all. Some still have the dots and others don't, why does this happen when I scroll?

struct AllView: View {
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<25){ _ in
                    LineChartView(dataPoints: (0..<100).map { _ in Double.random(in: 0.0...1.0) }, profit: true)
                }
            }
        }
    }
}
struct LineChartView: View {
    let dataPoints: [Double]
    let profit: Bool
    
    var body: some View {
        GeometryReader { geometry in
            let minY = dataPoints.min() ?? 0
            let maxY = dataPoints.max() ?? 1
            ZStack {
                Path { path in
                    for (index, dataPoint) in dataPoints.enumerated() {
                        let x = CGFloat(index) * (geometry.size.width / CGFloat(dataPoints.count - 1))
                        let y = geometry.size.height - (CGFloat((dataPoint - minY) / (maxY - minY)) * geometry.size.height)
                        
                        if index == 0 {
                            path.move(to: CGPoint(x: x, y: y))
                        } else {
                            path.addLine(to: CGPoint(x: x, y: y))
                        }
                    }
                }.stroke(profit ? Color.green : Color.red, lineWidth: 1)
                if let last = dataPoints.last {
                    let x = CGFloat(dataPoints.count - 1) * (geometry.size.width / CGFloat(dataPoints.count - 1))
                    let y = geometry.size.height - (CGFloat((last - minY) / (maxY - minY)) * geometry.size.height)
                    PulsingView(size: 50, green: profit).position(x: x, y: y)
                }
            }
        }
    }
}

struct PulsingView: View {
    @State var animate = false
    let size: CGFloat
    let green: Bool
    var body: some View {
        ZStack {
            Circle()
                .fill(green ? Color.green.opacity(0.65) : Color.red.opacity(0.65))
                .frame(width: size, height: size)
                .scaleEffect(self.animate ? 1 : 0)
                .opacity(animate ? 0 : 1)
            
            Circle()
                .fill(green ? Color.green : Color.red)
                .frame(width: size / 6, height: size / 6)
        }
        .onAppear {
            self.animate.toggle()
        }
        .animation(.linear(duration: 1.5).repeatForever(autoreverses: false), value: animate)
    }
}

Solution

  • I couldn't reproduce this problem. But I suspect it might be happening because the lines are re-created after they have been scrolled off the screen and then scrolled back. This might mean, .onAppear is being called again and setting the animate flag to false. Also, since you are building the lines using random data, this might explain why the pulsing points are in the wrong position when the lines are re-created.

    struct AllView: View {
        typealias DataPoints = [Double]
        let allDataPoints: [DataPoints]
    
        init() {
            var allDataPoints = [DataPoints]()
            for _ in 0..<25 {
                allDataPoints.append(
                    (0..<100).map { _ in Double.random(in: 0.0...1.0) }
                )
            }
            self.allDataPoints = allDataPoints
        }
    
        var body: some View {
            ScrollView {
                VStack {
                    ForEach(Array(allDataPoints.enumerated()), id: \.offset) { index, dataPoints in
                        LineChartView(dataPoints: dataPoints, profit: true)
                    }
                }
            }
        }
    }
    
    .onDisappear {
        animate = false
    }