Post not yet marked as solved
We persist ApplicationTokens in a storage container that ShieldConfigurationExtension has access to. In rare, cases all the ApplicationTokens for a user seem to change.
We know this because the Application parameter passed into configuration(shielding application: Application) -> ShieldConfiguration function has a Token that does not match (using == ) any of the ones we are persisting in storage.
Interestingly, the persisted ones still work, so I don't believe storage has gotten corrupted or anything. We can use them to add or remove shields, we can use them to display labels of the apps they represent, etc. But they don’t match what’s passed into the ShieldConfiguration extension. If the user goes into the FamilyPicker at this point and selects an app of a token that we are already persisting, the FamilyPickerSelection will have a token matching the new one that is passed into ShieldConfigurationExtension, not the one we persisted when they last selected that app.
This leads me to believe the tokens are updated/rotated in some cases. When and why does this happen, and how can we handle it gracefully?
Post not yet marked as solved
As per our code, we have the apps to be shielded whenever the threshold is reached. According to this use-case, our code in DeviceActivityExtension looks something like:
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
super.eventDidReachThreshold(event, activity: activity)
defaults?.setValue(event.rawValue, forKey: "appLimitEventName")
defaults?.setValue(true, forKey: "appLimitReached")
defaults?.synchronize()
// using darwinNotificationCenter to trigger callback in the application
let darwinNotificationCenter = DarwinNotificationsManager.sharedInstance()
darwinNotificationCenter.postNotification(withName: "nextAppLimitInitiated")
// using Notifications to debug since print doesn't work
scheduleNotification(with: "interval threshold reached")
}
And in our application, we have the shielding logic in place,
init() {
let darwinNotificationCenter = DarwinNotificationsManager.sharedInstance()
darwinNotificationCenter.register(forNotificationName: "nextAppLimitInitiated"){
print("callback received")
let appLimitReached = self.defaults?.bool(forKey: "appLimitReached")
let appLimitEventName = self.defaults?.string(forKey: "appLimitEventName")
if appLimitReached ?? false, appLimitEventName != "" {
// this sends the notification when callback is received
self.scheduleNotification(with: "init start")
self.defaults?.setValue(false, forKey: "appLimitReached")
guard var dataArray = self.defaults?.array(forKey: "appLimitdataArray"), !dataArray.isEmpty else {
return
}
let appLimitData = dataArray.first as! NSDictionary
let appLimitKey = appLimitData["appLimitId"] as! String
let data = self.getSchedule(key: appLimitEventName ?? "")
if let appTokens = data?.applicationTokens {
for token in appTokens {
if !self.applicationTokens.contains(appTokens) {
self.applicationTokens.insert(token)
}
}
}
self.store.shield.applications = self.applicationTokens
self.store.shield.applicationCategories = ShieldSettings.ActivityCategoryPolicy.specific(self.categoryTokens, except: Set())
dataArray.removeFirst()
//dataArray.append(appLimitData)
self.defaults?.set(dataArray, forKey: "appLimitdataArray")
self.initiateMonitoring(initiateAgain: true)
self.scheduleNotification(with: "init end")
}
}
}
This works as expected for multiple App Limits but only when the device is connected to the Xcode. If we disconnect the device from Xcode/ stop application from Xcode/ try in release mode, the callback is not received from extension to the app/init block.
When the device is connected to Xcode, if the apps hit the threshold, they are shielded automatically. But if the device is disconnected/ app is in release mode, the apps are not shielded automatically even after the threshold is reached. It is shielded later only after opening our app once.
Please let me know if I'm doing anything wrong in receiving callback or in my shielding logic. If I need to place the shielding logic in the extension, please tell me how I can handle multiple appTokens.
I am trying to understand how to approach 'x minute' pauses for a DeviceActivitySchedule.
For instance, I would like to let the user pause for 5 minutes from an active schedule (meaning un-shielding the apps and re-applying the shield after the 5 min has passed).
The only way that came to my mind was calling the following:
Calling .startMonitoring to start monitoring a new event with the same apps starting .now and ending .now + 5 minutes;
Calling in the intervalDidStart, store.shield.applications.subtract(apps) so that the apps are removed from the shield.
Calling in the intervalDidEnd, store.shield.applications = apps so that the apps are now shielded again.
The problem is that, from the Apple Developer Documentation:
The minimum interval length for monitoring device activity is fifteen minutes.
So the minimum pause I could offer to the user would be 15 minutes.
And that tells me this approach is most likely wrong, because all other Screen Time apps, like Opal, Jomo, AppBlock offer also 5 min pause.
Does anyone know / can think of a different and better approach?
Post not yet marked as solved
I am relatively new to swift, and working on an app blocker using flutter and swift. In my Xcode runner file, I used File > New > Target > ShieldConfigurationExtension and File > New > Target > ShieldActionExtension to create the extensions with all the necessary Info.plist values. However, when I try to build, I am presented with this error message:
Error (Xcode): Cycle inside Runner; building could produce unreliable results.
Cycle details:
→ Target 'Runner': CodeSign /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app
○ That command depends on command in Target 'Runner': script phase “[CP] Copy Pods Resources”
○ That command depends on command in Target 'Runner': script phase “Thin Binary”
○ Target 'Runner' has process command with output '/Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/Info.plist'
○ Target 'Runner' has copy command from '/Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/shieldAction.appex' to '/Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/PlugIns/shieldAction.appex'
2
Raw dependency cycle trace:
target: ->
node: <all> ->
command: <all> ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/_CodeSignature ->
command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:CodeSign /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/GoogleSignIn.bundle/ ->
directoryTreeSignature: [ ->
directoryContents: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/GoogleSignIn.bundle ->
CYCLE POINT ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/GoogleSignIn.bundle ->
command: P2:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:PhaseScriptExecution [CP] Copy Pods Resources /Users/arshgupta/Library/Developer/Xcode/DerivedData/Runner-gkxkhzabeikourbemhpfsdwlgfor/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/Script-F71E8F68D8E3D1B95F11D101.sh ->
node: /Users/arshgupta/Library/Developer/Xcode/DerivedData/Runner-gkxkhzabeikourbemhpfsdwlgfor/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/InputFileList-F71E8F68D8E3D1B95F11D101-Pods-Runner-resources-Debug-input-files-276c84640d21f41dd725929b3125799d-resolved.xcfilelist ->
command: P2:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:WriteAuxiliaryFile /Users/arshgupta/Library/Developer/Xcode/DerivedData/Runner-gkxkhzabeikourbemhpfsdwlgfor/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/InputFileList-F71E8F68D8E3D1B95F11D101-Pods-Runner-resources-Debug-input-files-276c84640d21f41dd725929b3125799d-resolved.xcfilelist ->
node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase4-thin-binary> ->
command: P0:::Gate target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase4-thin-binary ->
node: <execute-shell-script-18c1723432283e0cc55f10a6dcfd9e02f1eee2015e8ff5ebcd27678f788c2826-target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-> ->
command: P2:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:PhaseScriptExecution Thin Binary /Users/arshgupta/Library/Developer/Xcode/DerivedData/Runner-gkxkhzabeikourbemhpfsdwlgfor/Build/Intermediates.noindex/Runner.build/Debug-iphoneos/Runner.build/Script-3B06AD1E1E4923F5004D2608.sh ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/Info.plist/ ->
directoryTreeSignature: R ->
directoryContents: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/Info.plist ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/Info.plist ->
command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:ProcessInfoPlistFile /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/Info.plist /Users/arshgupta/Documents/pledge-1/ios/Runner/Info.plist ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/PlugIns/shieldAction.appex ->
command: P0:target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49-:Debug:Copy /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/PlugIns/shieldAction.appex /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/shieldAction.appex ->
node: <target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase5--cp--copy-pods-resources> ->
command: P0:::Gate target-Runner-18c1723432283e0cc55f10a6dcfd9e0288a783a885d8b0b3beb2e9f90bde3f49--fused-phase5--cp--copy-pods-resources ->
node: /Users/arshgupta/Documents/pledge-1/build/ios/Debug-iphoneos/Runner.app/GoogleSignIn.bundle
What can I do to fix this?
Post not yet marked as solved
I have two repeating DeviceActivitySchedules running in my app and noticed a super strange behavior:
whenever I modify Screen Time API permission settings (granting an additional app permission, or removing permission for a different app (completely different unrelated screen time apps from App Store)) all my DeviceActivitySchedules are cancelled and the intervalDidEnd callbacks are called
my permission is not modified in this scenario
this is a super strange and unexpected behavior
is anyone able to reproduce this?
for me this happens 100% of the times I do this
any known workarounds?
Post not yet marked as solved
Hello,
I am using a DeviceActivityEvent to limit access to an app after the user has spent x minutes on it.
Sometimes it can happen that a DeviceActivitySchedule spans from 11:55pm - 12:25am (just an example).
In these cases, I have noticed, the DeviceActivityMonitorExtension’s eventDidReachThreshold is not called after the user has reached the threshold time interval in the observed app.
I assume this is because we have transitioned over midnight and something weird happens to the DeviceActivitySchedule.
I’ve tried adding the day components to the DeviceActivitySchedule as well, but then it fails completely.
Any advice how to handle this?
I could, of course, create two separate DeviceActivitySchedules: one for before midnight, and one for after. But depending on the user’s real app usage that could lead to slightly different (and unexpected) behavior.
Post not yet marked as solved
Hi there,
In rare cases (about 0.2% of the time?), I'm seeing calls to startMonitoring on an instance of DeviceActivityCenter throw an error with localizedDescription "couldn’t communicate with a helper application."
I am certain I am passing valid parameters to the startMonitoring call because sometimes a naive retry after a few seconds succeeds. Similarly, I am certain that FamilyControls permissions have been granted by the user.
Was hoping to get more color from the systems team on what some causes of this error are and if it is avoidable by the programmer.
Post not yet marked as solved
If we want to un-shield category/application for particular schedule, then what is the mechanism for this, as for now we are using
ManagedSettingsStore().shield.applications = nil and ManagedSettingsStore().shield.applicationCategories = nil, which is actually un-shielding all category/applications without considering the selection of category/application for that schedule. What is the best approach to handle multiple schedules for shielding of apps?
Post not yet marked as solved
How can I add an X as close button on the FamilyActivityPicker Toolbar?
Desired effect see image
Post not yet marked as solved
It's no secret, the Screen Time/Family Controls API is incredibly challenging to figure out.
If you want to talk with other people building on the API, join the discord here: https://discord.gg/sjxhKWgry4
I want to connect with other people building so I made a discord where we can discuss challenges, updates & questions in more depth.
Post not yet marked as solved
I'm using the Screen Time API to shield apps the user selects. The problem is, that the apps are always shielded, even outside of the schedule. Here is my function initiateMonitoring:
func initiateMonitoring(scheduleStart: DateComponents, scheduleEnd: DateComponents) {
let schedule = DeviceActivitySchedule(
intervalStart: scheduleStart,
intervalEnd: scheduleEnd,
repeats: true
)
let center = DeviceActivityCenter()
do {
try center.startMonitoring(.daily, during: schedule)
logger.log("STARTED MONITORING")
} catch {
logger.log("FAILED MONITORING: \(error.localizedDescription)")
}
}
The expected result should be that the selected Apps should be blocked during the schedule the user specified, but not outside the schedule, but it is blocked 24/7. I've experimented with the DateComponents, but the issue may be somewhere else.
What's maybe interesting is, I tried Logging the DeviceActivityMonitor and somehow it doesn't get called. The logging-output from initiateMonitoring is printed in the console, but for the DeviceActivityMonitor it's not. I implemented it as recommended as a separate Extension Target. For more context, here is my DeviceActivityMonitor:
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
let store = ManagedSettingsStore()
let logger = Logger()
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
logger.log("INTERVAL STARTED")
let model = ShieldManager.shared
let applications = model.selectionToDiscourage.applicationTokens
let categories = model.selectionToDiscourage.categoryTokens
let webCategories = model.selectionToDiscourage.webDomainTokens
store.shield.applications = applications.isEmpty ? nil : applications
store.shield.applicationCategories = ShieldSettings.ActivityCategoryPolicy.specific(categories, except: Set())
store.shield.webDomains = webCategories.isEmpty ? nil : webCategories
}
...
What's confusing me is that the apps are in fact shielded, so the MonitorExtension should be called, but neither the logging works nor my separate ShieldConfigurationExtension, maybe the issues are connected...
Any help would be gladly appreciated!
Post not yet marked as solved
I am currently developing an app to help people focus on their work.
I use familycontrols , managedsettings and devie activity to develop it.
I am currently facing an issue where I would like to hide the app selected by the user from the main screen and resource library.
But I did not find any relevant information in the document. Who can help me.
Post not yet marked as solved
There is a way to allow DeviceActivityMonitor to make UI changes?
Currently I have some code in the intervalDidStart and intervalDidEnd that updates Core Data entities, and it works correctly.
However, the changes are only reflected if the app is terminated and opened again.
Thus, if the app is opened and one of the methods inside the DeviceActivityMonitor is being called, the UI won't be updated.
PS: I am using @FetchRequest to retrieve the core data entities called BlockEntity, and I have a little circle in the UI which should indicate the status of the block: active/inactive, so it would be nice to update the color in real time when intervalDidStart or intervalDidEnd are called.
Post not yet marked as solved
An app that uses screen time API.
After asking for permission and receiving them works, everything generally work, shields applications etc..
Upon new version update (manually update from testFlight old version or App Store)
I get an error message, I can then update again without any error but the permissions
are revoked and I have to ask for them again. shielding stopes.
This happens randomly on TestFlight or App Store and the permissions
Did anyone encounter this or solved this?
this seems to have started a month ago but maybe I just missed it before
Post not yet marked as solved
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
)
}
Post not yet marked as solved
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?
Post not yet marked as solved
Anyone had any luck modifying any of the Activity pickers with the view modifer. We've tried preferredColorScheme(_:) with no luck.
Stuck with black icons half the time - which doesn't match our UI.
We've had even less luck modifying the activityPicker sheet view to match our UI. A gigantic 'Choose Activity' sits right in the middle of our activity selection page which already has our own specific headers.
Not to mention trying add accessibility option into these views.
Anyone had better luck modifiying the activity picker views?
Post not yet marked as solved
Hello,
I want to make a simple project that simply displays the user's current daily ScreenTime.
I believe you may be able to do the functionality I want with: FamilyControls or ScreenTime. I believe that you may need to pay for the paid developer account in order to access the FamilyControl framework.
I am having trouble finding guidance as to how I would do this as most reddit/stackoverflow posts are incomplete with answers and I cannot find demos/code examples/tutorials/sample usage anywhere on google.
I would greatly appreciate any help and guidance. If the solution requires the paid developer tier, I am fine with that. Thank you in advance
Post not yet marked as solved
So I woke up today and I saw a screen time limit was placed on my phone. No one in my family placed the screentime limit, and I never placed one when I got my phone. I dont know a screen time passcode because I never made one, because I never set up a screentime limit. I tried restarting and force restarting, didnt work, I have no new updates so that doesnt work either. I dont know what to do. Ive watched like 6 hour video essays on this phone, and its never gave me a screentime limit or warning, so I know its a bug.
Post not yet marked as solved
I have implemented a shielding mechanism via Family Controls to prevent unauthenticated users from deleting specific shortcuts within the Shortcuts app. While this successfully secures the app, it has an unintended consequence: the Shortcuts app no longer appears in Spotlight Search and becomes inaccessible for users subject to this shielding. I understand that this is the default behavior when implementing app shielding or time limits on specific apps. Is there a way to retain the app's visibility in Spotlight Search despite it being shielded?