●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
Controlling HomeKit Accessories from Your Rork Max Native App — Permission and State-Sync Lessons from Indie Shipping
A walkthrough of adding HomeKit to the native Swift app Rork Max generates: listing and controlling accessories, from the permission dialog wording to the state-sync traps. Covers the territory React Native struggles to reach, from a working indie developer's perspective.
Smart-home requests reach even small indie products in surprising ways. "When the bedroom light is off at night, I'd love the screen to switch to a dark theme too." When a user said that, borrowing the HomeKit accessories the iPhone already manages felt far more natural than talking to devices myself.
But HomeKit gets heavy the moment you try to reach it through a React Native bridge. Because Rork Max generates native Swift apps in the browser, importing HomeKit puts the Apple-published types — from listing accessories to controlling them — right at your fingertips. This article walks through layering HomeKit onto the generated code, centered on the two hard parts: permissions and state synchronization.
Why reach this natively instead of via React Native
HomeKit is designed to be called directly from Swift or Objective-C. You can technically touch it from Expo through a community bridge, but you get jerked around by the bridge's coverage every time accessory types grow. I once tried to handle a dimmable light in an Expo test app, found the bridge didn't expose the brightness characteristic for writing, and ended up escaping to native code.
What Rork Max outputs is a plain native app, so characteristics like HMCharacteristicTypePowerState are available from the start. But precisely because it's native, you fail quietly in both review and production if you don't get the permission dialog and HomeKit's asynchronous state reflection right. Let's take them in order.
Step 1: Declare the capability and purpose minimally
The first thing is declaring permission. After enabling the HomeKit capability in your Rork Max project settings, write the purpose in Info.plist. An abstract sentence here gets you questioned in review about why the app needs it.
<key>NSHomeKitUsageDescription</key><string>Used to switch the screen theme automatically in sync with turning your home lights on and off.</string>
The point: this text appears verbatim in the permission dialog, so state concretely "what" you operate and "why." In my case, the first submission came back with a note that the wording was vague. Writing it honestly ends up building user trust too.
✦
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
✦Concrete Info.plist usage strings and Swift code for HMHomeManager initialization timing and an NSHomeKitUsageDescription that survives App Store review
✦How to diagnose the HomeKit-specific production symptom of 'accessories never appear' down to its real cause: delegate registration order
✦A state-sync pattern that reliably reflects a light toggle on a real device, with the calls I made shipping apps as an indie developer
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.
Step 2: Register the HMHomeManager delegate, then wait
This is where most people stumble. Right after you create HMHomeManager, homes returns an empty array. That doesn't mean "no home exists" — it means it's still syncing with iCloud to load the home layout.
The correct flow is to create the manager, register the delegate first, and wait for homeManagerDidUpdateHomes(_:) to fire.
import HomeKitfinal class HomeController: NSObject, HMHomeManagerDelegate { private let manager = HMHomeManager() private(set) var primaryHome: HMHome? var onReady: (() -> Void)? override init() { super.init() // Reading homes before the delegate is set always returns empty manager.delegate = self } func homeManagerDidUpdateHomes(_ mgr: HMHomeManager) { // Only here is the home layout finalized primaryHome = mgr.primaryHome ?? mgr.homes.first onReady?() }}
Reading manager.homes.first right after init and then worrying that "accessories never appear" is almost always this pattern. I hit the same trap in my first implementation and fixed it by waiting for the delegate.
Step 3: Find the light accessory and pull out its characteristic
Once the home layout is settled, pull the service you want to control (the light) and the characteristic representing its power and brightness out of the accessories.
func powerCharacteristic(in home: HMHome) -> HMCharacteristic? { for accessory in home.accessories { for service in accessory.services where service.serviceType == HMServiceTypeLightbulb { for ch in service.characteristics where ch.characteristicType == HMCharacteristicTypePowerState { return ch } } } return nil // Guard against finding no lights at all}
Mixing up serviceType and characteristicType produces a confusing failure: the write succeeds but the real light doesn't react. Power is PowerState, brightness is Brightness — checking each characteristic type one by one is the reliable path.
Step 4: Write the toggle, await the result, then update the UI
Writing to a characteristic is asynchronous. Firing writeValue and forgetting it makes the UI toggle move first while the real device doesn't, creating a state mismatch. Update the UI only after the write completes.
func setPower(_ on: Bool, _ ch: HMCharacteristic) async throws { try await ch.writeValue(on) // Wait until it completes // Confirm the UI state only on success await MainActor.run { self.isOn = on }}
At first I changed local state the instant the toggle was tapped. When a write failed, that left "the screen says on but the light is still off." After switching to await completion, this kind of support question dropped visibly.
Step 5: Subscribe to external changes to prevent drift
Lights change from outside this app too — the stock Home app or a voice command. If the state changes externally while your app is open and you don't ingest it, the display stays stale. Subscribe to characteristic updates via HMAccessoryDelegate to reflect outside changes.
extension HomeController: HMAccessoryDelegate { func accessory(_ accessory: HMAccessory, service: HMService, didUpdateValueFor ch: HMCharacteristic) { guard ch.characteristicType == HMCharacteristicTypePowerState else { return } if let value = ch.value as? Bool { Task { @MainActor in self.isOn = value } } }}
To start subscribing, besides setting accessory.delegate = self, you must call enableNotification(true, for:) per characteristic. Forget this and no external change ever flows in, leaving you puzzling over why it "won't sync."
Deciding how far to build as an indie developer
With the implementation so far, the on/off toggle and ingesting external changes stay stable. Trying to cover every accessory type, though, spikes the maintenance burden. The approach I took as an indie developer was to narrow to the single type my app truly needs to sync with (here, light power) and not force support for the rest. HomeKit increases your review accountability the moment you get greedy with coverage, so for a small AdMob-centric app the cost-benefit stops adding up.
Smart-home integration looks flashy, but its essence is letting the app sit quietly alongside the user's daily life. Now that Rork Max has opened a native door, polishing one integration carefully — rather than piling on features — is what pays off. I hope this helps anyone working on the same problem.
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.