I write about my apps and life as an indie iOS developer

Tutorial Axel Rivera Tutorial Axel Rivera

How to Import a FIT File to HealthKit

One of the things that motivated me to develop apps in the Health and Fitness space was seeing my Apple Health workouts section disappointingly empty after recording a bike ride. Whether I used the Strava app on my Apple Watch or tracked it with my Garmin Edge 130 Plus, my workouts simply didn’t show up properly in the Fitness app. It quickly became clear that some companies choose not to send full workout data to Apple Health, likely to keep users within their ecosystem. But I also wondered if there was a technical reason – maybe converting and saving a workout to HealthKit was just too complex? Determined to solve this, I made it my goal to learn how to import my rides into HealthKit, despite the challenges of converting the data to the right format.

One of the things that motivated me to develop apps in the Health and Fitness space was seeing my Apple Health workouts section disappointingly empty after recording a bike ride. Whether I used the Strava app on my Apple Watch or tracked it with my Garmin Edge 130 Plus, my workouts simply didn’t show up properly in the Fitness app. It quickly became clear that some companies choose not to send full workout data to Apple Health, likely to keep users within their ecosystem. But I also wondered if there was a technical reason – maybe converting and saving a workout to HealthKit was just too complex? I was determined to get this done, so I set out to learn how to import my rides into HealthKit, even though it was a bit tricky figuring out how to format the data.

Here’s a screenshot showing a comparison of two workouts from the Fitness app. On the left, you’ll see a workout recorded with the Strava app on my Apple Watch. On the right, you’ll see a workout recorded with a Wahoo ELEMNT Bolt cycling computer and the FIT file imported with my own app, Ride Import.

In this tutorial, I’ll guide you through importing workouts recorded by non-Apple devices, specifically cycling workouts saved as FIT files from devices like Garmin, Wahoo Fitness, and Coros. I’ll show you how to format and save the data to Apple Health so that it displays correctly in the Fitness app or other third-party apps. We’ll cover key concepts like cumulative vs. discrete samples, handling moving time, and setting data samples for accurate stats. While I’m focusing on cycling for simplicity, you can easily apply these concepts to other activities with some adjustments.

Getting Started

Before importing the data, we need to understand what a FIT file is and how it stores data.

A FIT (Flexible and Interoperable Data Transfer) file is a binary file format commonly used to store fitness and workout data, such as GPS coordinates, heart rate, speed, distance, elevation, and other activity-related metrics. Developed by Garmin, it’s widely used by fitness devices, smartwatches, cycling computers, and fitness tracking software due to its efficient storage and compatibility across different platforms.

Here’s a screenshot of the content inside a FIT file using a macOS app called FitFileExplorer.

FIT files are like spreadsheets that store data in different sections. We’ll only be looking at a few of them.

  1. Session — includes general information about the workout, including start time, sport, and total metrics

  2. Events — includes events triggered by pause, resume, etc.

  3. Records — includes all detailed samples for any sensor connected to a device including gps, distance, heart rate, calories, etc. Popular sports watches and cycling computer can record multiple events per second. The sample shown above includes more than 6,000 records for a 20 mile bike ride.

Reading the FIT File

To read the FIT files, I’m using a Swift dependency called FitDataProtocol developed by Kevin A. Hoogheem.

If you’re running your own Xcode project, you can install it using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/FitnessKit/FitDataProtocol", from: "2.1.4")
]

Workout Data Encoder

Below you can see a service called FitFileProcessor that parses the data from a FIT file and extracts the sections that we need. As you can see, there’s only one session object and multiple objects for events and records. Opening the FIT file itself is beyond the scope of this tutorial, but you can download the source code for a demo app below to see how it’s done.

import Foundation
import FitDataProtocol

enum FitFileProcessorError: Error {
    case missingSession
}

struct WorkoutData {
    let session: SessionMessage
    let events: [EventMessage]
    let records: [RecordMessage]
}

final class FitFileProcessor {    
    lazy var decoder = FitFileDecoder(crcCheckingStrategy: .throws)
    
