I'm writing an app that uses Family Control. Most of the functionality has already been debugged, including in the call try deviceActivityCenter.startMonitoring(activityName, during: schedule, events: [ eventName: event ]) Works fine on my IOS17 device, but on my old IOS16 device I found that it can't be called, like intervalDidStart, intervalDidEnd, eventDidReachThreshold etc can't be called. I checked some information, https://forums.developer.apple.com/forums/thread/724243 delete the Build Settings mentioned in this link, and change the ios Deployment Target to ios16, and found that it didn't work. Don't know where the problem is?
Family Controls
RSS for tagPrevent access to the Screen Time API without guardian approval and provide opaque tokens that represent apps and websites.
Posts under Family Controls tag
186 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I'm developing an iOS app that uses ScreenTime API.
Locally, everything works as expected - I have two physical devices in one Apple Family, and calling AuthorizationCenter.shared.requestAuthorization(for: .child) on a child device shows standard authorization dialog.
Currently the app is in a review by Apple, and they are saying they can't authorize as a child. From logs, and a screenshots provided by them, I see that on AuthorizationCenter.shared.requestAuthorization(for: .child) call, FamilyControlsError.restricted error is being thrown.
I didn't encounter this error once during development and can't simulate this issue now either. Tried everything - authorization on account that is not a child, not in a family, tried restricting everything possible for a child's device from ScreenTime settings. This specific error is never triggered.
Does anyone have idea why this error could happen? My last guess is the review team is using an enterprise account and that somehow intercepts with ScreenTime settings. But I don't have an enterprise account to test this theory.
Is there a way for me to programmatically check whether a user has disabled my app's access to Screen Time?
I currently request Family Controls / Screen Time access upon download by calling the following:
AuthorizationCenter.shared.requestAuthorization(for: .individual)
I need to be able to detect, though, when a user has gone into Settings -> Screen Time and disabled my app's Screen Time access. I tried calling the following:
AuthorizationCenter.shared.authorizationStatus
But it appears to have a value of Approved even after I turn off my app's Screen Time access.
Is there any way to accurately detect this in the code?
I am developing a project with the Screen Time API, and I cannot understand why the methods inside DeviceActivityMonitor extension are not being called. Some points to note:
I do start by requesting authorization from the User (this is working as expected when opening the app)
Both the DeviceActivityMonitor Extension and the main app are under the same App Group
I have added Family Controls capability to both targets and have the developer account to use it.
I start by calling center.startMonitoring()
I have overridden the original methods.
Yet, when startMonitoring is called, there is no action that is supposed to be taken that is being taken, and as far as I can tell, the methods aren't even being called.
Here are some relevant snippets of code:
// EXT
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
let store = ManagedSettingsStore(named: .daily)
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
let store = ManagedSettingsStore(named: .daily)
store.clearAllSettings()
}
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
super.eventDidReachThreshold(event, activity: activity)
let store = ManagedSettingsStore(named: .daily)
let model = BeeFreeModel.shared
let applications = model.selectionToDiscourage.applicationTokens
let categories = model.selectionToDiscourage.categoryTokens
let webDomains = model.selectionToDiscourage.webDomainTokens
store.shield.applications = applications.isEmpty ?
nil : applications
store.shield.applicationCategories = categories.isEmpty ?
nil : ShieldSettings.ActivityCategoryPolicy.specific(categories)
store.shield.webDomains = webDomains.isEmpty ?
nil : webDomains
}
}
// APP
extension DeviceActivityName {
static let daily = Self("daily")
}
extension DeviceActivityEvent.Name {
static let discouraged = Self("discouraged")
}
let schedule = DeviceActivitySchedule(
intervalStart: DateComponents(hour: 0, minute: 0, second: 0),
intervalEnd: DateComponents(hour: 23, minute: 59, second: 59),
repeats: true
)
class BeeFreeSchedule {
static public func setSchedule() {
print("Setting schedule...")
print("Hour is: ", Calendar.current.dateComponents([.hour, .minute], from: Date()).hour!)
let events: [DeviceActivityEvent.Name: DeviceActivityEvent] = [
.discouraged: DeviceActivityEvent(
applications: BeeFreeModel.shared.selectionToDiscourage.applicationTokens,
categories: BeeFreeModel.shared.selectionToDiscourage.categoryTokens,
webDomains: BeeFreeModel.shared.selectionToDiscourage.webDomainTokens,
threshold: BeeFreeModel.shared.thresholdToDiscourage
)
]
let center = DeviceActivityCenter()
do {
print("Try to start monitoring...")
// Call startMonitoring with the activity name, schedule, and events
try center.startMonitoring(.daily, during: schedule, events: events)
} catch {
print("Error monitoring schedule: ", error)
}
}
}
// APP
class BeeFreeModel: ObservableObject {
// Import ManagedSettings to get access to the application shield restriction
let store = ManagedSettingsStore()
//@EnvironmentObject var store: ManagedSettingsStore
@Published var selectionToDiscourage: FamilyActivitySelection
@Published var thresholdToDiscourage: DateComponents
@Published var setOfApps: [String]
init() {
selectionToDiscourage = FamilyActivitySelection()
thresholdToDiscourage = DateComponents()
var setOfAppIdentifiers: Set<String?> = Set<String?>()
setOfApps = [String]()
if selectionToDiscourage.applicationTokens.isEmpty {}
else {
for application in BeeFreeModel.shared.selectionToDiscourage.applications {
setOfAppIdentifiers.insert(application.localizedDisplayName)
setOfApps = setOfAppIdentifiers.compactMap { $0 }.sorted()
}
}
}
class var shared: BeeFreeModel {
return _BeeFreeModel
}
func setSelection() {
let applications = BeeFreeModel.shared.selectionToDiscourage
}
func changeThreshold(threshold: DateComponents) {
thresholdToDiscourage = threshold
}
}
// APP
/// ...
Button("Select Apps to Discourage") {
isDiscouragedPresented = true
}
.familyActivityPicker(isPresented: $isDiscouragedPresented, selection: $model.selectionToDiscourage)
}
.onChange(of: model.selectionToDiscourage) {
BeeFreeModel.shared.setSelection()
}
// ...
// ...
Button("Save Time") {
saveTime()
let new_threshold = DateComponents(hour: savedTime?.hours,
minute: savedTime?.minutes,
second: savedTime?.seconds)
model.changeThreshold(threshold: new_threshold)
}
}
.onChange(of: model.thresholdToDiscourage) {
BeeFreeSchedule.setSchedule()
// ...
I believe I am using the latest stable version of Xcode (recently deleted and reinstalled). I'm really new to mobile development to Swift so any help is greatly appreciated.
Thank you! :)
At 11:37 in this video - https://developer.apple.com/videos/play/wwdc2021/10123/ - Nolan instantiates MyModel() in swiftui view file. And then at 12:02 he just uses MyModel from extension.
I have the exact same code and when I try to build my project, it fails with error that MyModel() could not be found.
I shared my MyModel.swift file between extension target and main app. Now it builds. However, it seems there are two separate instances of MyModel.
What is proper way for DeviceActivityMonitor extension to pass data to main app? I simply want to increment counter from extension every minute and let the main app to know that.
Or even better, - is there a way to use SwiftData from Device Activity Monitor extension?
Sometimes the app switcher doesn't show the correct shield configuration if there is like 7 or more apps in the app switcher.
if I clear all apps from app switcher cache and leave 4 apps, the correct shield configuration will show every time instantly when I change the shield configuration from my parent app and go back to the app switcher; but if there is like 7 or more apps in the app switcher, I can see some of the app's shield configurations don't update instantly and I have to scroll past the app in the app switcher and go back to for it to update to the correct shield if it does at all.
This is misleading to users and in the case of my app detrimental to one of its core functionalities
override func configuration(shielding application: Application) -> ShieldConfiguration {
let sharedUserDefaults = UserDefaults(suiteName: SharedUserDefaults.suiteName.rawValue)!
let isPaidOn = sharedUserDefaults.bool(forKey: SharedUserDefaults.paidSwitchKey.rawValue)
if isPaidOn == true {
return ShieldConfig.paid
} else {
return ShieldConfig.free
}
}
Starting with iOS 17.4, the DeviceActivityEvent initializer includes a new parameter named includesPastActivity.
The default value for this parameter is set to false, whereas device activity events have behaved as though this parameter were set to true up until now.
This breaking change is a MAJOR ISSUE for developers who used device activity events in their apps before iOS 17.4 because their apps might not work the way they intended after the update. They'll have to release new app versions that specifically set includesPastActivityto true.
In my opinion, the default value for includesPastActivity should be true to avoid disrupting events scheduled on older versions of iOS.
I have filed an enhancement report (FB13573556) about this. I really hope this is changed before the official iOS 17.4 release.
class MyModel: ObservableObject {
@Published var selection = FamilyActivitySelection()
init() {
$selection.sink {
newSelection in
print(newSelection)
}
}
}
class MyView: View {
@StateObject var model = MyModel()
// some body
// ....
// my method
func removeToken(token: ApplicationToken) {
model.selection.applicationTokens.remove(token)
}
}
I am using the above code. When I call removeToken, the callback from the sink (which is registered in init() of MyModel) is called without any changes. newSelection still contains the token that I removed.
Currently, I am using the additional code below to work around the problem.
.onChange(of: model.selection.applicationTokens) {
newSet in
model.selection.applicationTokens = newSet
}
Should I use the workaround solution, or am I missing something?
Our app uses Family Control and have 2 extensions for monitoring and shielding.
We got Family controls Distribution entitlement for main app bundle and we have applied to get for extensions too, but its like 2 months we didn't get the Distribution entitlements for extensions. We need to upload the app to TestFlight, but without Distribution entitlements for extensions we can't do it.
Bundle id exp:
com.example.example -- Distribution entitlement provided
com.example.example.MonitorExtension -- only development entitlement
com.example.example.ShieldConfiguratoionExtension -- only development entitlement
Is there nay workaround?
App with DeviceActivityReport extension either won't install on device or won't upload to TestFlight
We have integrated DeviceActivityReport to our Family Controll app. All capabilities are added. We built the app and tested it, but when we want to upload to TestFlight the following error happened (see attached image)
When we are adding NSExtensionMainStoryboard or NSExtensionPrincipalClass the following error appears during installation process.
DeviceReport.appex with id com.example.example.DeviceReport defines either an NSExtensionMainStoryboard or NSExtensionPrincipalClass key, which is not allowed for the extension point com.apple.deviceactivityui.report-extension
Experiencing a very strange issue with 3rd party permissions not working on screen time as of yesterday on my device (iPhone 12, iOS 17.2.1) and we are concerned that other users may end up dealing with this if it's not resolved.
The video attached demonstrates what I mean but this is happening across every single screen time application, not just Opal or ours. I tried resetting my device, hard reboots, deleting and redownloading the apps, updating my phone, and turning screen time off and back on but nothing has worked. I also submitted my feedback (FB13540567)
It has been two weeks since I applied for the permission to open the family control api, but Apple hasn't replied to me yet, and I can't find my application number. May I know how long it will take for me to receive the notification? Also, how to check the application progress? Is there a website?
We are requesting authorization from AuthorizationCenter as a child, and that works as expected. But revoking is always approved, I assumed the guardian would have to identify on the device to approve the revoking.
Is this intended behavior or a bug?
Hi there, just bumping on whether this problem is being looked at. My feedback says there are "no similar reports," but it has been happening since the inception of Screen Time API.
This feedback has two separate sysdiagnoses: FB11888225
To clarify the problem: sometimes when calling ManagedSettingsStore.clearAllSettings(), MY apps shield is cleared but it still shows the Apple's default shield. So the end result is the app remains blocked. I would guess that Apple has code that synchronizes all the settings and as it tried to process my apps settings, it "cleared my apps settings," but failed to clear the "global" settings due to interprocess communication issues. If I am any correct, I'm sure it's tricky to solve, but maybe there can be some patches/hacks to add SOME extra reliability: ex. add timer/cron to double-check the integrity of ManagedSettingsStore.
This is a daily problem for my users, and it's a scary one. Sometimes users delete the app, and the block screens remain. One of the only solutions is to restart their device. Imagine scenarios where users have an emergency, but they aren't able to resolve it.
Here is one App Store 1-star review just to give a picture: "This app wrecked my phone. When 'pausing' a focus session, apps are not available. After a focus session ends, apps are not available. After deleting the app, apps are not available. Thank goodness I removed the Phone app from filtering in case I needed to call 911. This app is extremely dangerous. I am still dealing with website content being blocked. Of all things I can’t access the CaringBridge website to receive updates on my sick friend. I had to get in touch with apple support to fix the damage this app has done to my phone. I have never regretted installing an app more."
As always, I'm always happy to send more data/answer questions.
Over a month ago, I submitted the Family Controls Entitlement form: https://developer.apple.com/contact/request/family-controls-distribution
For an app I'm developing and I've received 0 feedback. No case number, no email and I've called and emailed support 3 times but they haven't been able to help me.
When can I expect to hear back from the Family Controls team, what's the average wait time.
Our users report frequent crashes with the FamilyActivityPicker. Since this is a screen controlled by Apple, I'm assuming that there's nothing I can do to prevent these crashes.
I'm wondering, though, if there's any way to gracefully handle these crashes? When this happens, the following is printed to the console:
[com.apple.FamilyControls.ActivityPickerExtension(1121)] Connection to plugin invalidated while in use.
Does anyone know how to handle/catch this error?
I am able to correctly select and store the tokens of apps and categories my users will block, but I am unable to pre-populate Picker whenever the app is rebooted with previously selected and stored tokens. Below is the selection variable I am passing to the picker...
var selectionsToBlock = FamilyActivitySelection() {
willSet {
saveSelection(selection: newValue)
blockersSelected = true
}
}
Is there any way I can provide my existing blockers (shown below) so that the user can easily edit their list of restricted apps from the Picker?
func savedBlockers() -> FamilyActivitySelection? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: userDefaultsKey) else {
return nil
}
return try? decoder.decode(
FamilyActivitySelection.self,
from: data
)
}
Hello, I am able to have users properly select apps that my app can limit and block, however I am having a lot of trouble figuring out how to restrict access for set periods of times. For example, I want the user to select the apps they wish to block, store them so they can be reused later and whenever they press a button restrict access for the next 30 minutes.
I understand this is doable if they are actively on the app, but I do not know how to automate the lifting of this restriction after the given period.
Moreover, what is the best way to persist the category and application tokens selected via the FamilyActivitySelector?
A good number of my users are having trouble when trying to use Family Sharing for my app. The feature is enabled in AppStoreConnect, but apparently I'm not allowed to disable it once it has been enabled.
Is there any workaround to disable it again?
On some users device the app simply won't start after they try to enter with Family Sharing. And even if they disable family sharing on their device, the same problem occurs. So basically, once they tried to connect to our Premium product through a family members subscription, they seem forever unable to use the app again.
Any thoughts?
Thanks
Note: All the devices/accounts mentioned below are part of the same Family Group/
Is it necessary for the "parent-side" application (that uses FamilyActivityPicker to select the list of restricted apps) to be the same as the "child-side" application (that will request FamilyControls permission and will implement the DeviceActivityMonitor extension), for FamilyActivityPicker to work as expected?
My observation is that given the FamilyControls permission being granted on the child device, FamilyActivityPicker when used on the parent device and rendered on an app with a different bundle identifier works as expected (by populating all the apps installed on the child device) in case of iOS 16 and 17, but does not work for iOS 15 (I am testing on iOS 15.8).