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.

    • To resolve the wrong position, try preparing the random data sets in advance and then make sure you always use the same set of data points for each line plot. An array of arrays can be used for this:
    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)
                    }
                }
            }
        }
    }
    
    • To resolve the missing pulsing dots, try re-setting the animate flag when the line goes out of view by adding an .onDisappear callback to PulsingView:
    .onDisappear {
        animate = false
    }