    func decode(data: Data) throws -> WorkoutData {
        var session: SessionMessage?
        var events: [EventMessage] = []
        var records: [RecordMessage] = []

        try decoder.decode(
            data: data,
            messages: FitFileDecoder.defaultMessages
        ) { message in
            if let sessionMessage = message as? SessionMessage {
                session = sessionMessage
            }
            
            if let event = message as? EventMessage {
                events.append(event)
            }
            
            if let message = message as? RecordMessage {
                records.append(message)
            }
        }
        
        guard let session else {
            throw GenericError("missing session")
        }
        
        return WorkoutData(session: session, events: events, records: records)
    }
    
}

Saving to HealthKit

Setup Workout Importer

I’m using another service to save data to HealthKit. First we start creating a class WorkoutImporter where we’ll be adding the logic later.

NOTE: Ideally, the service should be an Actor instead of a Class, but it looks that the metadata dictionary doesn’t want to play nice with concurrency and throws a compiler error.

import Foundation
import HealthKit
import CoreLocation
import FitDataProtocol
import AntMessageProtocol

enum WorkoutImporterError: Error {
    case invalidData
    case missingSession
    case activityNotSupported
    case saveFailed
}

final class WorkoutImporter {
    private let healthStore: HKHealthStore
    
    private var session: SessionMessage = SessionMessage()
    private var events: [EventMessage] = []
    private var records: [RecordMessage] = []
    
    init(healthStore: HealthStore = .shared) {
        self.healthStore = healthStore.defaultStore
    }
}

extension WorkoutImporter {
    
    func process(data: WorkoutData) async throws -> UUID {
        self.session = data.session
        self.events = data.events
        self.records = data.records
        return try await saveToHealth()
    }
    
}

The basic logic for saving a workout to HealthKit is as follows.

  1. Create a HKWorkoutConfiguration and set the activity type and indoor properties. We need to create an activityType() and isIndoor(subSport:) methods to extract the values from the FIT file session.

  2. Create an HKWorkoutBuilder object

  3. Call the beginCollection(at:) method on workout builder

  4. Generate and add metadata

  5. Generate and add events

  6. Generate and add samples for distance, energy, heart rate, cadence and power; these samples may depend on the activity type

  7. Call the endCollection(at:) method on workout builder

  8. Save and finish the workout

  9. Generate and save a route, if available

extension WorkoutImporter {
    
    func saveToHealth() async throws -> UUID {
        // all workouts must have a start time and duration
        guard let startDate = session.startTime?.recordDate, let elapsedTime = session.totalElapsedTime else {
            throw WorkoutImporterError.invalidData
        }
        
        // Map the Sport property in the FIT file to HKWorkoutActivityType
        // Or throw an error if an activity type is not supported
        guard let activityType = activityType() else {
            throw WorkoutImporterError.activityNotSupported
        }
        
        // A workout is indoor if the subSport is spinning, indoor cycling or virtual activity (i.e. Zwift)
        let subSport = session.subSport
        let isIndoor = isIndoor(subSport: subSport)
        
        // 1. Create Configuration
        let configuration = HKWorkoutConfiguration()
        configuration.activityType = activityType
        configuration.locationType = isIndoor  ? .indoor : .outdoor
        
        // 2. Create Builder
        let builder = HKWorkoutBuilder(healthStore: healthStore, configuration: configuration, device: .local())
        
        // 3. Begin Collecting Data
        try await builder.beginCollection(at: startDate)
        
        // 4. Add Metadata
        let metadata = try self.metadata(from: session)
        try await builder.addMetadata(metadata)
        
        // 5. Add Events
        // The app will crash if you try to add empty values to the builder
        let events = generateWorkoutEvents()
        if !events.isEmpty {
            try await builder.addWorkoutEvents(events)
        }
        
        // 6. Add Samples
        let samples = generateWorkoutSamples()
        if !samples.isEmpty {
            try await builder.addSamples(samples)
        }
        
        // 7. End Collecting Data
        // FIT files don't include an end data so we need to calculate it from the elapsed time
        let elapsedTimeInSeconds = elapsedTime.converted(to: .seconds).value
        let endDate = startDate.addingTimeInterval(elapsedTimeInSeconds)
        try await builder.endCollection(at: endDate)
        
        // 8. Finish the Workout
        guard let workout = try await builder.finishWorkout() else {
            throw WorkoutImporterError.saveFailed
        }
        
        // 9. Generate and save a route if needed
        let locations = generateLocations()
        
        // Route processing is ignored if the activity is virtual or indoor
        // It is also ignored if there are no locations available in the FIT file
        let shouldProcessRoute = !((subSport == .virtualActivity || isIndoor) || locations.isEmpty)
        if shouldProcessRoute {
            let routeBuilder = HKWorkoutRouteBuilder(healthStore: healthStore, device: .local())
            try await routeBuilder.insertRouteData(locations)
            try await routeBuilder.finishRoute(with: workout, metadata: nil)
        }
        
        return workout.uuid
    }

