Creating a live chart for real time ble data

Hi All,

Please excuse my relatively basic question but I am new to swift programming and I am battling with a project.

I currently have an app that receives data from an Arduino using ble and displays the data as an integer. I used this medium article From Arduino programming to iOS App development as a guide for most of the functionality but changed the sensor data being sent to better suit my project requirements.

Based on the link above, I have all of the bluetooth handling in PeripheralUseCase.swift file and then I have the ConnectView file for the display:

    
    @ObservedObject var viewModel: ConnectViewModel
    
    @Environment(\.dismiss) var dismiss
    
    @State var isToggleOn: Bool = false
    @State var isPeripheralReady: Bool = false
    @State var lastPressure: Int = 0

    var body: some View {
        VStack {
            Text(viewModel.connectedPeripheral.name ?? "Unknown")
                .font(.title)
            ZStack {
                CardView()
                VStack {
                    Text("Surface")
                    HStack {
                        
                        Button("Flats") {
                            viewModel.flats()
                        }
                        .disabled(!isPeripheralReady)
                        .buttonStyle(.borderedProminent)
                        Button("FlatPoint") {
                            viewModel.flatPoint()
                        }
                        .disabled(!isPeripheralReady)
                        .buttonStyle(.borderedProminent)
                        Button("Points") {
                            viewModel.points()
                        }
                        .disabled(!isPeripheralReady)
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
            ZStack {
                CardView()
                VStack {
                    Text("\(lastPressure) kPa")
                        .font(.largeTitle)
                    HStack {
                        Spacer()
                            .frame(alignment: .trailing)
                        Toggle("Notify", isOn: $isToggleOn)
                            .disabled(!isPeripheralReady)
                        Spacer()
                            .frame(alignment: .trailing)
                    }
                }
            }
           
            Spacer()
                .frame(maxHeight:.infinity)
            Button {
                dismiss()
            } label: {
                Text("Disconnect")
                    .frame(maxWidth: .infinity)
            }
            .buttonStyle(.borderedProminent)
            .padding(.horizontal)
        }
        .onChange(of: isToggleOn) { newValue in
            if newValue == true {
                viewModel.startNotifyPressure()
            } else {
                viewModel.stopNotifyPressure()
            }
            let startTime = Date().timeIntervalSince1970
        }
        .onReceive(viewModel.$state) { state in
            switch state {
            case .ready:
                isPeripheralReady = true
            case let .Pressure(temp):
                lastPressure = temp
            default:
                print("Not handled")
            }
        }
    }
}

struct PeripheralView_Previews: PreviewProvider {
    
    final class FakeUseCase: PeripheralUseCaseProtocol {
        
        var peripheral: Peripheral?
        
        var onWriteLedState: ((Bool) -> Void)?
        var onReadPressure: ((Int) -> Void)?
        var onPeripheralReady: (() -> Void)?
        var onError: ((Error) -> Void)?

        func writeLedState(isOn: String) {}
        
        func readPressure() {
            onReadPressure?(25)
        }
        
        func notifyPressure(_ isOn: Bool) {}
    }
    
    static var viewModel = {
        ConnectViewModel(useCase: FakeUseCase(),
                            connectedPeripheral: .init(name: "iOSArduinoBoard"))
    }()
    
    
    static var previews: some View {
        ConnectView(viewModel: viewModel, isPeripheralReady: true)
    }
}

struct CardView: View {
  var body: some View {
    RoundedRectangle(cornerRadius: 16, style: .continuous)
      .shadow(color: Color(white: 0.5, opacity: 0.2), radius: 6)
      .foregroundColor(.init(uiColor: .secondarySystemBackground))
  }
}

With the associated View Model:

    @Published var state = State.idle
    
    var useCase: PeripheralUseCaseProtocol
    let connectedPeripheral: Peripheral
    
    init(useCase: PeripheralUseCaseProtocol,
         connectedPeripheral: Peripheral) {
        self.useCase = useCase
        self.useCase.peripheral = connectedPeripheral
        self.connectedPeripheral = connectedPeripheral
        self.setCallbacks()
    }
    
    private func setCallbacks() {
        useCase.onPeripheralReady = { [weak self] in
            self?.state = .ready
        }
        
        useCase.onReadPressure = { [weak self] value in
            self?.state = .Pressure(value)
        }
        
        useCase.onWriteLedState = { [weak self] value in
            self?.state = .ledState(value)
        }
        
        useCase.onError = { error in
            print("Error \(error)")
        }
    }
    
    func startNotifyPressure() {
        useCase.notifyPressure(true)
    }
    
    func stopNotifyPressure() {
        useCase.notifyPressure(false)
    }
    
    func readPressure() {
        useCase.readPressure()
    }
    
    func flats() {
        useCase.writeLedState(isOn: "1")
    }
    
    func flatPoint() {
        useCase.writeLedState(isOn: "2")
    }
    func points() {
        useCase.writeLedState(isOn: "3")
    }
}

extension ConnectViewModel {
    enum State {
        case idle
        case ready
        case Pressure(Int)
        case ledState(Bool)
    }
}

What I am now trying to do is plot the data that is received from the Arduino in a line graph as it is received. Preferably the graph will scroll with time as well.

Replies

Just an update on this.

I have been reading several different articles about using Charts in Swift and have updated my ConnectView as shown below: `struct ConnectView: View {

@ObservedObject var viewModel: ConnectViewModel

@Environment(\.dismiss) var dismiss

@State var isToggleOn: Bool = false
@State var isPeripheralReady: Bool = false
@State var lastPressure: Int = 0
public  var startTime: Double = 0
public var nowTime: Double = 0
@State var graphData = [GraphData]()

var body: some View {
    VStack {
        Text(viewModel.connectedPeripheral.name ?? "Unknown")
            .font(.title)
        ZStack {
            CardView()
            VStack {
                Text("Surface")
                HStack {
                    
                    Button("Flats") {
                        viewModel.flats()
                    }
                    .disabled(!isPeripheralReady)
                    .buttonStyle(.borderedProminent)
                    Button("FlatPoint") {
                        viewModel.flatPoint()
                    }
                    .disabled(!isPeripheralReady)
                    .buttonStyle(.borderedProminent)
                    Button("Points") {
                        viewModel.points()
                    }
                    .disabled(!isPeripheralReady)
                    .buttonStyle(.borderedProminent)
                }
            }
        }
        ZStack {
            CardView()
            VStack {
                Text("\(lastPressure) kPa")
                    .font(.largeTitle)
                HStack {
                    Spacer()
                        .frame(alignment: .trailing)
                    Toggle("Notify", isOn: $isToggleOn)
                        .disabled(!isPeripheralReady)
                    Spacer()
                        .frame(alignment: .trailing)
                }
            }
        }
        ZStack{
           CardView()
            Chart(graphData) {
                LineMark(x: .value("Time", $0.time),
                         y: .value("Load", $0.load))
            }
        }
        .padding()
            .frame(maxHeight: .infinity)
        Button {
            dismiss()
        } label: {
            Text("Disconnect")
                .frame(maxWidth: .infinity)
        }
        .buttonStyle(.borderedProminent)
        .padding(.horizontal)
    }
    .onChange(of: isToggleOn) { newValue in
        if newValue == true {
            viewModel.startNotifyPressure()
        } else {
            viewModel.stopNotifyPressure()
        }
     //   let startTime = Date().timeIntervalSince1970
    }
    .onReceive(viewModel.$state) { state in
        switch state {
        case .ready:
            isPeripheralReady = true
        case let .Pressure(temp):
            lastPressure = temp
  //         let nowTime = (Date().timeIntervalSince1970 - startTime)
            let newDataPoint = GraphData(time: nowTime, load: lastPressure)
            graphData.append(newDataPoint)
            let nowTime = nowTime+1
        default:
            print("Not handled")
        }
    }
}

With the Struct for my graph data below:

struct GraphData: Identifiable {
    var time: Double
    var load: Int
    var id: Double {time}
}

This gives me the graph where I want it but the issue now is that it only plots the first data point it receives and does not update as new data comes in. I am also battling with getting the time on the x-axis.