2
\$\begingroup\$

I have a motionManager class which uses CMMotionManager() to send rotationRate and attitude values to a publisher in the form of a MotionValueModel.

final class MotionManager: MotionManagerProtocol {
    
    private let manager = CMMotionManager()
    
    init() {
        getMotionData()
    }
    
    let motionPublisher = PassthroughSubject<MotionValueModel, Error>()
    
    internal func getMotionData() {
        self.manager.deviceMotionUpdateInterval = 1.0
        self.manager.startDeviceMotionUpdates(to: .main) { [weak self] (data, error) in
            guard let self = self else { return }
            if let error = error {
                self.motionPublisher.send(completion: .failure(error))
            } else {
                guard let data = data else { return }
                let capturedValues: MotionValueModel = MotionValueModel(rotationX: data.rotationRate.x, rotationY: data.rotationRate.y, rotationZ: data.rotationRate.z, pitch: data.attitude.pitch, roll: data.attitude.roll, yaw: data.attitude.yaw)
                self.motionPublisher.send(capturedValues)
            }
        }
    }
    
    func stopUpdates() {
        manager.stopDeviceMotionUpdates()
        motionPublisher.send(MotionValueModel.example)
        motionPublisher.send(completion: .finished)
    }
}

I'm subscribing to the Publisher from within a viewModel and before sinking the values and assigning them to another publisher I want to map the data from the motionValueModel and transform them ultimately to a tuple of type (axis: [Double], angle: [Double]).

To achieve this I first create 6 arrays of in total 5 items (important!), one array for every property of my MotionValueModel. These arrays will be update every second by the values published by the model. The first values of the array will be deleted array.removeFirst(1)and the new value will be appended array.append(newValue)

Arrays can be created like this:

let rotationXArray = Array(repeating: 0.0, count: 5)
let rotationYArray = Array(repeating: 0.0, count: 5)
let rotationZArray = Array(repeating: 0.0, count: 5)

And values can be updated by using an extension of Array:

extension Array where Element: Equatable {
    mutating func update(with value: Element) {
        if self.isEmpty {
            return
        } else {
            self.removeFirst(1)
            self.append(value)
        }
    }
}

Example: rotationXArray.update(with: NewValue)

When the incoming values for example are: 1,2,3,4,5,6,n The array should be constructed like:

[0,0,0,0,0] // Initial value
[0,0,0,0,1] // Second 1
[0,0,0,1,2] // Second 2
[0,0,1,2,3] // Second 3
[0,1,2,3,4] // Second 4
[1,2,3,4,5] // Second 5
[2,3,4,5,6] // Second 6
[3,4,5,6,n] // Second n

The 3 arrays for the angle data are then stored in a parent array called AxisArray, the 3 arrays for the attitude data are stored in a parent array called AngleData. These are both 2D arrays containing each 3 arrays we previously created.

In my app, the parent and child arrays are created at the same time by code:

var axisArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)
var angleArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)

After this I use flatmap to merge the 3 child arrays into 1 big parent array:

let axisCombined = axisArray.flatMap { $0 }
let angleCombined = angleArray.flatMap { $0 }

Ander return them in a tuple: return (axisCombined, angleCombined)

Now, everything is working, but I'm not sure if I'm taking the right approach by constructing the axisArray and angleArray outside of my subscriber and importing them into my map function. Maybe I'm taking a big detour here and an easier approach is existing. The makeMotionTuple function is just made for this specific situation, not reusable at all. Is there any better approach ? Any possibility of creating and storing the axisArray and AngleArray inside of map function ?

The code I have now is following:

typealias MotionTuple = (axis: [Double], angle: [Double])

final class someViewModel: ObservableObject {

    let motionManager =  MotionManager()
    private var subscription: AnyCancellable?

    let tuplePublisher = PassthroughSubject<MotionTuple, Error>()
    
    init() {
        doYourThing()
    }