    func activityType() -> HKWorkoutActivityType? {
        // I"m only supporting cycling to keep things simple,
        // but you should map supported activities here.
        // You may need to include additional metadata or samples.
        
        switch session.sport {
        case .cycling:
            return .cycling
        default:
            return nil
        }
    }
    
    func isIndoor(subSport: SubSport?) -> Bool {
        guard let subSport else { return false }
        return [.indoorCycling, .spin, .virtualActivity].contains(subSport)
    }

}

Generate Metadata

When it comes to cycling, the only info you really need is the indoor flag, average speed, and elevation. You can also add some extra details like the weather, time zone, and a special code to keep track of your workouts in your own database. But other activities might need more info. Check out the extra metadata keys on Apple’s website for more details.

extension WorkoutImporter {

    func metadata(from session: SessionMessage) throws -> [String: Any] {
        var dictionary: [String: Any] = [:]
        dictionary[HKMetadataKeyIndoorWorkout] = isIndoor(subSport: session.subSport)
        
        if let avgSpeed = session.averageSpeed {
            let conversion = avgSpeed.converted(to: .metersPerSecond)
            let quantity = HKQuantity(unit: HKUnit.meter().unitDivided(by: .second()), doubleValue: conversion.value)
            dictionary[HKMetadataKeyAverageSpeed] = quantity
        }
        
        if let totalAscent = session.totalAscent {
            let conversion = totalAscent.converted(to: .meters)
            let quantity = HKQuantity(unit: HKUnit.meter(), doubleValue: conversion.value)
            dictionary[HKMetadataKeyElevationAscended] = quantity
        }
        
        return dictionary
    }

}

Generate Events

Generating events correctly is super important because it helps us figure out your average speed and moving time. For now, we only need to worry about pause and resume events. Just to reiterate, there’s no way to manually set moving time or average speed in HealthKit. The Fitness app calculates them automatically. As an app developer, you’ll also need to calculate the average speed manually in your apps by dividing the total distance by the moving time.

extension WorkoutImporter {

    func generateWorkoutEvents() -> [HKWorkoutEvent] {
        var workoutEvents: [HKWorkoutEvent] = []
        
        // We only care about start and stop events in the FIT file to map them to their corresponding
        // resume or pause events in HealthKit
        for event in events {
            guard let timestamp = event.timeStamp?.recordDate else { continue }
            
            let dateInterval = DateInterval(start: timestamp, end: timestamp)
            switch event.eventType {
            case .start:
                workoutEvents.append(.init(type: .resume, dateInterval: dateInterval, metadata: nil))
            case .stop, .stopAll:
                workoutEvents.append(.init(type: .pause, dateInterval: dateInterval, metadata: nil))
            default:
                break
            }
        }

        // cleaning the first and last events to make sure they are valid
        // the app will crash if the events are not in order following sequences
        // of pause, resume, pause, resume, etc...
        
        // the first event cannot be resume
        if let first = workoutEvents.first, first.type == .resume {
            workoutEvents = Array(workoutEvents.dropFirst())
        }
        
        // the last event cannot be a pause
        if let last = workoutEvents.last, last.type == .pause {
            workoutEvents = Array(workoutEvents.dropLast())
        }
        
        return workoutEvents
    }

}

Generate Samples

