●BUILD — Rork generates native iOS/Android apps with React Native (Expo) from a plain-English description into deployable code●MAX — Rork Max outputs native Swift, targeting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●MAX — Real Swift output balances performance and App Store eligibility — currently the only tool doing this●DEPLOY — Shareable test links and automatic iOS/Android builds remove the need for separate build pipelines●PRICE — Free to start, with paid plans from $25/month — practical for solo devs from prototype to release●FOCUS — Unlike web-first tools like Bolt or Lovable, Rork specializes in mobile apps●BUILD — Rork generates native iOS/Android apps with React Native (Expo) from a plain-English description into deployable code●MAX — Rork Max outputs native Swift, targeting iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●MAX — Real Swift output balances performance and App Store eligibility — currently the only tool doing this●DEPLOY — Shareable test links and automatic iOS/Android builds remove the need for separate build pipelines●PRICE — Free to start, with paid plans from $25/month — practical for solo devs from prototype to release●FOCUS — Unlike web-first tools like Bolt or Lovable, Rork specializes in mobile apps
Show Your Rork App's Progress on the Lock Screen and Dynamic Island
Add Live Activities and ActivityKit to a Swift app generated by Rork Max so a meditation timer or delivery status appears on the lock screen and Dynamic Island, with working code and the submission gotchas to watch for.
The thing that bothered me most about my own morning meditation app was reopening it just to see the remaining time. You close your eyes, settle your breathing, then light up the screen and tap back in. That alone breaks the focus. As an indie developer running a handful of calm-and-wellness apps on the App Store, I have learned that timer features live or die by how much of that reopening you can remove.
Rork Max emits real Swift, so this maps cleanly onto iOS Live Activities. You put a remaining-time card on the lock screen, and on supported devices it folds into the Dynamic Island too. Using a meditation timer as the example, this article walks through an ActivityKit implementation in the order I actually hit the problems. The same approach carries over to delivery tracking, sports scores, and anything else that is "one thing you keep glancing at for a short while."
What belongs in a Live Activity and what does not
Draw the line first. Live Activities exist to surface "a single event that changes moment to moment over minutes to a few hours." Meditation time left, a cooking timer, a ride arriving, an order being prepared — those fit.
Anything that should have been a notification does not belong here. A list-style message like "you have 3 new articles" clashes with the design intent and gets flagged in review. My first version greedily crammed several states onto one card, and the remaining time — the thing people most want — ended up tiny. Picking one star for the surface is the starting point.
Build the container first: Info.plist and a Widget Extension
ActivityKit needs two pieces of setup.
Add NSSupportsLiveActivities set to YES in the app's Info.plist. Without it, the Activity.request call below fails silently.
The Live Activity UI lives inside a Widget Extension. Add a Widget Extension target in Xcode and implement an ActivityConfiguration.
If the project Rork Max generated has no Widget Extension, you need to add one yourself. Do not leave this to generation — check it by hand.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Turn a countdown or meditation timer into something users can check on the lock screen and Dynamic Island without reopening the app
✦Get the three ActivityKit steps (start/update/end) plus the 16KB and 8-hour limits you need to know up front, all in working code
✦Avoid the push-update trap where updates work in TestFlight but silently stop in production
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
ActivityKit describes an activity's data with ActivityAttributes. The key is to separate values that never change (the session type) from values that get rewritten on each update (seconds remaining).
import ActivityKitstruct MeditationAttributes: ActivityAttributes { // Dynamic state rewritten on each update public struct ContentState: Codable, Hashable { var remainingSeconds: Int var phase: String // "breathing" / "rest" etc. } // Static values fixed at session start var sessionTitle: String var totalSeconds: Int}
Watch the size here: if the JSON in ContentState exceeds 16KB, the update is rejected. Do not carry heavy data like a Base64 image string directly — pass an identifier and draw it widget-side. Skip this and you get a hard-to-reproduce bug where small payloads pass in TestFlight but the moment you push a long string in production the update is dropped.
The three steps: start / update / end
The lifecycle is just start, update, and end. Starting a meditation session looks like this.
import ActivityKitfinal class MeditationLiveActivity { private var activity: Activity<MeditationAttributes>? func start(title: String, total: Int) { // Always confirm Live Activities are enabled on the device guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } let attributes = MeditationAttributes(sessionTitle: title, totalSeconds: total) let initial = MeditationAttributes.ContentState( remainingSeconds: total, phase: "breathing" ) do { activity = try Activity.request( attributes: attributes, content: .init(state: initial, staleDate: nil), pushType: nil // local updates only; push covered below ) } catch { // Do not swallow this — always log it print("Live Activity start failed: \(error)") } } func update(remaining: Int, phase: String) { let state = MeditationAttributes.ContentState( remainingSeconds: remaining, phase: phase ) Task { await activity?.update(.init(state: state, staleDate: nil)) } } func end() { Task { await activity?.end(nil, dismissalPolicy: .immediate) } }}
Skip the areActivitiesEnabled check and Activity.request throws on the devices of users who turned the feature off in Settings. I swallowed that error at first, and tracking down why "only some users never see the timer" cost me half a day. Always log the exception — that is basic production hygiene, not specific to Live Activities.
Drawing the lock screen versus the Dynamic Island
In the Widget Extension you draw the same data in two layouts: the larger lock-screen card and the folded Dynamic Island presentation.
Whether you add monospacedDigit() decides whether the number jitters every time a digit drops. Small detail, but a flickering display during meditation is noise, so for any timer I always use monospaced digits.
Local updates or push updates — which to pick
There are two update paths: local updates, where you call activity.update while the app is alive in the foreground or background, and push updates, where the server injects state through APNs.
For a meditation timer, where the user is watching the screen or away for a few minutes at most, local updates are enough. But when you want to keep updating even after the app is fully terminated — like delivery tracking — you must specify pushType: .token and send from APNs.
A common report: it worked with local updates during development, but in production "the remaining time freezes when I close the app." That is not a bug; it is how local updates work. Decide up front to switch to push updates if you need post-termination updates, and you avoid a rebuild later.
The 8-hour ceiling and avoiding orphaned cards
iOS automatically ends a Live Activity at most 8 hours after it starts. On top of that, if your app forgets to call end, a frozen timer lingers on the lock screen and unsettles users.
My rule is simple: enumerate every path that ends a session and make sure end is called no matter which one fires. I always verify the three paths — normal completion, user interruption, and recovery from a crash. After a crash especially, scanning Activity.activities at launch to clear stranded activities keeps things safe.
func cleanupStaleActivities() { for activity in Activity<MeditationAttributes>.activities { Task { await activity.end(nil, dismissalPolicy: .immediate) } }}
What I check before release
For submission I keep a three-point checklist. Live Activities look different on supported devices (Dynamic Island needs iPhone 14 Pro or later) versus older ones, so on-device testing is not optional.
Does a user who turned Live Activities off in Settings avoid a crash?
When updates cannot arrive in airplane mode or no signal, does the display stay coherent?
When a session is canceled midway, does the lock-screen card reliably disappear?
Plain checks, but passing them cuts both review rejections and low-star reviews. Even for apps earning through AdMob, I find this kind of polish lifts retention, which in turn grows impressions.
Once you can quietly place the remaining time on the lock screen, the app shifts from something you open to something that sits beside you. As a next step, write down the one piece of information in your own app that users would want to check without opening it. That becomes the first star you put into a Live Activity.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.