Skip to content
melihcolpanPublic

About

A SwiftUI SDK for rendering interactive human body muscle maps with highlights, heatmaps, gestures, and UIKit support. iOS 17+, macOS 14+.

Topics

Resources

Stars

270 stars

Watchers

2 watching

Forks

Repository files navigation

MuscleMap

A native SwiftUI SDK for rendering interactive human body muscle maps with highlighting, heatmaps, multi-select, zoom, gesture-rich interaction, and UIKit support.

Supports male & female body models with front & back views. Works with both SwiftUI and UIKit.

Male Front Male Back Female Front Female Back

Features

  • SVG-based body rendering via SwiftUI Canvas
  • 36 muscle groups (22 base + 14 sub-groups) with left/right side detection
  • Muscle sub-groups with parent/child inheritance and priority hit testing
  • Always-visible sub-groups (ankles, adductors, neck) — rendered by default, tap returns parent
  • Heatmap visualization with customizable color scales
  • Tap-to-select with hit testing
  • Multi-select (select multiple muscles at once)
  • Long press gesture (with configurable duration)
  • Drag-to-select (paint muscles by dragging)
  • Pinch-to-zoom & pan (with double-tap to reset)
  • Tooltips (custom content positioned above selected muscles)
  • Undo/redo (selection history tracking)
  • 4 preset styles (default, minimal, neon, medical)
  • Gradient fills (linear & radial gradients)
  • Transition animations (fade in/out on highlight changes)
  • Pulse/glow animation (for selected muscles)
  • Shadow/drop shadow support
  • UIKit wrappers (MuscleMapView, HeatmapLegendUIView)
  • Accessibility (VoiceOver support with localized muscle names)
  • Localization (11 languages: EN, TR, DE, ES, FR, JA, ZH, KO, AR, PT-BR, RU)
  • DocC documentation catalog
  • Zero external dependencies
  • iOS 17+ / macOS 14+

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/melihcolpan/MuscleMap.git", from: "1.9.0")
]

Or in Xcode: File > Add Package Dependencies and paste the repository URL.

CocoaPods

Add to your Podfile:

pod 'MuscleMap', '~> 1.9.0'

Then run pod install.

Quick Start

import SwiftUI
import MuscleMap

struct ContentView: View {
    var body: some View {
        BodyView(gender: .male, side: .front)
            .highlight(.chest, color: .red)
            .highlight(.biceps, color: .orange, opacity: 0.8)
            .frame(height: 400)
    }
}

Usage

Basic Highlighting

BodyView(gender: .male, side: .front)
    .highlight(.chest, color: .red)
    .highlight(.abs, color: .yellow, opacity: 0.6)
    .highlight([.quadriceps, .calves], color: .orange)

Gradient Highlighting

Linear Gradient Radial Gradient Neon Gradient

// Linear gradient (top to bottom)
BodyView(gender: .male, side: .front)
    .highlight(.chest, linearGradient: [.red, .orange], startPoint: .top, endPoint: .bottom)

// Radial gradient (center outward)
    .highlight(.biceps, radialGradient: [.white, .blue], center: .center, endRadius: 40)

// Mix gradients and solid colors
    .highlight(.quadriceps, color: .purple)

Tap Detection

BodyView(gender: .female, side: .front)
    .onMuscleSelected { muscle, side in
        print("\(muscle.displayName) (\(side))")
    }

Heatmap

Workout Heatmap Thermal Heatmap

// Integer scale (0-4, like workout trackers)
BodyView(gender: .male, side: .front)
    .intensities([
        .chest: 3,
        .biceps: 2,
        .quadriceps: 4,
        .abs: 1
    ])

// Custom intensity data (0.0 - 1.0)
let data = [
    MuscleIntensity(muscle: .chest, intensity: 0.8),
    MuscleIntensity(muscle: .biceps, intensity: 0.5, side: .left),
    MuscleIntensity(muscle: .abs, intensity: 0.3, color: .purple)
]
BodyView(gender: .male, side: .front)
    .heatmap(data, colorScale: .thermal)

Color Scales

Scale Colors
.workout gray -> yellow -> orange -> red
.thermal blue -> green -> yellow -> red
.medical green -> yellow -> red
.monochrome light gray -> dark
.workoutStepped workout with 5 discrete steps
.thermalSmooth thermal with ease-in-out curve

Custom:

let custom = HeatmapColorScale(colors: [.blue, .purple, .pink])

Color Interpolation

Stepped Heatmap

