Cómo mostrar los datos del acelerómetro de un iPhone en SwiftUI Charts
Cómo mostrar los datos del acelerómetro de un iPhone en SwiftUI Charts

Swift Charts y Acelerómetro

Extraer datos del acelerómetro del iPhone (o de cualquier otro dispositivo), es muy sencillo. Tan solo debemos importar el framework CoreMotion y crear una instancia de CMMotionManager. Aquí especificamos cada cuanto queremos obtener una muestra y de esta manera la printamos en un Chart en SwiftUI

SwiftBeta

Tabla de contenido

Aprende a mostrar los datos del acelerómetro del iPhone en un Chart en SwiftUI

Hoy en SwiftBeta vamos a ver Swift Charts que ya vimos en el anterior video y esta vez en lugar de representar datos estáticos, vamos a representar datos en real time. Para ser más concreto, vamos a obtener información del acelerómetro del iPhone.
Pero ¿qué es un acelerómetro? tu dispositivo, en este caso un iPhone está compuesto por una barbaridad de tecnología, y hay un componente que se encarga de obtener información de la fuerza de aceleración o gravedad que tiene tu iphone en los distintos ejes: X, Y y Z. Si coges el iPhone y lo sacudes de golpe, verás que depende en el eje que lo hayas sacudido hay un valor más alto.
Es importante destacar que el código que implementemos deberá compilarse en un dispositivo físico, es decir, un dispositivo real. Ya que no podemos probar el acelerómetro en un simulador.

Si quieres apoyar el canal y que siga creando este tipo de contenido, suscríbete al canal!

Creamos proyecto en Xcode

Lo primero de todo que vamos hacer es crear nuestro proyecto en Xcode, recuerda seleccionar la opción de interfaz SwiftUI (ya que es el framework de UI que vamos a utilizar).

Una vez creado el proyecto, vamos a crear un fichero nuevo, nuestro ViewModel. Dentro de este ViewModel vamos a crear la lógica que obtendrá los datos de los diferentes ejes del acelerómetro de nuestro iPhone.

Una vez creado nuestro ViewModel importamos CoreMotion, este Framework nos va a conseguir los datos del acelerómetro. Y a continuación vamos a crear una struct que nos servirá como modelo de los datos que vamos a ir guardando en memoria:

struct DataModel: Identifiable {
    let id: String = UUID().uuidString
    let axis: String
    var value: [Double]
    
    mutating func add(_ newValue: Double) {
        self.value.append(newValue)
    }
}
Tipo que nos servirá para diferenciar y almacenar valores en los distintos ejes del acelerómetro

Conformamos el protocolo Identifiable, ya que lo necesitaremos cuando usemos el ForEach en la vista. Y creamos dos propiedades, el eje en el que guardaremos el dato, que puede ser eje X, Y o Z. Y la última propiedad es un Array donde almacenaremos los valores del acelerómetro en ese eje.

Fíjate que también hemos creado un método que nos ayudará a añadir valores al array, es un método helper.

Vamos a continuar, dentro de nuestra clase ViewModel, vamos a crear una propiedad de tipo CMMotionManager. Este manager, este objeto nos obtendrá la información del acelerómetro. Vamos a crear también un init que llame al método que pondrá todo en marcha.

    private let motionManager = CMMotionManager()
    
    init() {
        startAccelerometer()
    }

    private func startAccelerometer() {
        // TODO:
    }
Instancia de CMMotionManager

Dentro del método startAccelerometer, vamos a meter la siguiente lógica:

    private func startAccelerometer() {
        guard motionManager.isAccelerometerAvailable else {	// 1
            print("Accelerometer data not available in this device")
            return
        }
        motionManager.accelerometerUpdateInterval = 1/60	// 2
        motionManager.startAccelerometerUpdates(to: .main) { accelerometerData, error in // 3
            guard let error = error else {
                self.updateProperties(with: accelerometerData) // 4
                return
            }
            print("Error \(error.localizedDescription)") // 5
        }
    }
Accedemos a los valores del acelerómetro del iPhone
  1. Aquí comprobamos que el device en el que estamos probando nuestra app tiene un acelerómetro disponible. Si no lo tiene omitimos cualquier lógica, ya que no tendría sentido.
  2. Asignamos un valor a la propiedad accelerometerUpdateInterval para obtener muestras del acelerómetro. Un valor más alto indicaría obtener menos muestras (y la gráfica se dibujaría más lentamente)
  3. Llamamos al método startAccelerometerUpdate aquí es donde está toda la magia, ya que este método se va a llamar indefinidamente y va a ir obteniendo información de los 3 ejes del acelerómetro. Si no tenemos error, pasamos esta información a un método que crearemos justo a continuación.
  4. Si tenemos un error lo printamos por consola

