Inspiration

Most of my workouts never happened, and most of my screen time did. The two problems have the same shape: a few free minutes at home, a phone in hand, and a reflex that opens Instagram instead of doing ten push-ups. Workout apps like Strong are built for the gym — sets, plates, an hour. I wanted the opposite: a logger light enough for a two-minute gap, and a way to point the scrolling reflex at the floor.

So GapFit does one odd thing: when you try to open a blocked app, Screen Time shows a shield, and the shield's only button is "one set to unlock."

What it does

  • Log in two taps. Your last reps carry over, so a set is one tap to confirm; 12 bodyweight presets (push-ups, squats, plank, pull-ups…), optional weight and RPE.
  • Personal records, automatically. Max reps and max volume are detected per exercise and marked with a trophy.
  • Sticks around: daily streaks, Home Screen and Lock Screen widgets, and a one-tap "log my last set" widget action that records without opening the app.
  • Routines: templates so "what should I do today" never blocks you (one free, unlimited with Pro).
  • App Block (GapFit Pro): pick the apps to block. Opening one shows the shield; clear the challenge — a fixed exercise, a random one, or any exercise you choose — and the app unlocks for a window you set. A Live Activity counts down on the Lock Screen, then the block returns by itself.
  • Weekly report (Pro): an 8-week rep-trend chart and the number that matters — how many times you worked out instead of scrolling.
  • Private by design: training data stays on the iPhone; the list of blocked apps never leaves the device.
  • Japanese and English.

How we built it

  • SwiftUI + SwiftData, with the store in an App Group container so the widgets and extensions read the same data (with a one-time migration from the original store).
  • The Screen Time stack: the FamilyControls app picker in the app, a ManagedSettings shield, and three extensions — a ShieldConfiguration extension that draws the challenge, a ShieldAction extension that handles the button, and a DeviceActivityMonitor that ends the unlock window and re-applies the block.
  • Shield → app hand-off. Shield extensions cannot open the host app, so the action posts a local notification that deep-links to gapfit://unlock, where the challenge UI lives.
  • ActivityKit for the unlock countdown on the Lock Screen, WidgetKit + App Intents for the one-tap "log last set" (LogLastSetIntent), Swift Charts for the weekly report.
  • Onboarding is a 12-screen questionnaire flow modeled on the highest-converting subscription apps (goal → pain → empathy → solution → body parts → notification priming → processing → exercise demo → generated menu → paywall), with the Screen Time permission deferred to the block-settings screen where it makes sense.
  • Monetization with RevenueCat: one pro entitlement, RevenueCat's paywall UI, Test Store key in Debug and the production key in Release with a guard against shipping a test_ key.
  • Tuist project, String Catalog (ja/en), iOS 17+.

Challenges we ran into

  • One string blocked the whole upload. The shield-configuration extension's NSExtensionPointIdentifier must be com.apple.ManagedSettingsUI.shield-configuration-service — with ManagedSettings (no UI) the binary passes locally and App Store Connect rejects it with validation error 90349.
  • The Simulator can't do Screen Time. Family Controls never authorizes there, so every shield change was a device build; the entitlement also needs Apple's distribution approval before a store build can be signed.
  • DeviceActivity's floor is 15 minutes. Unlock windows shorter than that simply can't be scheduled, so the product defines "a short break" as 15+ minutes.
  • Review rejection 3.1.2(c) for subscription disclosure. While fixing it we cancelled and recreated the review submission through the API — and discovered that an API-created submission silently drops the in-app purchases. The final submission had to be made from the web UI so the three products were bundled with the version.
  • Extensions compile everything under Shared/, so the widget and monitor extensions choked on FamilyControls imports until the code was split into a WidgetShared/ module without them.
  • Archiving on a macOS 27 beta host: Xcode 26.6 for the archive, BuildMachineOSBuild patched before export.

What we learned

  • The cheapest habit trigger is one the user already has. Nobody forgets to open Instagram.
  • Screen Time is three processes pretending to be one app. Decide early which process owns the shield.
  • Store plumbing is part of the product: a wrong extension identifier or an API-made submission can cost a week.

What's next

  • Camera rep counting with Vision pose estimation so the shield can verify the set.
  • A strict mode with no "skip" and random-exercise unlocks for people who game the fixed one.
  • Routine sharing between friends.

Built With

  • activitykit
  • app-intents
  • device-activity
  • family-controls
  • managed-settings
  • revenuecat
  • revenuecat-paywalls
  • screen-time-api
  • string-catalog
  • swift
  • swift-charts
  • swiftdata
  • swiftui
  • tuist
  • user-notifications
  • widgetkit
  • xcode
Share this project:

Updates

Submission history