Control how intensity values map to colors across the scale:

// Ease-in-out for smoother transitions
BodyView(gender: .male, side: .front)
    .heatmap(data, colorScale: .thermal)
    .heatmapInterpolation(.easeInOut)

// Stepped (discrete levels)
BodyView(gender: .male, side: .front)
    .heatmap(data, colorScale: .workoutStepped)  // built-in 5-step preset

// Custom curve
.heatmapInterpolation(.custom { t in t * t * t })

Available interpolations: .linear, .easeIn, .easeOut, .easeInOut, .step(count:), .custom()

A color scale's own interpolation is used by default, so .workoutStepped renders in steps and .thermalSmooth eases. An interpolation set with .heatmapInterpolation(_:) or a HeatmapConfiguration takes precedence when it is not .linear.

Heatmap Threshold

Hide muscles below a minimum intensity:

BodyView(gender: .male, side: .front)
    .heatmap(data)
    .heatmapThreshold(0.2)  // muscles with intensity < 0.2 are hidden

Gradient Heatmap Fill

Gradient Heatmap Stepped Heatmap

Apply intra-muscle gradients based on intensity (low-to-high color within each muscle):

BodyView(gender: .male, side: .front)
    .heatmap(data, colorScale: .thermal)
    .heatmapGradient(direction: .topToBottom, lowFactor: 0.3)

Directions: .topToBottom, .bottomToTop, .leftToRight, .rightToLeft

Heatmap Configuration

Combine all heatmap settings in a single configuration:

let config = HeatmapConfiguration(
    colorScale: .thermal,
    interpolation: .easeInOut,
    threshold: 0.2,
    isGradientFillEnabled: true,
    gradientDirection: .topToBottom,
    gradientLowIntensityFactor: 0.3
)

BodyView(gender: .male, side: .front)
    .heatmap(data, configuration: config)

Heatmap Legend

Display a color bar legend alongside the body view:

// Horizontal legend
HeatmapLegendView(colorScale: .workout)
    .frame(width: 200)

// Vertical legend with custom labels
HeatmapLegendView(
    colorScale: .thermal,
    interpolation: .easeInOut,
    orientation: .vertical,
    barThickness: 20,
    labelMin: "Rest",
    labelMax: "Max"
)
.frame(width: 60, height: 200)

Animated Heatmap Transitions

When using .animated(), color transitions between heatmap states are now smoothly interpolated:

BodyView(gender: .male, side: .front)
    .heatmap(currentData, colorScale: .thermal)
    .animated(duration: 0.5)

Styles

Neon Style Medical Style

BodyView(gender: .male, side: .front)
    .bodyStyle(.neon)
Style Description
.default Gray fill, green selection
.minimal Subtle fill, thin strokes
.neon Dark background, cyan selection, glow shadow
.medical Clinical blue-gray tones

Custom:

let style = BodyViewStyle(
    defaultFillColor: .gray,
    strokeColor: .white,
    strokeWidth: 1,
    selectionColor: .yellow,
    selectionStrokeColor: .yellow,
    selectionStrokeWidth: 3,
    headColor: .gray,
    hairColor: .black,
    shadowColor: .blue.opacity(0.5),
    shadowRadius: 6,
    shadowOffset: CGSize(width: 0, height: 2)
)

Animations

Transition Animation

Smooth fade-in/fade-out when highlights change:

BodyView(gender: .male, side: .front)
    .highlight(.chest, color: .red)
    .animated(duration: 0.3)

Pulse Animation

Pulsing glow effect on the selected muscle:

@State private var selected: Muscle?

BodyView(gender: .male, side: .front)
    .highlight(.chest, color: .red)
    .selected(selected)
    .pulseSelected(speed: 1.5, range: 0.6...1.0)
    .onMuscleSelected { muscle, _ in
        selected = muscle
    }

Selection State

// Single selection (backward compatible)
@State private var selected: Muscle?

BodyView(gender: .male, side: .front)
    .selected(selected)
    .onMuscleSelected { muscle, _ in
        selected = muscle
    }

Multi-Select

@State private var selectedMuscles: Set<Muscle> = []

BodyView(gender: .male, side: .front)
    .selected(selectedMuscles)
    .onMuscleSelected { muscle, _ in
        if selectedMuscles.contains(muscle) {
            selectedMuscles.remove(muscle)
        } else {
            selectedMuscles.insert(muscle)
        }
    }

Long Press

