Quick start
We’ll build a screen that shows the camera, the live elbow angle and the reps. By the end you’ll have the three pieces every integration uses: pose source, session and events.
-
Create the pose source and the session
VisionPoseProviderreads the front camera and delivers onePoseper frame.MotionSessiontakes those poses for one exercise and one side of the body.import KinesiaMotionimport KinesiaMotionVisionlet camera = VisionPoseProvider(camera: .front)let session = MotionSession(exercise: ExerciseLibrary.elbowFlexion, side: .right) -
Ask for permission and start
if !VisionPoseProvider.isAuthorized {guard await VisionPoseProvider.requestAccess() else { return }}session.start(with: camera)The camera turns on when the session starts reading poses and turns itself off when you call
session.stop(). -
Draw from the snapshot
MotionSessionis@Observable: SwiftUI redraws wheneversession.snapshotchanges.Text(session.snapshot.value.map { "\(Int($0))°" } ?? "—")Text("\(session.snapshot.repCount) reps") -
React to events
Events are for things that happen once: a finished rep, a form correction.
for await event in session.events() {switch event {case .repCompleted(let rep): print("✓ \(Int(rep.peak))°")case .formFault(let fault): print("Fix: \(fault)")default: break}}
The full example
Section titled “The full example”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 = "Stand sideways and show your whole arm"
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) reps · best \(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 = "Nice! \(Int(rep.peak))°" case .repRejected: message = "That one doesn't count: watch your form" case .formFault(.keepUpperArmStill): message = "Keep your upper arm by your side" case .visibilityChanged(let visibility) where !visibility.isVisible: message = "I can't see your whole arm" 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 } }}Next steps
Section titled “Next steps”- Understand how the engine works inside.
- Add a baseline measurement to know where the patient starts.
- Use measurement quality to guide placement in front of the camera.