Aprende a crear una app de contador de pasos en tu iPhone y Apple Watch
Aprende a crear una app de contador de pasos en tu iPhone y Apple Watch

¿Cómo crear tu primera app en iPhone y Apple Watch?

Aprende a crear tu primera app para tu Apple Watch. Aprendemos a crear un contador de pasos usando el framework HealthKit. Crea una app iOS y reutiliza tu código en tu app watchOS, lo único que necesitas es crear un módulo (con Swift Package Manager) y compartir la lógica y vistas.

SwiftBeta

Tabla de contenido

Aprende a crear una app de contador de pasos en tu iPhone y Apple Watch

Hoy en SwiftBeta vamos a ver un post muy potente. En el sentido de que vamos a crear una app muy sencilla para nuestro iPhone que nos muestre los pasos que hemos realizado durante el día. Y lo siguiente que haremos será crear la misma app en nuestro Apple Watch. Para hacerlo crearemos un módulo con Swift Package  Manager y extraeremos la lógica utilizada en nuestra app para iPhone y así la reutilizaremos en nuestro Apple Watch. ¿El beneficio de esto? reusaremos la misma lógica en lugar de duplicar código.

Creamos nuestra iOS app

Abrimos Xcode y creamos una app iOS.

Creamos una app iOS en Xcode
Creamos una app iOS en Xcode

Le ponemos nombre, en mi caso la voy a llamar ContadorDePasos

En Xcode añadimos el nombre de nuestra app
En Xcode añadimos el nombre de nuestra app

Añadimos HealthKit en Capabilities

Lo primero de todo que vamos hacer es irnos a la sección de Signing & Capabilities y allí vamos a añadir HealthKit. (Es necesario hacer este paso para poder obtener la información)

Añadimos Healhkit como capabilities de nuestra app
Añadimos Healhkit como capabilities de nuestra app

Una vez añadido ya podemos empezar a escribir código!

Creamos el ViewModel

Lo siguiente que vamos hacer es crear un fichero nuevo, vamos a crear un ViewModel y vamos a añadir la siguiente lógica:

import Foundation
import HealthKit

final class ViewModel: ObservableObject {
    private let healthStore = HKHealthStore()
        
    func requestAccessToHealthData() {
        let readableTypes: Set<HKSampleType> = [HKQuantityType.quantityType(forIdentifier: .stepCount)!]
        
        guard HKHealthStore.isHealthDataAvailable() else {
            return
        }
        
        healthStore.requestAuthorization(toShare: nil, read: readableTypes) { success, error in
            print("Request Authorization \(success.description)")
        }
    }
}
Código Swift para solicitar acceso al user sobre sus datos
  1. Hemos importado el framework HealthKit. Lo necesitamos para poder extraer los pasos realizados durante un período de tiempo concreto. En nuestro caso vamos a sacar los pasos realizados durante un día.
  2. Tenemos que pedir acceso al user para que nos permita obtener esta información. Es por eso que si tenemos datos disponibles, lo que haremos será pedir autorización a un user para indicar que queremos acceder al número de pasos. Aquí podríamos acceder a otro tipo de información (como distancia que hemos caminado, nuestro peso, ritmo cardiaco, etc), pero en nuestro caso solo accederemos al número de pasos.

Una vez tenemos este código implementado, queremos que cuando un user accepta la autorización, se obtenga esta información, por lo tanto vamos a crear un método que será llamado desde dentro del closure de requestAuthorization

Lo siguiente que vamos hacer es crear este método, va a ser el encargado de escuchar cambios en los datos de nuestros pasos, cuando hagamos más pasos este método tendrá una lógica para refrescar nuestra información. Nuestro código quedaría de la siguiente manera:

import Foundation
import HealthKit

final class ViewModel: ObservableObject {
    private let healthStore = HKHealthStore()
    private var observerQuery: HKObserverQuery?
        
    func requestAccessToHealthData() {
        let readableTypes: Set<HKSampleType> = [HKQuantityType.quantityType(forIdentifier: .stepCount)!]
        
        guard HKHealthStore.isHealthDataAvailable() else {
            return
        }
        
        healthStore.requestAuthorization(toShare: nil, read: readableTypes) { success, error in
            print("Request Authorization \(success.description)")
            if success {
                self.getTodaySteps()
            }
        }
    }
    
    public func getTodaySteps() {
        guard let stepCountType = HKObjectType.quantityType(forIdentifier: .stepCount) else {
            fatalError("Error Identifier .stepCount")
        }
        
        observerQuery = HKObserverQuery(sampleType: stepCountType, predicate: nil) { (query, completionHandler, errorOrNil) in
            
            if let error = errorOrNil {
                print("Error: \(error.localizedDescription)")
                return
            }
                
            // TODO: We will call getMySteps() method
        }
        
        observerQuery.map(healthStore.execute)
    }
}
Creamos HKObserverQuery para escuchar cambios en nuestros pasos