BodyView(gender: .male, side: .front)
    .onMuscleLongPressed(duration: 0.5) { muscle, side in
        print("Long pressed: \(muscle.displayName)")
    }

Drag-to-Select

BodyView(gender: .male, side: .front)
    .onMuscleDragged({ muscle, side in
        selectedMuscles.insert(muscle)
    }, onEnded: {
        print("Drag ended")
    })

Pinch-to-Zoom

BodyView(gender: .male, side: .front)
    .zoomable(minScale: 1.0, maxScale: 4.0)

Tooltips

BodyView(gender: .male, side: .front)
    .selected(selectedMuscles)
    .tooltip { muscle, side in
        Text(muscle.displayName)
            .font(.caption)
            .padding(4)
            .background(.ultraThinMaterial)
    }

Undo/Redo

@State private var history = SelectionHistory()

BodyView(gender: .male, side: .front)
    .undoable(history)

Button("Undo") { if let state = history.undo() { selectedMuscles = state } }
    .disabled(!history.canUndo)
Button("Redo") { if let state = history.redo() { selectedMuscles = state } }
    .disabled(!history.canRedo)

Muscle Sub-Groups

Front Sub-Groups Back Sub-Groups

Sub-groups provide finer control over muscle regions. Each one is cut from its parent muscle's artwork: the upper and lower chest, the front deltoid, the upper and lower abs, the serratus slips on the ribs, the top of the quadriceps (hip flexors) and the inner and outer quadriceps heads. They inherit the parent muscle's highlight when no specific highlight is set, and take priority in hit testing.

Always-visible sub-groups (ankles, adductors, neck) are rendered by default but return their parent muscle on tap — so tapping the ankle area returns .feet, tapping the neck returns .head, etc.

// Highlight parent and sub-group with different intensities
BodyView(gender: .male, side: .front)
    .highlight(.chest, color: .red, opacity: 0.4)       // parent (dimmer)
    .highlight(.upperChest, color: .red, opacity: 0.9)   // sub-group (brighter)
    .highlight(.quadriceps, color: .blue, opacity: 0.4)
    .highlight(.innerQuad, color: .blue, opacity: 0.9)

Query sub-group relationships:

Muscle.chest.subGroups       // [.upperChest, .lowerChest]
Muscle.upperChest.parentGroup // .chest
Muscle.upperChest.isSubGroup  // true

// Always-visible sub-groups
Muscle.ankles.isAlwaysVisibleSubGroup  // true
Muscle.ankles.parentGroup              // .feet

Gender & Side

BodyView(gender: .male, side: .front)   // Male front
BodyView(gender: .male, side: .back)    // Male back
BodyView(gender: .female, side: .front) // Female front
BodyView(gender: .female, side: .back)  // Female back

Checking What Is Drawn

Back Regions

Every muscle is drawn in at least one view, but many only on the front or the back. Use isDrawable to check a muscle mapping for a specific view, for example in a unit test:

Muscle.rhomboids.isDrawable                               // true (drawn somewhere)
Muscle.rhomboids.isDrawable(gender: .male, side: .back)   // true
Muscle.rhomboids.isDrawable(gender: .male, side: .front)  // false
Muscle.head.isDrawable(gender: .female, side: .back)      // false, the hair covers it

The back views have these extra regions (shown above with .showSubGroups()):

  • .upperTrapezius and .lowerTrapezius: the trapezius above and below the rhomboids. On the front views the visible trapezius is the upper part.
  • .rhomboids: inside the trapezius, painted only while highlighted or selected, so an unhighlighted body and a .trapezius highlight look exactly as before.
  • .rearDeltoid: the deltoid as seen from behind.
  • .rotatorCuff: the infraspinatus area above the shoulder blade. It shows the .upperBack highlight when it has none of its own.

With sub-groups hidden (the default), tapping the rhomboids or the rotator cuff returns .trapezius / .upperBack.

Faster First Render

Creating a BodyView starts parsing the body artwork in the background. To have it ready before the first body appears, start it at launch:

@main
struct MyApp: App {
    init() {
        BodyView.preloadArtwork()
    }
    // ...
}

Available Muscles

Base Muscles (22)

Muscle Key
Abs .abs
Biceps .biceps
Calves .calves
Chest .chest
Deltoids .deltoids
Feet .feet
Forearm .forearm
Gluteal .gluteal
Hamstring .hamstring
Hands .hands
Head .head
Knees .knees
Lower Back .lowerBack
Obliques .obliques
Quadriceps .quadriceps
Rhomboids .rhomboids (back views)
Rotator Cuff .rotatorCuff (back views)
Serratus .serratus
Tibialis .tibialis
Trapezius .trapezius
Triceps .triceps
Upper Back .upperBack

