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.
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.