![Aprende a crear una app de contador de pasos en tu iPhone y Apple Watch](/content/images/2022/03/Crea-App-Apple-Watch-2.webp)
¿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.
Tabla de contenido
![Aprende a crear una app de contador de pasos en tu iPhone y Apple Watch](https://www.swiftbeta.com/content/images/2023/04/Aprende-a-crear-una-app-de-contador-de-pasos-en-tu-iPhone-y-Apple-Watch.webp)
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](https://www.swiftbeta.com/content/images/2022/02/1-1.png)
Le ponemos nombre, en mi caso la voy a llamar ContadorDePasos
![En Xcode añadimos el nombre de nuestra app](https://www.swiftbeta.com/content/images/2022/02/2-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/3-1.png)
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)")
}
}
}
- 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.
- 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)
}
}
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)
}
- Lo único que hemos hecho ha sido seleccionar el tipo de datos que queremos obtener, en nuestro caso solo pasos.
- Hemos creado un rango de tiempo para sacar esta información, en nuestro caso es lo que llevamos de día.
- Creamos una query para obtener esta información
- 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.
- 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()
}
}
}
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](https://www.swiftbeta.com/content/images/2022/02/4-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/5.webp)
![Permisos que debemos aceptar para poder acceder a los pasos realizados durante el día](https://www.swiftbeta.com/content/images/2022/02/6.webp)
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](https://www.swiftbeta.com/content/images/2022/02/7.webp)
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](https://www.swiftbeta.com/content/images/2022/02/8-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/9-1.png)
Allí escogemos:
- Multiplatorm: watchOS
- Application: Watch App for iOS App
![En Xcode seleccionamos Watch App for iOS App](https://www.swiftbeta.com/content/images/2022/02/10-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/11-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/12-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/13-1.png)
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](https://www.swiftbeta.com/content/images/2022/02/15.png)
Y una vez le damos a Create vemos como aparece en nuestro listado de ficheros de Xcode.
![Fichero Package de nuestro nuevo módulo](https://www.swiftbeta.com/content/images/2022/02/16.png)
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](https://www.swiftbeta.com/content/images/2022/02/17.png)
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
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"]),
]
)
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](https://www.swiftbeta.com/content/images/2022/02/18.png)
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](https://www.swiftbeta.com/content/images/2022/02/19.png)
Y si compilamos nuestra app, vemos que funciona perfectamente:
![Al compilar vemos que aparece la vista y el número de pasos](https://www.swiftbeta.com/content/images/2022/02/20.png)
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()
}
}
}
Y vamos a compilar a ver qué ocurre:
![Al compilar las dos plataformas iOS y watchOS muestran los mismos datos](https://www.swiftbeta.com/content/images/2022/02/21.png)
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()
}
}
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 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