This is probably the most challenging part of converting data from a FIT file to a workout. The format is quite standard and used by most players in the industry. HealthKit prefers data in a specific way, so we need to convert it. HealthKit has two types of data samples: cumulative and discrete.

  1. Cumulative samples increase during the workout and each value depends on the previous. Some examples may include distance or energy.

  2. Discrete samples are independent values such as heart rate. Some examples may include heart rate, cadence, and power.

Here’s a basic overview of distance, heart rate, and cycling cadence. The process should be similar for the other metrics. You can download the complete demo source code below to see the full implementation.

Cumulative Example

extension WorkoutImporter {

    func distanceSample(start: RecordMessage, end: RecordMessage) -> HKCumulativeQuantitySample? {
        guard let startMeasurement = start.distance,
              let endMeasurement = end.distance else { return nil }
        guard let startDate = start.timeStamp?.recordDate,
              let endDate = end.timeStamp?.recordDate else { return nil }

        let startValue = startMeasurement.converted(to: .meters).value
        let endValue = endMeasurement.converted(to: .meters).value

        // FIT files include the total cummulative value in each record
        // but samples in HealthKit expects fractional values as they increase
        let value = endValue - startValue
        guard value > 0 else { return nil }

        let quantity = HKQuantity(unit: .meter(), doubleValue: value)
        return HKCumulativeQuantitySample(type: .distanceCycling(), quantity: quantity, start: startDate, end: endDate)
    }

}

Discrete Example

Here are two examples, one for heart rate and one for cycling cadence. Cycling cadence is a bit different because it’s calculated using two things from the FIT file: cadence and fractional cadence.

extension WorkoutImporter {

    func heartRateSample(record: RecordMessage) -> HKDiscreteQuantitySample? {
        guard let date = record.timeStamp?.recordDate else { return nil }
        guard let measurement = record.heartRate else { return nil }
        
        let value = measurement.value
        guard value > 0 else { return nil }
                
        let quantity = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()), doubleValue: value)
        return .init(type: .heartRate(), quantity: quantity, start: date, end: date)
    }
    
    func cadenceSample(record: RecordMessage) -> HKDiscreteQuantitySample? {
        guard let date = record.timeStamp?.recordDate else { return nil }
        guard let measurement = record.cadence else { return nil }
        guard measurement.value > 0 else { return nil }
        
        // NOTE ABOUNT CADENCE:
        // cadence samples use two properties in FIT files due to number constraints
        // the total cadence is the sum of the cadence and fractional cadence properties in the FIT file
        let fractionalCadence = record.fractionalCadence ?? .init(value: 0, unit: .revolutionsPerMinute)
        let value = measurement.value + fractionalCadence.value
                
        let quantity = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()), doubleValue: value)
        return .init(type: .cyclingCadence(), quantity: quantity, start: date, end: date)
    }

}

Generating All Samples

To generate all the samples, we simply iterate through the FIT file, taking two records at a time and saving them individually. For cumulative metrics, we need two values to calculate the delta between them, making them compatible with HealthKit.

I’ve omitted some of the methods for brevity. You can download the full demo source code linked below to see the complete implementation.

extension WorkoutImporter {

    func generateWorkoutSamples() -> [HKSample] {
        var distance: [HKSample] = []
        var energy: [HKSample] = []
        var heartRate: [HKSample] = []
        var cadence: [HKSample] = []
        var power: [HKSample] = []
        
        for (record, nextRecord) in zip(records, records.dropFirst()) {
            if let value = distanceSample(start: record, end: nextRecord) {
                distance.append(value)
            }
            
            if let value = energySample(start: record, end: nextRecord) {
                energy.append(value)
            }
            
            if let value = heartRateSample(record: record) {
                heartRate.append(value)
            }
            
            if let value = cadenceSample(record: record) {
                cadence.append(value)
            }
            
            if let value = powerSample(record: record) {
                power.append(value)
            }
        }
        
        return distance + energy + heartRate + cadence + power
    }

}

Generate the Route

