Inicio rápido
Vamos a construir una pantalla que muestra la cámara, el ángulo del codo en vivo y las repeticiones. Al terminar tendrás las tres piezas que usa cualquier integración: fuente de poses, sesión y eventos.
-
Crea la fuente de poses y la sesión
VisionPoseProviderlee la cámara frontal y entrega unaPosepor fotograma.MotionSessionrecibe esas poses para un ejercicio y un lado del cuerpo.import KinesiaMotionimport KinesiaMotionVisionlet camera = VisionPoseProvider(camera: .front)let session = MotionSession(exercise: ExerciseLibrary.elbowFlexion, side: .right) -
Pide permiso y arranca
if !VisionPoseProvider.isAuthorized {guard await VisionPoseProvider.requestAccess() else { return }}session.start(with: camera)La cámara se enciende cuando la sesión empieza a leer poses y se apaga sola al llamar
session.stop(). -
Dibuja con el snapshot
MotionSessiones@Observable: SwiftUI se redibuja cada vez que cambiasession.snapshot.Text(session.snapshot.value.map { "\(Int($0))°" } ?? "—")Text("\(session.snapshot.repCount) repeticiones") -
Reacciona a los eventos
Los eventos son para lo que pasa una sola vez: una repetición terminada, una corrección de técnica.
for await event in session.events() {switch event {case .repCompleted(let rep): print("✓ \(Int(rep.peak))°")case .formFault(let fault): print("Corrige: \(fault)")default: break}}
El ejemplo completo
Sección titulada «El ejemplo completo»import SwiftUIimport AVFoundationimport KinesiaMotionimport KinesiaMotionVision
struct ElbowView: View { @State private var camera = VisionPoseProvider(camera: .front) @State private var session = MotionSession(exercise: ExerciseLibrary.elbowFlexion, side: .right) @State private var message = "Ponte de lado y muestra todo el brazo"
var body: some View { ZStack(alignment: .bottom) { CameraPreview(session: camera.captureSession) .ignoresSafeArea()
VStack(spacing: 8) { Text(session.snapshot.value.map { "\(Int($0))°" } ?? "—") .font(.system(size: 64, weight: .bold, design: .rounded)) Text("\(session.snapshot.repCount) repeticiones · mejor \(Int(session.snapshot.best))°") Text(message).font(.callout) } .padding() .background(.ultraThinMaterial, in: .rect(cornerRadius: 20)) .padding() } .task { if !VisionPoseProvider.isAuthorized { guard await VisionPoseProvider.requestAccess() else { return } } session.start(with: camera)
for await event in session.events() { switch event { case .repCompleted(let rep): message = "¡Bien! \(Int(rep.peak))°" case .repRejected: message = "Esa no cuenta: cuida la técnica" case .formFault(.keepUpperArmStill): message = "Mantén el brazo pegado al cuerpo" case .visibilityChanged(let visibility) where !visibility.isVisible: message = "No veo todo el brazo" default: break } } } .onDisappear { session.stop() } }}
/// The camera image behind everything.struct CameraPreview: UIViewRepresentable { let session: AVCaptureSession
func makeUIView(context: Context) -> PreviewView { let view = PreviewView() view.previewLayer.session = session view.previewLayer.videoGravity = .resizeAspectFill return view }
func updateUIView(_ uiView: PreviewView, context: Context) {}
final class PreviewView: UIView { override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } }}Siguientes pasos
Sección titulada «Siguientes pasos»- Entiende cómo funciona el motor por dentro.
- Agrega una medición inicial para saber de dónde parte el paciente.
- Usa la calidad de la medición para guiar la ubicación frente a la cámara.