The WWDC video makes it look approachable. You call requestAuthorization, set some shields, and you're done. That framing is missing about 70% of what you need to actually ship.
FamilyControls is three separate frameworks that each do a distinct job. If you treat them as one API you'll spend days confused about why things aren't working.
The three frameworks
FamilyControls is just the authorization layer. You call AuthorizationCenter.shared.requestAuthorization(for: .individual) and get a permission prompt. That's the whole framework. Without this approval, the other two won't do anything.
ManagedSettings is where the actual blocking happens. You get a ManagedSettingsStore and set properties on it — shield.applications, shield.webDomains, appCategories.blocked. These take effect immediately.
DeviceActivity is the scheduling and monitoring layer. You define a DeviceActivitySchedule with start and end times, attach a DeviceActivityEvent with usage thresholds, and the system calls your DeviceActivityMonitor extension when thresholds are crossed.
The entitlement is not a checkbox
For App Store distribution without an MDM profile, you need Apple to manually approve your app for FamilyControls. You submit a request form, describe your use case, and wait. Apple reads these. Saying "focus app" is not enough — be specific about what you're blocking and why.
Plan for 1–2 weeks of review time. I found out about this after I'd already built most of LockIn. Build your app, then apply, then wait while doing other things.
ManagedSettingsStore resets on reboot
This is the one that caught me most off guard. The store does not persist across device reboots. If a user restarts their phone while a focus session is active, the shields are gone when it comes back up.
The fix is to re-apply shields from your DeviceActivityMonitor extension's intervalDidStart callback. That extension gets woken by the system when a scheduled interval starts — including after a reboot that interrupted an active interval. So your extension needs to know the current session state independently.
class MyMonitor: DeviceActivityMonitor {
override func intervalDidStart(for activity: DeviceActivityName) {
// Re-apply shields from shared state
let store = ManagedSettingsStore()
let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp")
if let tokenData = sharedDefaults?.data(forKey: "blockedTokens"),
let tokens = try? JSONDecoder().decode(Set<ApplicationToken>.self, from: tokenData) {
store.shield.applications = tokens
}
}
}
The extension is a separate process
Your DeviceActivityMonitor extension runs in its own process, sandboxed away from your main app. It cannot read your main app's UserDefaults, its Core Data store, or anything else in your main container.
The only shared storage between your app and the extension is an App Group. You must set up an App Group entitlement in both targets, then read and write session state through UserDefaults(suiteName: "group.com.yourapp"). Every piece of data the extension needs — current session tokens, session duration, whatever — has to flow through that shared container.
The simulator is broken for this
Don't waste time trying to test ManagedSettings or DeviceActivity in the simulator. The simulator doesn't enforce shields. The DeviceActivityMonitor callbacks don't fire on schedule. The authorization prompt behaves differently. Test on a physical device from day one.
Also worth knowing: testing on your personal device means your own apps can get blocked. Keep a secondary test device or use a secondary Apple ID.
What actually shipped
LockIn uses FamilyControls for the one-time permission request, ManagedSettings to apply shields when a session starts, and DeviceActivity for scheduled focus blocks. The session state lives in an App Group so the monitor extension can re-apply shields after a reboot. It works, but I rebuilt the data flow twice before I understood that the extension was a separate process.
The API is capable. The documentation assumes you already understand the process boundary thing, which is a pretty significant assumption.