    private func doYourThing() {

        // The arrays are created outside of the subscription to keep them alive.    
        var axisArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)
        var angleArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)
        
        subscription = motionManager.motionPublisher
            .receive(on: DispatchQueue.global(qos: .default)) //Background thread
            .map({ makeMotionTuple(from: &axisArray, and: &angleArray, with: $0) })
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished: break
                case .failure(let error): tuplePublisher.send(error)
                }
            }, receiveValue: { returnedTuple in
                tuplePublisher.send(returnedTuple)
            })
        
        // Nested func which creates the tuple. I'm using an inout parameter to pass in and change the arrays. 
        func makeMotionTuple(from array1: inout [[Double]], and array2: inout [[Double]], with input: MotionValueModel) -> MotionTuple {
            array1[0].update(with: input.rotationX)
            array1[1].update(with: input.rotationY)
            array1[2].update(with: input.rotationZ)
            array2[0].update(with: input.pitch)
            array2[1].update(with: input.roll)
            array2[2].update(with: input.yaw)
            let axisCombined = array1.flatMap { $0 }
            let angleCombined = array2.flatMap { $0 }
            return (axisCombined, angleCombined)
            
        }
    }

    func stopUpdates() {
        motionManager.stopUpdates()
        subscription?.cancel()
    }
}
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

First, one should avoid map with side effects. It is solely for taking a value and transforming it to another value. I would perform any model updates you want in the sink:

private func doYourThing() {
    var axisArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)
    var angleArray = Array(repeating: Array(repeating: 0.0, count: 5), count: 3)

    subscription = motionManager.motionPublisher
        .receive(on: DispatchQueue.global(qos: .default))  // Background thread ... do you really want a concurrent queue here?!
        .sink { completion in
            switch completion {
            case .finished: break                          // shouldn't we finish `tuplePublisher` on the successful finish, too? Why only on error?
            case .failure(let error): self.tuplePublisher.send(completion: .failure(error))
            }
        } receiveValue: { motion in
            updateModel(from: &axisArray, and: &angleArray, with: motion)
            publishTuple(from: axisArray, and: angleArray)
        }

    func updateModel(from axisArray: inout [[Double]], and angleArray: inout [[Double]], with input: MotionValueModel) {
        axisArray[0].update(with: input.rotationX)
        axisArray[1].update(with: input.rotationY)
        axisArray[2].update(with: input.rotationZ)
        angleArray[0].update(with: input.pitch)
        angleArray[1].update(with: input.roll)
        angleArray[2].update(with: input.yaw)
    }

    func publishTuple(from axisArray: [[Double]], and angleArray: [[Double]]) {
        let axisCombined = axisArray.flatMap { $0 }
        let angleCombined = angleArray.flatMap { $0 }
        tuplePublisher.send((axisCombined, angleCombined))
    }
}

If you are open to a more significant observation, I would suggest retiring the [[Double]] (which is a bit opaque), and just deal [MotionValueModel]. E.g.,

let motionsPublisher = PassthroughSubject<[MotionValueModel], Error>()

private func doYourThing() {
    var motions: [MotionValueModel] = Array(repeating: .zero, count: 5) // personally, I'd start with `[]`, but this is as you requested.

    let backgroundQueue = DispatchQueue(label: "myqueue")               // if you want a background queue, fine, but I'd encourage you to use a serial queue

    subscription = motionManager.motionPublisher
        .receive(on: backgroundQueue)
        .sink { completion in
            switch completion {
            case .finished:           self.motionsPublisher.send(completion: .finished)
            case .failure(let error): self.motionsPublisher.send(completion: .failure(error))
            }
        } receiveValue: { motion in
            motions.update(with: motion)
            self.motionsPublisher.send(motions)
        }
}

Also, if you really want to repopulate the array with the “zero” motions, like above, then you would need this:

extension MotionValueModel {
    static let zero = MotionValueModel(rotationX: 0, rotationY: 0, rotationZ: 0, pitch: 0, roll: 0, yaw: 0)
}

It seems that a publisher of [MotionValueModel] is more intuitive. And if you want an array of the rotationX values, it would be:

let rotationXs = motions.map(\.rotationX)

I would also suggest that update should work regardless of whether the array was empty or not:

extension Array {
    mutating func update(with value: Element, maxCount: Int = 5) {
        removeFirst(Swift.max(0, count - maxCount + 1))
        append(value)
    }
}

Perhaps rotate or shift might be a better name than update, which is a little generic.

Personally, I would rename MotionValueModel to just be Motion or something, but that is up to you.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.