Hemos creado un método nuevo con un HKObserverQuery que trigeará el contenido de su closure cada vez que se detecten nuevos pasos. Y cada vez que se tengamos nuevos datos, llamaremos a un nuevo método lo vamos a llamar getMySteps()

@Published public var allMySteps: String = "0"
private var query: HKStatisticsQuery?


private func getMySteps() {
        let stepsQuantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
        
        let now = Date()
        let startOfDay = Calendar.current.startOfDay(for: now)
        let predicate = HKQuery.predicateForSamples(
            withStart: startOfDay,
            end: now,
            options: .strictStartDate
        )
        
        self.query = HKStatisticsQuery(
            quantityType: stepsQuantityType,
            quantitySamplePredicate: predicate,
            options: .cumulativeSum
        ) { _, result, _ in
            guard let result = result, let sum = result.sumQuantity() else {
                DispatchQueue.main.async {
                    self.allMySteps = String(Int(0))
                }
                return
            }
            DispatchQueue.main.async {
                self.allMySteps = String(Int(sum.doubleValue(for: HKUnit.count())))
            }
        }
        
	query.map(healthStore.execute)
}
Ejecutamos la query que nos devolverá los pasos exactos realizados
  1. Lo único que hemos hecho ha sido seleccionar el tipo de datos que queremos obtener, en nuestro caso solo pasos.
  2. Hemos creado un rango de tiempo para sacar esta información, en nuestro caso es lo que llevamos de día.
  3. Creamos una query para obtener esta información
  4. Una vez obtenemos la información lo que hacemos es sumar estos datos y lo asignamos a una nueva propiedad @Published llamada allMySteps. Que nos servirá para refrescar nuestra vista.
  5. Ejecutamos la query

Creamos la vista

Una vez hemos creado los métodos necesarios en nuestro ViewModel, nos vamos a nuestro ContentView.  Y vamos a crear la siguiente vista:

import SwiftUI
import SwiftBetaPackage

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "figure.walk")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.red)
                .frame(width: 60, height: 60)
            Text("Pasos Hoy")
                .font(.system(size: 20, weight: .bold, design: .rounded))
            Text(viewModel.allMySteps)
                .font(.system(size: 40, weight: .bold, design: .rounded))
        }
        .padding(12)
        .overlay(
            RoundedRectangle(cornerRadius: 8)
                .stroke(style: StrokeStyle(lineWidth: 3, dash: [5]))
        )
        .task {
            viewModel.requestAccessToHealthData()
        }
    }
}
Creamos la vista en SwiftUI para mostrar el número de pasos

Ahora antes de compilar nuestra app vamos a añadir una Key en nuestro Info.plist.    Necesitamos añadir la key Privacy - Health Share Usage Description y darle un texto. El motivo es que cuando pedimos autorización para obtener los datos sobre salud de nuestro iPhone, necesitamos mostrar un Alert pidiendo permiso al user, en ese alert debe aparecer un mensaje, el que queramos, pero debemos especificarlo sí o sí.

Antes de compilar, añadimos la key Privacy - Health Share Usage Description en nuestro Info.plist
Antes de compilar, añadimos la key Privacy - Health Share Usage Description en nuestro Info.plist

En mi caso, he añadido el mensaje: Hola! queremos acceder al número de pasos que has realizado a lo largo de hoy.

Vamos a compilar a ver qué ocurre.

Vista nativa para tener acceso a los datos de HealthKit
Vista nativa para tener acceso a los datos de HealthKit
Permisos que debemos aceptar para poder acceder a los pasos realizados durante el día
Permisos que debemos aceptar para poder acceder a los pasos realizados durante el día

Una vez lanzamos nuestra app, automáticamente vemos cómo aparece la pantalla  de la imagen anterior, debemos aceptar para que nuestra app pueda acceder a la información especificada. En nuestro caso van a ser solo los pasos, pero podría ser mucha más información.

Si te fijas, también aparece el mensaje de Hola! queremos acceder al número de pasos que has realizado a lo largo de hoy.

Y una vez aceptado que nuestra app accederá a los datos de Salud, nos aparece la vista de nuestra app con la información de los pasos que hemos realizado en el día de hoy.

App que muestra el número de pasos realizados a lo largo del día
App que muestra el número de pasos realizados a lo largo del día

La imagen que os muestro a continuación es de la compilación que he hecho de mi app en mi iPhone, pero si lo hacéis en vuestro simulador, debereís añadir esta información manual. ¿Por qué? en mi iPhone tengo información que se va almacenando en la app Health, pero en el simulador nadie está nutriendo con datos la app Healtch, es por eso que lo podemos hacer de forma manual.

