Return...

A zen meditation and focus timer across five screens: iPhone, iPad, Apple Watch, Apple TV, and Mac.

Shipped April 21, 2026. One codebase. Twenty-seven languages including Arabic and Hebrew. Four themes, three bells, zero analytics. What follows is how it came together: the technical choices, the design tradeoffs, and the long quiet process of editing hundreds of AI-generated water drops down to one.

Universal

One codebase, five screens.

Return is the first app I've shipped that runs across every Apple screen class from a single Xcode project: iPhone, iPad, Apple Watch, Apple TV, and Mac. Fifty-seven Swift files, about 12,700 lines of code, and zero external dependencies. Pure SwiftUI, AVFoundation, HealthKit, ActivityKit, and WidgetKit.

The naïve way to do this is one universal TimerManager with #if branches for every platform difference. I didn't. Return ships three timer classes (TimerManager on iOS and macOS, TVTimerManager on tvOS, WatchTimerManager on watchOS) that share state semantics but respect what each platform is actually good at. Live Activities only on iOS. HealthKit only where the API exists. Extended runtime sessions only on Watch. Each manager is shorter and more honest than a single polymorphic class would be.

Return running on Apple TV with the Fire theme
Apple TV
Return running on macOS with the Fire theme, windowed on a dark desktop
Mac
Return running on iPad Pro 13-inch with the Fire theme
iPad
Return running on iPhone 17 Pro Max with the Fire theme
iPhone
Return running on Apple Watch Series 11 with the Fire theme
Watch

Shared where it matters.

A single Shared/ folder carries the pieces that all targets need to agree on: the MeditationSession data model, the SessionStore iCloud wrapper, and SessionHistoryView. Settings sync across Watch and phone through an App Group (group.com.941apps.Return). The rest is platform-specific on purpose.

The clearest example is the one line that decides whether a session has already been logged to HealthKit. iPhone writes directly, so "synced" is true the moment the session ends. Mac and TV can't write to HealthKit at all, so "synced" is false until the iPhone picks the pending session up later. Same intent, opposite boolean, one #if:

Swift · TimerManager.swift:120-138
/// Save session to SessionStore for cross-device sync and HealthKit syncing
private func saveSessionToStore(startTime: Date, endTime: Date) {
    // On iOS: if healthKitEnabled, we save directly to HealthKit, so mark as synced
    // On Mac: if healthKitEnabled, we want to sync to iPhone, so mark as NOT synced
    #if os(iOS)
    let alreadySynced = settings.healthKitEnabled
    #else
    let alreadySynced = !settings.healthKitEnabled
    #endif

    let session = MeditationSession(
        startDate: startTime,
        endDate: endTime,
        sourceDevice: .current,
        syncedToHealthKit: alreadySynced
    )

    SessionStore.shared.addSession(session)
}

I come back to that pattern constantly: the fewest lines that still make the intent legible. When the same boolean means different things on different platforms, write it as different booleans. The #if becomes part of the documentation.

Localized

Twenty-seven languages, and right-to-left support.

Return is the first Apple app I've shipped in every language I cared about. Twenty-seven locales went through a full review pass, including Arabic and Hebrew. All of it lives in one Localizable.xcstrings file, which is less heroic than it sounds. Xcode does most of the work if you agree to stop hand-rolling strings.

Return home screen, Water theme, English
EnglishHome · Water
Return home screen, Fire theme, Japanese
日本語Home · Fire
Return home screen, Forest theme, Simplified Chinese
简体中文Home · Forest
Return settings screen, German
DeutschSettings
Return HealthKit permission screen, Korean
한국어HealthKit

RTL is a free win if you stop fighting it.

SwiftUI treats .leading and .trailing as semantic directions rather than .left and .right as fixed ones. Lay a screen out in semantic directions once, and the same screen mirrors automatically in Arabic, Hebrew, Persian, or Urdu without a dedicated code path. Settings labels flip, the back chevron reverses, switch positions invert. Theme icons (drop, flame, leaf) stay put. I did not write a line of RTL code for this behavior.

Return home screen, Forest theme, English, left-to-right layout
English · LTR
Return home screen, Forest theme, Arabic, right-to-left layout
Arabic · RTL
Return home screen, Forest theme, Hebrew, right-to-left layout
Hebrew · RTL

One exception I caught shipping: SwiftUI applies layout direction to Text views too, which meant the first cut of the Arabic and Hebrew screenshots had the timer reading "00:02" instead of "20:00" — Latin digits laid out right-to-left. One .environment(\.layoutDirection, .leftToRight) modifier on every Text view that holds time or numeric content fixes it. The screenshots above are from the release that ships with that modifier in place.

The screenshot set was generated by fastlane running the same UI tests with different -AppleLanguages arguments. The app's own effectiveLocale pattern reads the flag, rebuilds the view hierarchy, and captures the result. One helper, twenty-seven locales, four device classes, all in one overnight run.

Swift · ReturnWatchApp.swift:92-111
/// The locale to use for the app - either user-selected or system default
/// In snapshot mode, always use system language (set by -AppleLanguages)
/// to allow screenshot generation for different locales
private var effectiveLocale: Locale {
    if isSnapshotMode || appLanguage.isEmpty {
        if let preferredLanguage = Locale.preferredLanguages.first {
            return Locale(identifier: preferredLanguage)
        }
        return .current
    }
    return Locale(identifier: appLanguage)
}

var body: some Scene {
    WindowGroup {
        WatchContentView()
            .preferredColorScheme(.dark)
            .environment(\.locale, effectiveLocale)
            .id(appLanguage) // Force rebuild when locale changes
    }
}