FIT files have a unique way of storing GPS data, so we need to convert it to a CLLocation. Why CLLocation instead of just CLLocationCoordinate2D? Well, HealthKit saves extra info when you save a workout route. You can check out the elevation chart in workout details when using the Fitness app or access the elevation data directly in your app.

extension WorkoutImporter {

    func generateLocations() -> [CLLocation] {
        // NOTE: Including location metadata is important!
        // The metadata is used when showing the elevation chart in Workout Details in the Fitness app.
        
        records.compactMap { (record) -> CLLocation? in
            guard let position = record.position else { return nil }
            guard let latitude = position.latitude, let longitude = position.longitude else {
                return nil
            }
            
            // FIT files have their own data structure for location data
            // We need to convert to CLLocationCoordinate2D and CLLocation
            let newLatitude = latitude.converted(to: .degrees).value
            let newLongitude = longitude.converted(to: .degrees).value
            let coordinate = CLLocationCoordinate2D(latitude: newLatitude, longitude: newLongitude)
            
            guard CLLocationCoordinate2DIsValid(coordinate) else { return nil }
            guard let timestamp = record.timeStamp?.recordDate else { return nil }
            
            let accuracy = record.gpsAccuracy?.converted(to: .meters).value ?? 0
            let altitude = record.altitude?.converted(to: .meters).value ?? 0
            let speed = record.speed?.converted(to: .metersPerSecond).value ?? 0
            
            return CLLocation(
                coordinate: coordinate,
                altitude: altitude,
                horizontalAccuracy: accuracy,
                verticalAccuracy: accuracy,
                course: -1,
                speed: speed,
                timestamp: timestamp
            )
        }
    }

}

Where to Go From Here?

You can download the source code for the sample app on GitHub. The code is organized for reusability, making it a solid foundation for your own projects. No FIT file? No problem—the demo app includes several sample files, so you can start experimenting right away.

This code has been thoroughly tested in production over the last four years, recording more than 15,000 cycling miles to HealthKit.

If you plan to support additional activity types, be sure to review the HealthKit documentation for further insights.

At a future date, I’ll be posting about how to fetch workouts and create an infinite feed of workouts that also show the route. Stay tuned!

Read More
Apps Axel Rivera Apps Axel Rivera

The Apple Watch Rings Don’t Work for Endurance Athletes

When the Apple Watch first came out, I was immediately impressed by the Activity rings. They were a simple but genius way to make sure people stayed active, and I still believe they're effective for promoting general fitness. For the average person, closing those rings each day can be a great way to stay on track. After all, staying active is one of the most important factors for maintaining good health.

Cyclist Climbing

The Genius of Apple Watch Rings

When the Apple Watch first came out, I was immediately impressed by the Activity rings. They were a simple but genius way to make sure people stayed active, and I still believe they're effective for promoting general fitness. For the average person, closing those rings each day can be a great way to stay on track. After all, staying active is one of the most important factors for maintaining good health.

However, when the Apple Watch first launched, I wasn't very active. I'd try to run or walk whenever I could and felt accomplished when I closed my rings, but my activities were random. I wasn't motivated by walking, and running wasn't my thing. Then, in 2020, I discovered cycling, and everything changed. Suddenly, I found a sport that clicked with me, and I started building a real habit around it.

Discovering the Limits of the Rings with Cycling

At first, my focus was still on closing my rings. Seeing 3-, 5-, or 7-day streaks was a huge motivator. But there were two things I didn't expect: how quickly my body would adapt to cycling, and how much more I wanted to do once I started riding regularly. I initially thought riding 5-10 miles would cover my fitness needs, but I was wrong. What started as a challenge quickly became routine, and soon enough, I was aiming for longer distances.

I remember talking to a neighbor who mentioned a 20-mile bike ride that left him exhausted. At the time, that seemed like a long-term goal for me. But after six months of consistent cycling, I completed that same ride and had energy to spare. What once felt impossible had become attainable.

By the end of 2020, I had logged 1,000 miles without even realizing it. I was riding multiple days a week, with distances like 20 miles on Wednesday, 25 on Friday, and longer rides on the weekend. But something started to shift: as my rides got longer, I began taking rest days more seriously.

That's when I noticed a problem with the Apple Watch rings. While the rings are great for ensuring basic activity levels, they don't account for the demands of endurance sports. The rings encourage daily activity, like a 30-minute walk, which fits the Mayo Clinic's recommendation of 150 minutes of moderate aerobic exercise per week. But I was cycling 9-10 hours a week, far exceeding the minimum, yet feeling unproductive on rest days when my rings weren't closing.

Shifting Focus to Long-Term Goals

Eventually, I had to reframe my thinking. Instead of focusing on daily streaks, I shifted my attention to weekly and yearly mileage goals. I realized that for endurance athletes, full rest days are crucial for recovery, and closing my rings every day wasn't necessary. I stopped feeling guilty about taking days off and started focusing on hitting my bigger targets—like 75 miles a week and 4,000 miles a year.

This shift in mindset was a game-changer, and it's part of what inspired me to create Active Goals, an app that helps track weekly, monthly, and yearly performance. It allows me to stay motivated by long-term goals rather than daily streaks, giving me the flexibility to adjust my targets based on my life and schedule.

In the end, the Apple Watch rings are a brilliant tool for promoting basic fitness, but for those of us in endurance sports, they're not the full picture. What really matters is finding what motivates you and setting goals that challenge you without burning out. For me, that means focusing on miles over streaks—and that's how I keep pushing forward.

Read More
Development Axel Rivera Development Axel Rivera

I’m Sticking with Native iOS Development

Lately, every time I see a job opening for iOS development it’s either React Native, Flutter, or some type of cross-platform stack. I admit that these technologies make sense in many cases. Learning React Native has crossed my mind a few times. I’m choosing to stick with native development using Swift and SwiftUI.

Lately, every time I see a job opening for iOS development it’s either React Native, Flutter, or some type of cross-platform stack.

I admit that these technologies make sense in many cases. Learning React Native has crossed my mind a few times. I’m choosing to stick with native development using Swift and SwiftUI.

Here’s why…

Swift is the Programming Language that I Know the Most

I often hear people debating what’s the best programming language. My answer is the one that you know the best when starting a new a project.

I have a lot of experience with Ruby and Ruby on Rails, but I avoid mobile apps that require backends for reasons that I’ll discuss later.

I think it’s OK to experiment with new languages, but regardless of how senior you are, your first app in any language will suck. You don’t know the best practices yet. You don’t know the standard libraries well enough. In fact, you will write your app using another language as a reference point. It will get the job done, but you’ll be ashamed of yourself in the following months.

Currently, Swift is the programming language that I know the most. I’ve been coding in Swift since it came out in 2014. In the past 10 years, I’ve used it to publish more than 10 apps on the App Store. I only trust Swift when I have to put an app in the hands of users. It’s never failed me yet!

Swift Makes Developing Fast

SwiftUI is the shining new toy for native iOS development. It’s hard to believe that’s been around for nearly 5 years now. I see many people complaining online that’s not ready for production yet. The main arguments being that’s hard to customize, or it doesn’t scale very well. I don’t disagree with those statements. But SwiftUI lets me finish a polished MVP for an app in a fraction of the time compared to UIKit.

UIKit is powerful. You can code pixel perfect UIs, but you’ll write more code. It will take a lot longer. That is not acceptable for me as a solo developer.

In fact, if SwiftUI wasn’t available, I would have probably considered React Native or Flutter for app development. SwiftUI made me change my mind.

Is the framework perfect? No.

Does it have many limitations? Yes.

SwiftUI requires a different mindset. In UIKit, you customize your app to match a design or wireframe. In SwiftUI, you should start by adjusting your design to take advantage of what’s given to you by the framework. By using vanilla controls, you can build a working MVP in a fraction of the time.

In fact, for my personal projects, I stopped using Figma or Sketch to create wireframes. In my current process, I build a few prototypes or proof of concepts in SwiftUI to get an idea of what’s possible and start from there.

Making a Living from the App Store

I’m currently aiming to become a successful indie app developer. As a solo developer, focusing on a single platform makes sense.

There are personal and technical reasons for my choice.

Here’s my personal reason…

I’m at a point where I have to make choices that will decide the second half of my life.

I’m choosing to prioritize having a simple and flexible lifestyle: work on things that are important to me, or being able to go for a 4-hour bike ride on a weekday morning.

To reach that goal, I’m learning to do more with less. I’m deciding to prioritize my quality of life over money. Without going into much detail, I don’t pay rent or have a mortgage. That means that my monthly expenses are low, and that gives me options.

Now the technical reason…

Focusing on a single platform, in this case Swift and SwiftUI, keeps things simple. I don’t need to implement a backend to persist data between multiple platforms. I can use Core Data and CloudKit for that. For my personal projects, I avoid implementing a backend like the plague.

To me, a backend is overhead! It’s extra work that I don’t enjoy as much. I’d rather spend my time working on the mobile app.

I choose to go all in with Apple’s frameworks and technologies. It keeps things simple, but more importantly, it increases the odds of getting featured on the App Store.

Is Apple’s walled garden perfect? No. But as an indie developer, it makes my life easier. I can focus on the things that are important and deliver value to my users.

Conclusion

I’m aware that by sticking with native iOS development, I may be leaving a lot of money and opportunities on the table. I’m selecting the path that I enjoy the most and prioritizing a flexible lifestyle.

Read More
Apps Axel Rivera Apps Axel Rivera

Apollo Weather for Apple Watch Released

I'm thrilled to introduce Apollo Weather for Apple Watch, an important addition to the existing iOS app designed to keep outdoor enthusiasts and endurance athletes one step ahead of the weather.

I'm thrilled to introduce Apollo Weather for Apple Watch, an important addition to the existing iOS app designed to keep outdoor enthusiasts and endurance athletes one step ahead of the weather.

Key Features

  1. Detailed Forecasts: Users can access both current and 10-day hourly and daily weather forecasts, ensuring they're always informed of changing conditions.

  2. Innovative Complications: Apollo Weather brings a fresh perspective to complications with designs that are simple yet intuitive. Their primary goals are:

    • Clear, bold text for easy readability during activities like cycling or running.

    • Colors and icons that instantly relay information about the user's ideal conditions.

  3. Focus on Athletes: Apollo isn't just another weather app; it's tailored specifically for endurance athletes, prioritizing data that empowers them to make informed training decisions. Some examples include its unique features, such as the display of ideal ratings and active times.

Availability & Pricing

Apollo Weather for Apple Watch requires a PREMIUM subscription, priced at $9.99 annually. First-time users are welcome to explore the PREMIUM features with a 7-day free trial.


Note that the Apple Watch app is compatible with watchOS 10+.

Read More
Apps Axel Rivera Apps Axel Rivera

Feature: Ideal Conditions Provide Unique Value to Endurance Athletes

What does perfect weather mean to you? One of the features that make Apollo Weather unique and valuable to endurance athletes is Ideal Ratings. How does it work? In Apollo, you can define your own conditions to define your ideal workout. You can configure perfect and acceptable parameters for temperature, precipitation, wind, and UV Index.

What does perfect weather mean to you? One of the features that make Apollo Weather unique and valuable to endurance athletes is Ideal Ratings. How does it work?

In Apollo, you can define your own conditions to define your ideal workout. You can configure perfect and acceptable parameters for:

  • Temperature

  • Precipitation

  • Wind

  • UV Index

Apollo includes a 10-day hourly forecast, and every hour is rated as PERFECT, ACCEPTABLE, or BAD depending on your ideal conditions.

You can then configure your active times to reduce noise and make it easier to find the best days and times to train.

How can you take advantage of Ideal Conditions?

1. The Today screen shows hourly ratings for the current forecast and the next 24 hours.

2. The Daily screen shows a 10-day forecast with a color-coded bar chart that helps you find the best days to train in seconds.

3. Once you find the best days to train, you can zoom in on any day to find the best hours to train during the day.

4. You can always tap on any hour to get a detailed hourly rating for individual data points.

How do I use Ideal Ratings?

Everybody has their own routine for planning their weekly workouts. I'll share with you how I plan my weekly bike rides.

