Skip to content

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 FrontMale BackFemale FrontFemale 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.6.4")]

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

CocoaPods

Add to your Podfile:

pod'MuscleMap','~> 1.6.4'

Then run pod install.

Quick Start

import SwiftUI
import MuscleMap
structContentView:View{varbody:someView{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 GradientRadial GradientNeon 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 inprint("\(muscle.displayName) (\(side))")}

Heatmap

Workout HeatmapThermal 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)
letdata=[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

ScaleColors
.workoutgray -> yellow -> orange -> red
.thermalblue -> green -> yellow -> red
.medicalgreen -> yellow -> red
.monochromelight gray -> dark
.workoutSteppedworkout with 5 discrete steps
.thermalSmooththermal with ease-in-out curve

Custom:

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

Color Interpolation

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()

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 HeatmapStepped 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:

letconfig=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 StyleMedical Style

BodyView(gender:.male, side:.front).bodyStyle(.neon)
StyleDescription
.defaultGray fill, green selection
.minimalSubtle fill, thin strokes
.neonDark background, cyan selection, glow shadow
.medicalClinical blue-gray tones

Custom:

letstyle=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:

@Stateprivatevarselected: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)
@Stateprivatevarselected:Muscle?BodyView(gender:.male, side:.front).selected(selected).onMuscleSelected{ muscle, _ in
selected = muscle
}

Multi-Select

@StateprivatevarselectedMuscles:Set<Muscle>=[]BodyView(gender:.male, side:.front).selected(selectedMuscles).onMuscleSelected{ muscle, _ inif selectedMuscles.contains(muscle){
selectedMuscles.remove(muscle)}else{
selectedMuscles.insert(muscle)}}

Long Press

BodyView(gender:.male, side:.front).onMuscleLongPressed(duration:0.5){ muscle, side inprint("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 inText(muscle.displayName).font(.caption).padding(4).background(.ultraThinMaterial)}

Undo/Redo

@Stateprivatevarhistory=SelectionHistory()BodyView(gender:.male, side:.front).undoable(history)Button("Undo"){iflet state = history.undo(){ selectedMuscles = state }}.disabled(!history.canUndo)Button("Redo"){iflet state = history.redo(){ selectedMuscles = state }}.disabled(!history.canRedo)

Muscle Sub-Groups

Sub-groups provide finer control over muscle regions. 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

Available Muscles

Base Muscles (22)

MuscleKey
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
Rotator Cuff.rotatorCuff
Serratus.serratus
Tibialis.tibialis
Trapezius.trapezius
Triceps.triceps
Upper Back.upperBack

Sub-Groups (14)

Sub-GroupKeyParentAlways 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.feetYes
Adductors.adductors.hamstringYes
Neck.neck.headYes

UIKit Integration

MuscleMapView

Drop-in UIView wrapper for UIKit-based projects:

import MuscleMap
classViewController:UIViewController{overridefunc viewDidLoad(){
super.viewDidLoad()letmuscleMap=MuscleMapView(gender:.male, side:.front)
muscleMap.highlight(.chest, color:.systemRed)
muscleMap.highlight(.biceps, color:.systemOrange, opacity:0.8)
muscleMap.onMuscleSelected ={ muscle, side inprint("\(muscle.displayName) tapped")}
view.addSubview(muscleMap)
muscleMap.translatesAutoresizingMaskIntoConstraints =falseNSLayoutConstraint.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:

letlegend=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
  • 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:

LanguageCode
Englishen
Turkishtr
Germande
Spanishes
Frenchfr
Japaneseja
Chinese (Simplified)zh-Hans
Koreanko
Arabicar
Portuguese (Brazil)pt-BR
Russianru

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+

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

225 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages