Summary
When using DeviceActivitySelectionViewPersisted, iOS's native FamilyActivityPicker silently auto-promotes individual app selections into a category-level selection once enough sub-items of the same category are chosen. The onSelectionChange payload reflects this collapse, which makes any user-facing counter or limit logic based on applicationCount / categoryCount misleading or incorrect.
Environment
react-native-device-activity: 0.6.1
- iOS: 26.0 (iPhone 13 mini, physical device)
- Xcode: 26
- Expo SDK 54, New Architecture enabled
- React Native version: latest matching SDK 54
Reproduction
Starting from an empty FamilyActivitySelection (selectionId \"my_selection\"):
- Open the picker
- Inside category Spiele/Games, tap App A individually (e.g. Clash of Clans)
onSelectionChange emits: `{"applicationCount":1,"categoryCount":0,"webDomainCount":0,"includeEntireCategory":false}` ✅ as expected
- Tap App B individually inside the same category (e.g. Mazery)
onSelectionChange emits: `{"applicationCount":0,"categoryCount":1,"webDomainCount":0,"includeEntireCategory":false}` ❌
Expected for step 3: `applicationCount: 2, categoryCount: 0` (two individual apps).
Actual: iOS auto-collapsed the two app tokens into a single category token. The picker visually still shows both apps with checkmarks, but the underlying FamilyActivitySelection no longer contains the individual applicationTokens — only the parent categoryToken.
Why this matters
We use the count for a freemium quota ("block up to N individual apps for free, unlimited with premium"). With the auto-promotion behavior, a user who selects 2 apps gets reported as "1 category selected" — making it impossible to:
- Show a correct progress counter (
X / N selected)
- Enforce a per-app limit
- Distinguish "user explicitly chose the whole category" from "user picked some apps and iOS merged them"
The metadata struct we get back (`applicationCount`, `categoryCount`, `webDomainCount`, `includeEntireCategory`) doesn't carry enough info to differentiate user intent.
What we tried
- Tracking the delta between consecutive
onSelectionChange events and inferring auto-promotion when applicationCount drops while categoryCount rises — works during a single picker session but is lost on app restart since the persisted selection only contains the collapsed form
- Inspecting the
includeEntireCategory flag — appears unrelated; it's false in both "user picked Alle" and "iOS auto-promoted" cases on iOS 26
- Looking at the native side (
ReactNativeDeviceActivityModule.swift, Shared.swift) — the application/category/webDomain tokens are read but only counts are exposed across the bridge
Feature request / questions
-
Is this iOS behavior documented somewhere? When does iOS promote individual selections to a category? Always when all visible apps of a category are picked? When N items are picked? Does it depend on Family Controls authorization scope?
-
Could the library expose additional info via onSelectionChange? For example:
- The last-tapped token type (
app | category | webDomain)
- A boolean like
lastChangeWasAutoPromotion based on internal state diffing
- Optionally the raw serialized selection token so consumers can do their own diffing across sessions
-
Workaround suggestions? Has anyone solved counter-style UX ("X of 3 apps selected") on top of this picker?
Happy to put together a PR if there's a desired direction. Thank you for the library — it's been very helpful for our Deep Work app.
Summary
When using
DeviceActivitySelectionViewPersisted, iOS's nativeFamilyActivityPickersilently auto-promotes individual app selections into a category-level selection once enough sub-items of the same category are chosen. TheonSelectionChangepayload reflects this collapse, which makes any user-facing counter or limit logic based onapplicationCount/categoryCountmisleading or incorrect.Environment
react-native-device-activity: 0.6.1Reproduction
Starting from an empty
FamilyActivitySelection(selectionId\"my_selection\"):onSelectionChangeemits: `{"applicationCount":1,"categoryCount":0,"webDomainCount":0,"includeEntireCategory":false}` ✅ as expectedonSelectionChangeemits: `{"applicationCount":0,"categoryCount":1,"webDomainCount":0,"includeEntireCategory":false}` ❌Expected for step 3: `applicationCount: 2, categoryCount: 0` (two individual apps).
Actual: iOS auto-collapsed the two app tokens into a single category token. The picker visually still shows both apps with checkmarks, but the underlying
FamilyActivitySelectionno longer contains the individualapplicationTokens— only the parentcategoryToken.Why this matters
We use the count for a freemium quota ("block up to N individual apps for free, unlimited with premium"). With the auto-promotion behavior, a user who selects 2 apps gets reported as "1 category selected" — making it impossible to:
X / N selected)The metadata struct we get back (`applicationCount`, `categoryCount`, `webDomainCount`, `includeEntireCategory`) doesn't carry enough info to differentiate user intent.
What we tried
onSelectionChangeevents and inferring auto-promotion whenapplicationCountdrops whilecategoryCountrises — works during a single picker session but is lost on app restart since the persisted selection only contains the collapsed formincludeEntireCategoryflag — appears unrelated; it'sfalsein both "user picked Alle" and "iOS auto-promoted" cases on iOS 26ReactNativeDeviceActivityModule.swift,Shared.swift) — the application/category/webDomain tokens are read but only counts are exposed across the bridgeFeature request / questions
Is this iOS behavior documented somewhere? When does iOS promote individual selections to a category? Always when all visible apps of a category are picked? When N items are picked? Does it depend on Family Controls authorization scope?
Could the library expose additional info via
onSelectionChange? For example:app|category|webDomain)lastChangeWasAutoPromotionbased on internal state diffingWorkaround suggestions? Has anyone solved counter-style UX ("X of 3 apps selected") on top of this picker?
Happy to put together a PR if there's a desired direction. Thank you for the library — it's been very helpful for our Deep Work app.