Al usar el simulador, añadimos los pasos de forma manual en la app Salud
Al usar el simulador, añadimos los pasos de forma manual en la app Salud

Si por ejemplo, habéis compilado la app en un dispositivo físico real, y empezáis a caminar, podréis ver como el número de pasos aumentan (el cambio no es inmediato, se tarda unos segundos en ver el cambio).

Ahora vamos a crear nuestra app en el Apple Watch

Mostramos los pasos en el Apple Watch

Ahora, sin salir de Xcode vamos a crear la app para el Apple Watch, lo que vamos hacer es crear una extensión creando un target nuevo. Nos vamos a File -> New -> Target.

Creamos un nuevo target y creamos nuestra extensión del Apple Watch
Creamos un nuevo target y creamos nuestra extensión del Apple Watch

Allí escogemos:

  • Multiplatorm: watchOS
  • Application: Watch App for iOS App
En Xcode seleccionamos Watch App for iOS App
En Xcode seleccionamos Watch App for iOS App

Le damos a Next y añadimos un nombre a nuestro nuevo Target. En mi caso voy a poner ContadorDePasosWatch.

Añadimos un nombre a nuestra app para Apple Watch
Añadimos un nombre a nuestra app para Apple Watch 

Una vez lo tenemos creado, veremos en nuestro listado de ficheros que se han añadido más carpetas y ficheros. Estos pertenecen al target de la app que acabamos de crear para el Apple Watch.

En el listado de ficheros de Xcode vemos que han aparecido de nuevos al crear el target del Apple Watch
En el listado de ficheros de Xcode vemos que han aparecido de nuevos al crear el target del Apple Watch

Nosotros, ahora lo que queremos hacer es acceder a la lógica de nuestro ViewModel, pero ¿qué pasa? está en nuestra app principal. ¿Cómo podemos acceder a los métodos de nuestro ViewModel? podríamos copiar y pegar nuestro ViewModel en el target del Apple Watch, pero no es una buena solución.
Lo que vamos hacer es crear un módulo, y la lógica que añadamos allí nos servirá para utilizar en multiples partes de nuestra app. Sin necesidad de duplicar código.

Creamos un módulo con Swift Package Manager

Para crear un módulo nos vamos a File -> New -> Package

Usamos Swift Package Manager para crear un módulo
Usamos Swift Package Manager para crear un módulo

Nos aparecerá una ventana para que indiquemos el nombre de nuestro módulo y dónde lo queremos añadir. En nuestro caso, vamos a guardarlo en la misma carpeta que nuestra app y vamos a seleccionar:

  • Add to: ContadorDePasos (en nuestra app, quizás las has llamado diferente)
  • Group: ContadorDePasos (en nuestra app, quizás las has llamado diferente)
Asignamos un nombre a nuestro módulo
Asignamos un nombre a nuestro módulo

Y una vez le damos a Create vemos como aparece en nuestro listado de ficheros de Xcode.

Fichero Package de nuestro nuevo módulo
Fichero Package de nuestro nuevo módulo

Lo que vamos hacer es copiar el ViewModel de nuestra app y lo vamos a pegar dentro de la carpeta Source de nuestro módulo, en el fichero llamado StepsModule

Copiamos y pegamos el ViewModel a nuestro módulo
Copiamos y pegamos el ViewModel a nuestro módulo

Lo siguiente que vamos hacer, es marcar como public los métodos que necesitemos acceder desde nuestra app. También crearemos un método init público para que se pueda instanciar nuestro ViewModel desde fuera de su módulo:

import Foundation
import HealthKit

final public class ViewModel: ObservableObject {
    private let healthStore = HKHealthStore()
    private var observerQuery: HKObserverQuery?
    @Published public var allMySteps: String = "0"
    private var query: HKStatisticsQuery?
    
    public init() { }
        
    public func requestAccessToHealthData() {
        let readableTypes: Set<HKSampleType> = [HKQuantityType.quantityType(forIdentifier: .stepCount)!]
        
        guard HKHealthStore.isHealthDataAvailable() else {
            return
        }
        
        healthStore.requestAuthorization(toShare: nil, read: readableTypes) { success, error in
            print("Request Authorization \(success.description)")
            if success {
                self.getTodaySteps()
            }
        }
    }

// etc
Al añadir el código del ViewModel a nuestro módulo debemos exponer con el Access Level public

Una vez hemos hecho esto, nos vamos a nuestro fichero Package de nuestro módulo que acabamos de crear, y vamos a añadir una línea, justo después de name, es decir:

let package = Package(
    name: "StepsModule",
    platforms: [.iOS(.v15), .watchOS(.v8)],

Para que quede mas claro, pego todo el fichero Package:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "StepsModule",
    platforms: [.iOS(.v15), .watchOS(.v8)],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "StepsModule",
            targets: ["StepsModule"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "StepsModule",
            dependencies: []),
        .testTarget(
            name: "StepsModuleTests",
            dependencies: ["StepsModule"]),
    ]
)
Nuestro módulo debe funcionar en iOS y watchOS, por eso añadimos la línea debajo de name

