Skip to content

LachPawel/FormKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

6 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FormKit

A minimal, production-ready iOS bootstrap for building vision-based fitness apps.

FormKit wires together Apple's Vision framework, AVFoundation, and SwiftUI so you can go from zero to a working real-time pose-detection app in minutes β€” without boilerplate. Clone it, drop in your exercise logic, and ship.


Features

Feature Details
πŸ“· Live camera feed Front/back switchable AVCaptureSession with portrait orientation and mirroring
🦴 Real-time skeleton overlay Glowing stick-figure drawn on top of the camera preview using SwiftUI Path
🀸 Human body pose detection Apple Vision DetectHumanBodyPoseRequest β€” 15 joints tracked at up to 30 fps
πŸ” Rep counter engine Protocol-based ExerciseRule β€” implement one method to count any exercise
πŸ“Š Live debug HUD On-screen FPS counter + joint coordinates overlay for rapid iteration
⚑ Efficient frame processing Processes every 3rd frame, drops late frames, avoids redundant work
πŸ”’ Thread-safe architecture @MainActor isolation for UI, dedicated session & processing queues for capture

Demo

Real-time pose skeleton rendered over the front camera feed.

WIll be added later.


πŸ—οΈ Architecture

FormKit/
β”œβ”€β”€ FormKitApp.swift              # App entry point
β”œβ”€β”€ ContentView.swift             # Root view β†’ PoseDetectionView
β”‚
β”œβ”€β”€ Camera/
β”‚   β”œβ”€β”€ CameraViewModel.swift     # AVCaptureSession lifecycle, camera switching
β”‚   β”œβ”€β”€ CameraView.swift          # SwiftUI session-state router (loading/error/running)
β”‚   β”œβ”€β”€ CameraPreview.swift       # UIViewRepresentable wrapping AVCaptureVideoPreviewLayer
β”‚   β”œβ”€β”€ CameraViewWrapper.swift   # Convenience wrapper (session start/stop on appear)
β”‚   β”œβ”€β”€ PoseEstimator.swift       # @MainActor Vision inference engine + FPS tracking
β”‚   β”œβ”€β”€ ExerciseRepCounter.swift  # Protocol-driven rep-counting engine
β”‚   β”œβ”€β”€ CIImage_Extension.swift   # CIImage β†’ CGImage helper
β”‚   └── CMSampleBuffer_Extension.swift  # CMSampleBuffer β†’ CGImage helper
β”‚
β”œβ”€β”€ Skeleton/
β”‚   β”œβ”€β”€ FreePostureStickFigureView.swift  # SwiftUI skeleton overlay (bones + joints)
β”‚   └── Stick.swift               # Generic polyline Shape (coordinate flip helper)
β”‚
└── Views/
    └── PoseDetectionView.swift   # Main screen: camera + skeleton + debug HUD

Data flow

AVCaptureSession
    β”‚  (sample buffer, every 3rd frame)
    β–Ό
PoseEstimator          ← nonisolated AVCaptureVideoDataOutputSampleBufferDelegate
    β”‚  DetectHumanBodyPoseRequest (Vision)
    β”‚  publishes bodyParts [@MainActor]
    β–Ό
ExerciseRepCounter     ← called via $bodyParts Combine subscription
    β”‚  ExerciseRule.evaluate(joints:)
    β”‚  publishes currentReps, currentPhase
    β–Ό
PoseDetectionView      ← @StateObject, redraws on each publish
    β”œβ”€β”€ CameraView          (live preview)
    β”œβ”€β”€ FreePostureStickFigureView  (skeleton)
    └── debugOverlay        (HUD)

πŸš€ Getting Started

Requirements

  • Xcode 16+
  • iOS 17+ (uses DetectHumanBodyPoseRequest from the Vision v2 API)
  • A physical iPhone or iPad β€” the Simulator does not provide a camera feed

Clone & run

git clone https://github.com/LachPawel/FormKit.git
cd FormKit
open FormKit.xcodeproj

Select your device in Xcode and press ⌘R.

Camera permission β€” Xcode will prompt automatically on first launch. Make sure NSCameraUsageDescription is set in Info.plist (already included).


Adding Your Own Exercise

All exercise intelligence lives in one place: ExerciseRepCounter.swift.

1. Implement ExerciseRule