Sub-Groups (14)

Sub-Group Key Parent Always Visible
Upper Chest .upperChest .chest
Lower Chest .lowerChest .chest
Upper Abs .upperAbs .abs
Lower Abs .lowerAbs .abs
Inner Quad .innerQuad .quadriceps
Outer Quad .outerQuad .quadriceps
Hip Flexors .hipFlexors .quadriceps
Front Deltoid .frontDeltoid .deltoids
Rear Deltoid .rearDeltoid .deltoids
Upper Trapezius .upperTrapezius .trapezius
Lower Trapezius .lowerTrapezius .trapezius
Ankles .ankles .feet Yes
Adductors .adductors .hamstring Yes
Neck .neck .head Yes

.rearDeltoid, .upperTrapezius and .lowerTrapezius are drawn on the back views, and .upperTrapezius on the front views too.

UIKit Integration

MuscleMapView

Drop-in UIView wrapper for UIKit-based projects:

import MuscleMap

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let muscleMap = MuscleMapView(gender: .male, side: .front)
        muscleMap.highlight(.chest, color: .systemRed)
        muscleMap.highlight(.biceps, color: .systemOrange, opacity: 0.8)
        muscleMap.onMuscleSelected = { muscle, side in
            print("\(muscle.displayName) tapped")
        }

        view.addSubview(muscleMap)
        muscleMap.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            muscleMap.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            muscleMap.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            muscleMap.widthAnchor.constraint(equalToConstant: 300),
            muscleMap.heightAnchor.constraint(equalToConstant: 500)
        ])
    }
}

HeatmapLegendUIView

UIKit wrapper for the heatmap legend:

let legend = HeatmapLegendUIView(colorScale: .thermal)
legend.orientation = .vertical
legend.labelMin = "Rest"
legend.labelMax = "Max"
view.addSubview(legend)

Accessibility

MuscleMap includes full VoiceOver support. Each muscle region is exposed as an accessibility element with:

  • Localized muscle name as the accessibility label; muscles drawn on both sides get one element per side ("Biceps, Left", "Biceps, Right")
  • Selection state ("Selected" / "Not selected")
  • Tap and long press hints
  • Top-to-bottom traversal order (anatomical navigation)

Cosmetic parts (e.g., head) are excluded from the accessibility tree.

// Accessibility works automatically — no extra configuration needed
BodyView(gender: .male, side: .front)
    .highlight(.chest, color: .red)
    .onMuscleSelected { muscle, side in
        // VoiceOver users can double-tap to select
    }

Localization

All muscle names, side labels, and accessibility strings are localized in 11 languages:

Language Code
English en
Turkish tr
German de
Spanish es
French fr
Japanese ja
Chinese (Simplified) zh-Hans
Korean ko
Arabic ar
Portuguese (Brazil) pt-BR
Russian ru

Localized names are available via displayName:

// Returns localized name based on user's device language
Muscle.chest.displayName        // "Chest" (EN), "Göğüs" (TR), "Brust" (DE)
MuscleSide.left.displayName     // "Left" (EN), "Sol" (TR), "Links" (DE)
BodySide.front.displayName      // "Front" (EN), "Ön" (TR), "Vorderseite" (DE)
BodyGender.male.displayName     // "Male" (EN), "Erkek" (TR), "Männlich" (DE)

Example App

A demo app is included in the Example/ directory. Open Example/MuscleMapDemoApp.xcodeproj in Xcode to explore all features interactively.

Requirements

  • iOS 17.0+
  • macOS 14.0+
  • Swift 5.9+ (builds in the Swift 6 language mode on a Swift 6 toolchain)

Development

swift test                                   # unit tests
swift run --package-path Tools/ScreenshotGenerator ScreenshotGenerator Screenshots
                                             # regenerate the README screenshots

Every push and pull request runs the tests, an iOS build, the demo app build, the documentation build and pod lib lint on GitHub Actions. Dependabot keeps the workflow's actions up to date.

The API documentation is on the Swift Package Index.

License

MIT License. See LICENSE for details.

About

A SwiftUI SDK for rendering interactive human body muscle maps with highlights, heatmaps, gestures, and UIKit support. iOS 17+, macOS 14+.

Topics

Resources

Stars

270 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages