---
title: outdooractive/gis-tools-fit
framework: Swift Package Catalog
role: article
path: packages/outdooractive/gis-tools-fit
---

# outdooractive/gis-tools-fit

FIT (Flexible and Interoperable Data Transfer) file read **and write** support for Swift, built on top of [**gis-tools**](https://github.com/Outdooractive/gis-tools). Parses Garmin FIT activity files into typed `FeatureCollection` objects and writes them back to binary FIT, pure

## Features

- Pure Swift FIT decoder and encoder — no external dependencies - Reads and writes FIT 2.0 files with CRC validation - Record points → `Feature<MultiLineString>` with per-point sensor data as parallel arrays - Lap and Event messages → their own `Feature` markers with full metadata - Session, Lap, Event, File-ID, and Activity messages → `Feature` properties + `FeatureCollection` metadata - **Per-point arrays**: heart rate, cadence, power, speed, temperature, altitude, timestamps - **Typed convenience API** on `Feature` and `FeatureCollection` — no manual dictionary casting - **Content-preserving round-trip**: read → write reproduces every message and field - **Enum name mapping**: turn raw enum values (e.g. `event: 9`) into names (e.g. `"lap"`) - `fitPointFeatures()` → expand track into individual `Point` features with each record's sensor data - Time-window, distance-window, and fractional-window slicing of point data

## Requirements

Swift 6.1 or higher. Compiles on iOS (≥ iOS 15), macOS (≥ macOS 15), tvOS (≥ tvOS 15), watchOS (≥ watchOS 7), Linux, Android and Wasm. No external dependencies beyond the base `gis-tools` package.

## Installation with Swift Package Manager

```swift dependencies: [     .package(url: "https://github.com/Outdooractive/gis-tools-fit", from: "1.0.0"),     .package(url: "https://github.com/Outdooractive/gis-tools", from: "2.0.0"), ], targets: [     .target(name: "MyTarget", dependencies: [         .product(name: "GISToolsFIT", package: "gis-tools-fit"),         .product(name: "GISTools", package: "gis-tools"),     ]), ] ```

## Usage

### Reading

```swift import GISTools import GISToolsFIT

let url = URL(fileURLWithPath: "/path/to/activity.fit") let fc = try FITCoder.read(from: url)

// Or via the convenience init: guard let fc = FeatureCollection(fit: url) else { return } ```

### Writing

```swift // Encode a FeatureCollection back to binary FIT data let data = try FITCoder.write(from: fc)

// Or write directly to a file try FITCoder.write(from: fc, to: url)

// Or via the FeatureCollection convenience method try fc.writeFIT(to: url) ```

The `FeatureCollection` must contain a track `Feature` with a `MultiLineString` geometry and `fit_type == "record"`, as produced by `FITCoder.read(from:)`. Per-point sensor arrays, session, lap, and activity metadata are written back into the corresponding FIT messages. Round-trip read → write → read preserves coordinates, heart rate, cadence, power, speed, temperature, altitude, and timestamps.

The round-trip is **content-preserving**: every message (`file_id`, `activity`, `session`, `lap`, `record`, `event`, `device_info`) and every field is written back, so the re-encoded file decodes to the same values as the original.

### Track geometry

```swift let track = fc.features.first! let multiLine = track.geometry as! MultiLineString print("\(multiLine.lineStrings.count) lap segments") for (i, seg) in multiLine.lineStrings.enumerated() {     print("  Lap \(i): \(seg.coordinates.count) points") } print("\(multiLine.lineStrings.flatMap { $0.coordinates }.count) total points") ```

### Per-point sensor data (parallel arrays)

Per-point arrays are indexed parallel to the flattened `MultiLineString` coordinates. For Garmin devices, every record contains all sensor fields, so the arrays always align:

```swift let hr = track.fitHeartRates      // [Int?]? — heart rate per track point let cad = track.fitCadences        // [Int?]? — cadence per track point let pw = track.fitPowers           // [Int?]? — power in watts let spd = track.fitSpeeds          // [Double?]? — speed in m/s let tmp = track.fitTemperatures    // [Double?]? — temperature in °C let alt = track.fitAltitudes       // [Double?]? — altitude in meters let ts = track.fitTimestamps       // [Date?]? — timestamps per point

// Index i in the arrays → coordinate[i] in the flattened MultiLineString // nil means no data recorded for that sensor at that point for i in 0..<(track.fitHeartRates?.count ?? 0) {     if let hr = hr[i], let cad = cad[i], let pw = pw[i] {         print("Point \(i): HR=\(hr), cad=\(cad), power=\(pw)")     } } ```

### Point feature conversion

Convert the multi-point track into individual `Point` features, each carrying its own sensor data:

```swift // All track points as individual Features let pts = track.fitPointFeatures() pts.features.count                  // e.g. 5283 pts.features[0].properties["heart_rate"]  // 120 pts.features[0].properties["power"]       // 150 pts.features[0].properties["timestamp"]   // ISO8601 date string ```

### Time-based slicing

```swift import Foundation

let start = FITDateFromTimestamp(500100) let end = FITDateFromTimestamp(500200) let segment = track.fitPointFeatures(from: start, to: end) print("\(segment.features.count) points in this window") ```

### Distance-based slicing

```swift // Points between 1 km and 5 km along the track let middle = track.fitPointFeatures(from: 1000.0, to: 5000.0)

// Last kilometer let lastKm = track.fitPointFeatures(from: totalMeters - 1000.0, to: totalMeters) ```

### Fractional slicing

```swift // Middle third of the track let midThird = track.fitPointFeatures(fraction: 0.33, to: 0.66) ```

### Reconstructing a track from point features

Convert a `FeatureCollection` of Point features (from `fitPointFeatures()`) back into a `Feature<MultiLineString>` track with per-point sensor arrays rebuilt:

```swift let pts = track.fitPointFeatures()

// Via FeatureCollection: let rebuilt = pts.fitTrackFromPointFeatures() rebuilt?.fitHeartRates?[0]   // 120 rebuilt?.fitPowers?[2]        // 280

// Via Feature convenience init: let track = Feature(fitTrackFrom: pts) track?.fitCadences?[1]        // 90 ```

Non-Point features are silently skipped. Returns `nil` if no Point features exist.

### FeatureCollection convenience properties

```swift // Quick access to record features let records = fc.fitRecords records[0].properties["heart_rate"]  // 120

// Device and activity metadata let device = fc.fitDevice print(device?["manufacturer"])  // e.g. "Garmin"

let activity = fc.fitActivity print(fc.fitSport)             // e.g. "running" (mapped from the raw sport value) ```

### Enum name mapping

FIT stores enum-typed fields (e.g. `event`, `sport`, `event_type`) as raw integers. Use `FIT.name(for:value:)` to turn a value into its human-readable name, or `fitEnumName(for:)` on a `Feature` to map a property directly:

```swift // Direct lookup FIT.name(for: "event", value: 9)         // "lap" FIT.name(for: "event_type", value: 1)    // "stop" FIT.name(for: "sport", value: 1)         // "running" FIT.name(for: "manufacturer", value: 1)  // "garmin"

// Via a Feature's properties let event = fc.features.first { $0.fitType == .event } event.fitEnumName(for: "event")       // "timer" event.fitEnumName(for: "event_type")  // "start" ```

Field names that differ from their enum type name (e.g. `trigger` → `session_trigger`, `sensor_position` → `body_location`) are resolved automatically.

### Summary data

```swift // Session metadata (avg/max HR, speed, distance, calories) print(track.fitAvgHeartRate)        // Int? — e.g. 145 print(track.fitMaxHeartRate)        // Int? print(track.fitTotalDistance)       // Double? — meters print(track.fitTotalCalories)       // Int? — kcal ``` ### Per-point array reference

| Property | Type | FIT Field | |---|---|---| | `fitHeartRates` | `[Int?]?` | `record.heart_rate` | | `fitCadences` | `[Int?]?` | `record.cadence` | | `fitPowers` | `[Int?]?` | `record.power` | | `fitSpeeds` | `[Double?]?` | `record.speed` (converted) | | `fitTemperatures` | `[Double?]?` | `record.temperature` | | `fitAltitudes` | `[Double?]?` | `record.altitude` (converted) | | `fitTimestamps` | `[Date?]?` | `record.timestamp` |

### Single-value property reference

| Property | Type | Source | |---|---|---| | `fitType` | `FITRecordType?` | `.record` / `.lap` / `.session` | | `fitHeartRate` | `Int?` | single-record summary | | `fitCadence` | `Int?` | single-record summary | | `fitPower` | `Int?` | single-record summary | | `fitSpeed` | `Double?` | falls back to `enhanced_speed` | | `fitTemperature` | `Double?` | | | `fitAltitude` | `Double?` | falls back to `enhanced_altitude` | | `fitDistance` | `Double?` | `record.distance` (converted) | | `fitPositionLat` / `fitPositionLon` | `CLLocationDegrees?` | converted from semicircles | | `fitAvgHeartRate` / `fitMaxHeartRate` | `Int?` | session summary | | `fitAvgSpeed` / `fitMaxSpeed` | `Double?` | session summary | | `fitTotalDistance` | `Double?` | session total (meters) | | `fitTotalElapsedTime` | `Double?` | session total (seconds) | | `fitTotalCalories` | `Int?` | session total (kcal) | | `fitTotalAscent` / `fitTotalDescent` | `Double?` | session total (meters) |

### FeatureCollection metadata reference

| Property | Type | Source | |---|---|---| | `fitRecords` | `[Feature]` | features filtered by `fitType == .record` | | `fitActivity` | `[String: Sendable]?` | activity message | | `fitDevice` | `[String: Sendable]?` | file_id + device_info | | `fitSport` | `String?` | session sport type |

**Methods on `FeatureCollection`:**

| Method | Returns | Description | |---|---|---| | `fitTrackFromPointFeatures()` | `Feature?` | Reconstructs a `Feature<MultiLineString>` from Point features produced by `fitPointFeatures()`. Sensor data is re-accumulated into per-point arrays. Returns `nil` if no Point features exist. |

**Convenience initializer on `Feature`:**

| Initializer | Description | |---|---| | `Feature(fitTrackFrom:)` | Creates a track Feature from a `FeatureCollection` of Point features. Same as `fitTrackFromPointFeatures()`. |

### Limitations

- **Compressed timestamps**: basic support; time-offset accumulation is minimal - **Developer fields**: parsed but not exposed via typed API - **Large files**: all messages are decoded in memory at once; files with 500K+ records may use significant memory - **No Garmin SDK**: this is a clean-room implementation of the FIT protocol; edge cases in vendor-specific extensions may not be handled

## Contributing

Please [create an issue](https://github.com/Outdooractive/gis-tools-fit/issues) or [open a pull request](https://github.com/Outdooractive/gis-tools-fit/pulls) with a fix or enhancement.

## License

MIT

## Authors

Thomas Rasch, Outdooractive

Built on top of [**gis-tools**](https://github.com/Outdooractive/gis-tools).

## Package Metadata

Repository: outdooractive/gis-tools-fit

Default branch: main

README: README.md