My ideal weekly mileage goal is 100 miles, with a minimum of 75 miles. I don't have a planned schedule for bike rides because, often, work forces me to readjust my priorities. I ride 3 to 4 times per week, and in most cases, I ride both Saturday and Sunday.

  • I start with the Daily screen to get a general idea of conditions for the full week.

  • If there's rain forecasted for the weekend, I'll try to add an extra ride during the week.

  • If weekday mornings are too cold, I'll look for weekend afternoons to schedule my long rides.

  • Before rides, I look at the rating for the current hour to decide how to prepare for my ride. That means extra electrolytes or fuel during the summer or additional layers during the winter.

As you can see, my schedule can be defined as chaotic during the week. Balancing full-time work, developing Apollo, and working out is extremely challenging. Apollo helps me reduce weather uncertainty to make sure I can complete my weekly goals.

Read More
Apps Axel Rivera Apps Axel Rivera

Apollo Weather Adds Support for Lock Screen Widgets

I'm excited to announce that Apollo Weather finally has support for Lock Screen Widgets. Home screen widgets were released back in March and have been popular among existing users including cyclists and running coaches.

I'm excited to announce that Apollo Weather finally has support for Lock Screen Widgets. Home screen widgets were released back in March and have been popular among existing users including cyclists and running coaches.

For Lock Screen Widgets, I wanted to build something unique instead of duplicating widgets from Apple's weather app. That's why all widgets in Apollo focus on ideal ratings. Every hourly forecast is rated as Perfect, Acceptable or Bad, depending on ideal conditions set by users.

New widgets include:

  1. Ideal Forecast — users can view the current weather and ideal ratings. Plus two customizable data points.

  2. Ideal Hourly Forecast — users can view the hourly forecast and ideal ratings. There's a customizable data point for each hour.

  3. Data Point Rating — Individual data and rating for Temperature, Feels Like, Wet Bulb, Wind Speed and UV Index.


You can download Apollo Weather from the App Store.

Read More
Apps Axel Rivera Apps Axel Rivera

Apollo Weather Adds Support for Wet Bulb Globe Temperature

I'm excited to announce that Apollo is adding support for Wet Bulb Globe Temperature just before the summer kicks into full gear. The measurement is a secret weapon for engaging in outdoor activities in direct sunlight.

I'm excited to announce that Apollo is adding support for Wet Bulb Globe Temperature just before the summer kicks into full gear. The measurement is a secret weapon for engaging in outdoor activities in direct sunlight.

What is Wet Bulb Globe Temperature (WBGT)?

The WBGT is a quantitative measure of heat stress in direct sunlight, which encompasses factors such as temperature, humidity, wind speed, sun angle, and cloud cover (solar radiation). The WBGT is used by military agencies, OSHA, and many nations to manage workload in direct sunlight.

How Can Apollo Users Benefit from WBGT?

Wet Bulb Globe Temperature is a better tool for athletes to keep track of temperature conditions during the summer because of its increased accuracy when measuring conditions in direct sunlight.

Here's how users can use WBGT within the app:

  1. Users have the option of choosing “Wet Bulb” as the temperature method in their Ideal Conditions

  2. Every hourly forecast now includes WBGT measurements

  3. Hourly ratings show Perfect, Acceptable or Bad conditions using WBGT instead of Actual and Feels Like temperatures

You can download Apollo Weather from the App Store.

Read More
Axel Rivera Axel Rivera

Hello World!

My name is Axel Rivera. I’m an indie iOS developer living in Orlando, FL. I have over 10 years of experience building iOS apps in an enterprise environment, including tech startups, banks, government, and health care. Now I mostly freelance to support working on my iOS apps.

Hi there,

My name is Axel Rivera. I’m an indie iOS developer living in Orlando, FL. I have over 10 years of experience building iOS apps in an enterprise environment, including tech startups, banks, government, and health care. Now I mostly freelance to support working on my iOS apps.

This blog is a place for me to cover topics about iOS development. My goal goes beyond just coding, even if some content is technical. I want to assist other indie developers in creating great iOS apps by sharing tips, tools, and my experiences.

Welcome aboard!

Read More