The .id(appLanguage) is the detail that earns its keep. Without it, SwiftUI caches the old view hierarchy and strings don't refresh when you flip languages at runtime. With it, the whole tree discards and rebuilds, and everything re-reads its localized strings automatically. One line, a category of bugs deleted.

HealthKit

Mindful minutes, finally.

Apple's native Watch Mindfulness app caps built-in Reflect and Breathe sessions at five minutes. The HealthKit API itself has no such cap. It will happily accept any HKCategorySample where the end date is after the start date. The limit lives in the UI, not the system. Return puts a 5-to-60-minute picker on every device and writes whatever you actually sat for.

Swift · HealthKitManager.swift:92-103
/// Save a mindful session with the given start and end time
func saveMindfulSession(start: Date, end: Date) async -> Bool {
    guard isAvailable else { return false }

    // Don't save if end is before or equal to start
    guard end > start else { return false }

    let sample = HKCategorySample(
        type: mindfulType,
        value: HKCategoryValue.notApplicable.rawValue,
        start: start,
        end: end
    )
    ...
}

The only validation is end > start. That is all HealthKit itself validates. Apple's API has always been willing to log a forty-five-minute meditation. The button to request one was just missing.

Cross-device without HealthKit on three of them.

Mac and Apple TV don't have HealthKit at all. The obvious response is "then don't bother logging sessions there." The less obvious, correct response is to log them anyway, to iCloud Key-Value Store, and let the phone pick them up the next time it wakes. Return's SessionStore is the shared store, MeditationSession.syncedToHealthKit is the pending flag, and HealthKitManager.syncPendingSessions() runs every time the iOS app returns to the foreground.

iPhone iPad Apple Watch Apple TV Mac
SessionStore
iCloud Key-Value Store
Pending sessions
iPhone writes to HealthKit ♥
Apple Health Mindful Minutes bar chart showing 20-minute average over a month
Apple Health Mindful Minutes, bar view. Apple's own Mindfulness app tops out at a five-minute Reflect session. The underlying store does not care what you write to it.
Apple Health Mindful Minutes calendar showing 18 days of practice in the past 4 weeks
Same data, calendar view: 18 days in the past 4 weeks, every session logged from Return.
Return Session History screen showing a list of 20-minute meditation sessions
Return's own session history. Every device contributes, and every session carries a source marker.

This is the piece I think Apple should ship themselves: a proper cross-platform Mindful Minutes writer that doesn't require a phone to be active when you want to meditate on a Mac. Until they do, Return does.

Generative

Where the water came from.

Four themes. Four ambient loops. Three bells. All of it generated, most of it thrown away. The videos are Midjourney, the audio is ElevenLabs, and the work that mattered wasn't the prompting. It was the editing. Looking at a grid of two hundred water drops and picking the one that loops cleanly without a visible seam. Listening to forty variations of a temple bell until one has the right attack and the right decay and doesn't sound like a phone notification.

Midjourney contact sheet: hundreds of water drop variations, a few marked with hearts and play triangles
Water · 128 shown
Midjourney contact sheet: dozens of fire variations
Fire · 96 shown
Midjourney contact sheet: tree canopy and leaf variations
Forest · 60 shown
Midjourney contact sheet: cloud and sky exploration that did not ship
Unreleased exploration · 128 shown

Every tile is a generation. The hearts are the ones that survived a first pass. The play triangles are the ones I took to video. Four themes shipped. Everything else stayed in the grid, and that's the whole point of the process: the ratio matters.

The bells followed the same arc in audio. Prompt, listen, refine, prompt again. I kept three: Singing Bowl, Temple Bell, Soft Chime. Each one iterated until it stopped sounding synthetic.

I won't pretend to count the total generations. Hundreds per theme is honest. The discipline isn't in the prompts. It's in throwing away everything that's merely good, and keeping only the ones that can sit behind a timer for twenty silent minutes without ever becoming the thing you notice.

Restraint

What isn't in Return.

Return is not Calm. It is not Headspace. There is no British narrator easing you into a body scan. There is no cartoon avatar celebrating your streak. There is no subscription that unlocks new guided programs. Return is a timer. The idea is that if you already have a practice, you don't need a teacher in the app. You need a tool that holds time for you and gets out of the way.

  • No guided voice or narration
  • No streaks, scores, or gamification
  • No subscription or in-app purchases
  • No ads, ever
  • No analytics; the app tracks nothing
  • No social login or sharing
  • No nag screens, no cold-start modals
  • No dark patterns in the IAP flow, because there is no IAP flow

What is in Return, kept deliberately small: four repeat modes (Once, Until Stopped, Until Time, Repeat N Times), a two-second breathing pause between cycles, one-to-three bell rings at each transition, a choice of three bells, four themes, HealthKit opt-in, and a language picker. That's the entire product.

The cost of being this strict shows up in the settings model. Every user-facing preference is clamped to a valid range by the property itself, not by UI validation. UI validation is another dark pattern if you're not careful. The bellRepeatCount getter can't return anything except 1, 2, or 3. Writing 0 or 47 into the underlying @AppStorage silently clamps back to the allowed range.

Swift · Settings.swift:74-81
@ObservationIgnored
@AppStorage("bellRepeatCount") private var _bellRepeatCount = 1

/// Validated bell repeat count (1-3)
var bellRepeatCount: Int {
    get { max(1, min(3, _bellRepeatCount)) }
    set { _bellRepeatCount = max(1, min(3, newValue)) }
}

Return is $2.99. You pay for it once and you own it. No server costs to support, no subscription to renew, no analytics pipeline watching what you do. The product is the product. If you want the longer version of why I keep building apps this way, read Minimum Worthy Product and The Steve Test. The short version lives in this section.

Return.

Available now on the App Store for iPhone, iPad, Apple Watch, Apple TV, and Mac.