Ahora solo nos falta usar nuestro módulo en la app principal y en nuestra app del watch.

Usar nuestro módulo en nuestra app iOS y watchOS

Lo siguiente que vamos hacer es borrar nuestro ViewModel de la app. Ya que ahora  usaremos el código de nuestro ViewModel que tenemos en el módulo. Al borrar el ViewModel si intentamos compilar obtendremos un error:

Al compilar obtenemos un error, debemos importar nuestro nuevo módulo
Al compilar obtenemos un error, debemos importar nuestro nuevo módulo

Al borrar el ViewModel, no sabe de donde coger esa referencia. Para arreglarlo vamos a importar nuestro módulo en ContentView.

Antes de compilar, vamos a añadir nuestro módulo en la sección de Frameworks, Libraries, and Embedded Content

También debemos añadir el módulo en la sección de Frameworks, Libraries and Embedded Content
También debemos añadir el módulo en la sección de Frameworks, Libraries and Embedded Content

Y si compilamos nuestra app, vemos que funciona perfectamente:

Al compilar vemos que aparece la vista y el número de pasos
Al compilar vemos que aparece la vista y el número de pasos

Ahora, vamos a hacer exactamente lo mismo para nuestra extensión del Apple Watch, vamos a:

  • Importar nuestro módulo en la sección de Frameworks, Libraries, and Embedded Content
  • Añadir en Signing & Capabilities HealthKit
  • Añadir la Key Privacy - Health Share Usage Description con el valor Hola! queremos acceder al número de pasos que has realizado a lo largo de hoy en los targets de nuestro Apple Watch.

Una vez hecho esto, podemos copiar y pegar la vista ContentView de nuestra app en la ContentView de nuestra WatchKit Extension:

import SwiftUI
import StepsModule

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Image(systemName: "figure.walk")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.red)
                .frame(width: 60, height: 60)
            Text("Pasos Hoy")
                .font(.system(size: 20, weight: .bold, design: .rounded))
            Text(viewModel.allMySteps)
                .font(.system(size: 40, weight: .bold, design: .rounded))
        }
        .padding(12)
        .overlay(
            RoundedRectangle(cornerRadius: 8)
                .stroke(style: StrokeStyle(lineWidth: 3, dash: [5]))
        )
        .task {
            viewModel.requestAccessToHealthData()
        }
    }
}
Copiamos la vista en nuestra extensión del Apple Watch

Y vamos a compilar a ver qué ocurre:

Al compilar las dos plataformas iOS y watchOS muestran los mismos datos
Al compilar las dos plataformas iOS y watchOS muestran los mismos datos

Acaba de funcionar perfectamente. Acabamos de crear una app que funciona en un iPhone y en un Apple Watch. Pero para dejarlo de 10, vamos a mover la vista que hemos duplicado en el Apple Watch y la vamos a meter dentro de nuestro módulo:

import SwiftUI

public struct StepView: View {
    @StateObject var viewModel = ViewModel()
    
    public init() { }
    
    public var body: some View {
        VStack {
            Image(systemName: "figure.walk")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.red)
                .frame(width: 60, height: 60)
            Text("Pasos Hoy")
                .font(.system(size: 20, weight: .bold, design: .rounded))
            Text(viewModel.allMySteps)
                .font(.system(size: 40, weight: .bold, design: .rounded))
        }
        .padding(12)
        .overlay(
            RoundedRectangle(cornerRadius: 8)
                .stroke(style: StrokeStyle(lineWidth: 3, dash: [5]))
        )
        .task {
            viewModel.requestAccessToHealthData()
        }
    }
}

struct StepView_Previews: PreviewProvider {
    static var previews: some View {
        StepView()
    }
}
Movemos la vista al módulo de PasosModule y usamos el Access Level public

Ahora en las vistas ContentView de nuestra app y nuestro WatchKit Extension, podemos actualizarlo a:

import SwiftUI
import StepsModule

struct ContentView: View {
    
    var body: some View {
        StepView()
    }
}
Ahora nuestras vistas de iOS y watchOS solo necesitan usar la vista StepView de nuestro módulo

Ahora sí ¡Mucho más limpio!

Conclusión

Hoy hemos aprendido a cómo crear una app que funciona igual en un iPhone que en un Apple Watch. Y hemos reusado toda su lógica

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