Ahora, el compilador se queja, debemos crear el método updateProperties, pero anter vamos a crear 4 propiedades. Esta propiedas nos van a servir para almacenar los valores de cada eje del acelerómetro, y refrecar nuestro Chart con la nueva información:

    @Published var data: [DataModel] = []
    
    private var x: DataModel = .init(axis: "x", value: [])
    private var y: DataModel = .init(axis: "y", value: [])
    private var z: DataModel = .init(axis: "z", value: [])
Propiedades de nuestro ViewModel

Una vez hemos creado las propiedades, vamos a crear el método updateProperties del que se queja el compilador de Xcode:

    private func updateProperties(with accelerometerData: CMAccelerometerData?) {
        if let data = motionManager.accelerometerData {
            let (x, y, z) = (data.acceleration.x, data.acceleration.y, data.acceleration.z)
            
            self.x.add(x)
            self.y.add(y)
            self.z.add(z)
            
            self.data = [self.x, self.y, self.z]
            
            print("Eje X \(self.x)")
            print("Eje Y \(self.y)")
            print("Eje Z \(self.z)")
        }
    }
Cada valor recibido de nuestro acelerómetro lo actualizamos en las propiedades del ViewModel
  • Este método es muy sencillo, accede a los valores de la aceleración en cada eje
  • Añade el valor de cada eje en las propiedades que acabamos de crear
  • Añade los valores en el Array data de tipo @Published. Cada cambio en esta propiedad hará que se dibuje en nuestro Chart en SwiftUI.
  • He añadido unos printas para que cuando creemos la instancia de ViewModel, veas por consola los valores del acelerómetro.

Empezamos a monitorizar valores del acelerómetro

Ahora vamos a nuestro ContentView y vamos a crear una instancia de ViewModel.

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
    }
}
Creamos una instancia de nuestro ViewModel

Al compilar, deberás ver por consola un montón de valores de los 3 ejes del acelerómetro. Ahora toca crear la parte visual, vamos a crear un Chart para representar estos datos de una manera que seamos capaces de entender qué ocurre.

import Charts

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Label("Acelerómetro del iPhone", systemImage: "sensor.fill")
            .font(.system(size: 20, weight: .bold, design: .rounded))
        Chart(viewModel.data) { data in
            ForEach(Array(data.value.enumerated()), id: \.offset) { index, element in
                LineMark(x: .value("Index", index),
                         y: .value("Value", element))
            }
            .foregroundStyle(by: .value("Axis", data.axis))
        }
        .frame(height: 400)
    }
}
Chart para dibujar los valores del acelerómetro

Y si compilamos vemos como a medida que obtenemos datos del acelerómetro, aparecen en los distintos ejes, cada uno con su color y valor.

Valores del acelerómetro representados en un iPhone
Valores del acelerómetro representados en un iPhone

Reset de los datos del acelerómetro

Ahora vamos a crear un Button para que cuando lo pulsemos, todos los valores almacenados en memoria sean liberados.

Justo debajo de nuestro Chart añadimos el Button:

import Charts

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Label("Acelerómetro del iPhone", systemImage: "sensor.fill")
            .font(.system(size: 20, weight: .bold, design: .rounded))
        Chart(viewModel.data) { data in
            ForEach(Array(data.value.enumerated()), id: \.offset) { index, element in
                LineMark(x: .value("Index", index),
                         y: .value("Value", element))
            }
            .foregroundStyle(by: .value("Axis", data.axis))
        }
        .frame(height: 400)
        Button("Reset", action: {
            viewModel.removeData()
        })
        .foregroundColor(.green)
        .buttonStyle(.bordered)
    }
}
Añadimos un Button para resetear los valores de nuestro Chart

Ahora vamos a nuestro ViewModel para crear el método removeData:

    func removeData() {
        data.removeAll()
        x = .init(axis: "x", value: [])
        y = .init(axis: "y", value: [])
        z = .init(axis: "z", value: [])
    }
Borramos el contenido de la propiedad data y de los 3 ejes (X, Y, y Z)

Ahora si compilamos vemos como al pulsar el Button la información de nuestro Chart se resetea.

Y por seguridad, podéis añadir una condición en el método updateProperties para que llegado un gran número de muestras, se resetee automáticamente, es decir:

    private func updateProperties(with accelerometerData: CMAccelerometerData?) {
        if let data = motionManager.accelerometerData {
            let (x, y, z) = (data.acceleration.x, data.acceleration.y, data.acceleration.z)
            
            self.x.add(x)
            self.y.add(y)
            self.z.add(z)
            
            if self.x.value.count > 300 {
                removeData()
            } else {
                self.data = [self.x, self.y, self.z]
            }
        }
    }

Conclusión

Hoy hemos aprendido a extraer información de nuestro dispositivo. En este caso ha sido un iPhone. Y la información que hemos extraido ha sido del acelerómetro.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple