Search code examples
swiftnetwork-programmingbuffersensorscombine

How to use Combine to send async accelerometer updates to server


My goal is to send async accelerometer readings to a server in periodic payloads.

Accelerometer data continues while offline and concurrently during the network requests, so I'll need to handle network failures as well as data that arrives during the duration of each network request.

My inelegant approach is to append each new update to an array:

motionManager.startAccelerometerUpdates(to: .main) { data, error in
    dataArray.append(data)
}

And then periodically send a group of values to the server (network is my wrapper around NWPathMonitor()):

let timer = Timer(fire: Date(), interval: 5, // Every 5 seconds
              repeats: true, block: { timer in
                if network.connected {
                    postAccelerometerData(payload: dataArray) { success
                        if success {
                            dataArray.removeAll()
                        }
                    }
                }
            })


RunLoop.current.add(timer, forMode: RunLoop.Mode.default)

The major issue with this approach is that the elements of the array added between when the network request fires and when it completes would be removed from the array without ever being sent to the server.

I've had some ideas about adding a queue and dequeuing X elements on for each network request (but then do I add them back to the queue if the request fails?).

I can't help but think there is a better way to approach this using Combine to "stream" these accelerometer updates to some sort of data structure to buffer them, and then send those on to a server.

The postAccelerometerData() function just encodes a JSON structure and makes the network request. Nothing particularly special there.


Solution

  • Combine has a way to collect values for a certain amount of time and emit an array. So, you could orchestrate your solution around that approach, by using a PassthroughSubject to send each value, and the .collect operator with byTime strategy to collect the values into an array.

    let accelerometerData = PassthroughSubject<CMAccelerometerData, Never>()
    
    motionManager.startAccelerometerUpdates(to: .main) { data, error in
       guard let data = data else { return } // for demo purposes, ignoring errors
       accelerometerData.send(data)
    }
    
    // set up a pipeline that periodically sends data to the server
    accelerometerData
       .collect(.byTime(DispatchQueue.main, .seconds(5))) // collect for 5 sec
       .sink { dataArray in
           // send to server
           postAccelerometerData(payload: dataArray) { success in
               print("success:", success)
           }
       }
       .store(in: &cancellables) 
    

    The above is a simplified example - it doesn't handle accelerometer errors or network errors - because it seems to be beyond what you're asking in this question. But if you need to handle network errors and, say, retry - then you could wrap postAccelerometerData in a Future and integrate it into the Combine pipeline.