struct BicepCurlRule: ExerciseRule {
    let name = "Bicep Curl"
    private var phase: Phase = .down

    private enum Phase { case up, down }

    mutating func evaluate(
        joints: [HumanBodyPoseObservation.PoseJointName: Joint],
        currentRepCount: Int
    ) -> RepCounterUpdate {

        guard
            let shoulder = joints[.rightShoulder],
            let elbow    = joints[.rightElbow],
            let wrist    = joints[.rightWrist],
            shoulder.confidence > 0.5,
            elbow.confidence    > 0.5,
            wrist.confidence    > 0.5
        else {
            return RepCounterUpdate(didIncrementRep: false, phase: "tracking lost", debugMessage: "low confidence")
        }

        let angle = elbowAngle(shoulder: shoulder.location,
                               elbow:    elbow.location,
                               wrist:    wrist.location)

        switch phase {
        case .down where angle < 50:
            phase = .up
            return RepCounterUpdate(didIncrementRep: false, phase: "up", debugMessage: "angle=\(Int(angle))Β°")
        case .up where angle > 150:
            phase = .down
            return RepCounterUpdate(didIncrementRep: true,  phase: "down", debugMessage: "rep! angle=\(Int(angle))Β°")
        default:
            return RepCounterUpdate(didIncrementRep: false, phase: phase == .up ? "up" : "down", debugMessage: "angle=\(Int(angle))Β°")
        }
    }

    mutating func reset() { phase = .down }
}

2. Inject the rule

In PoseDetectionView.swift (or wherever you initialise PoseEstimator), pass your rule to the counter:

// PoseEstimator.swift β€” inside init()
repCounter = ExerciseRepCounter(rule: BicepCurlRule())

That's it. repCounter.currentReps and repCounter.currentPhase are already @Published and available in every view that observes PoseEstimator.


Key Components

PoseEstimator

@MainActor class PoseEstimator: ObservableObject
  • Conforms to AVCaptureVideoDataOutputSampleBufferDelegate via a nonisolated extension β€” safe to call from any queue
  • Processes every 3rd frame to balance accuracy and battery life
  • Calculates live FPS using CACurrentMediaTime
  • Filters joints by confidence > 0 before publishing

CameraViewModel

class CameraViewModel: ObservableObject
  • Manages AVCaptureSession on a dedicated serial sessionQueue
  • Supports front ↔ back camera switching at runtime via NotificationCenter (.switchCamera)
  • Disables the idle timer while the session is active to prevent screen sleep
  • Exposes sessionState: CameraSessionState (.initializing / .running / .failed) for UI feedback

FreePostureStickFigureView

  • Draws bones as Path strokes with a LinearGradient and a glow shadow
  • Draws joints as layered Circle shapes (glow + white fill + accent border)
  • Low-confidence joints (< 0.5) show a red debug ring
  • Coordinate conversion: Vision's (0,0) is bottom-left; the view flips Y to match UIKit

ExerciseRule protocol

protocol ExerciseRule {
    var name: String { get }
    mutating func evaluate(joints: [...], currentRepCount: Int) -> RepCounterUpdate
    mutating func reset()
}

A value type (struct) is preferred so phase state is contained inside the rule.


πŸ“ Joint Map

Apple Vision provides 19 named joints. FormKit uses the following subset:

Screenshot 2026-03-08 at 09 53 34

All joint names match HumanBodyPoseObservation.PoseJointName from the Vision framework.


πŸ›‘οΈ Privacy

Add the following key to your Info.plist:

<key>NSCameraUsageDescription</key>
<string>FormKit uses the camera to detect your body pose in real time.</string>

No camera data is stored or transmitted. All inference runs on-device using Apple Vision.


🀝 Contributing

  1. Fork the repo and create a feature branch: git checkout -b feature/my-exercise
  2. Add your ExerciseRule implementation (and tests if applicable)
  3. Open a pull request β€” please include a short screen recording if the change is visual

πŸ“„ License

MIT β€” see LICENSE for details.


πŸ™ Acknowledgements

  • Apple Vision Framework β€” on-device human body pose detection
  • Apple AVFoundation β€” camera capture pipeline
  • Detecting Human Body Poses in Images β€” the capability to detect human body poses to your app using the Vision framework.

About

Apple Vision - Human Body Pose based Fitness apps template

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages