I have developed a Flutter application to lock selected apps using the Screen Time API (iOS). My app has been registered in Family Control and currently successfully executes app restrictions in both emulators and devices through TestFlight. I intend to incorporate a countdown timer feature to ensure that the app restriction function is activated when the timer stops.
However, the issue I'm facing is I can't get FamilyActivitySelection values from DeviceActivityMonitorExtension, and it cause selected app always empty and I can't do restriction.
I have attempted various ways, such as:
- Using singleton on MyModel
- Using same App Groups both App & Extension Target
- Using UserDefault to save & load FamilyActivitySelection values (already tested on App and it returns correct value, but when I count it from DeviceActivityMonitorExtension and shows with local notification, the value is always empty)
Is there any solution to address this matter?
import Foundation
import FamilyControls
import ManagedSettings
import DeviceActivity
import UserNotifications
class MyModel: ObservableObject {
let store = ManagedSettingsStore(named: .mySettingStore)
static let shared = MyModel()
@Published var familyActivitySelection: FamilyActivitySelection
// Used to encode codable to UserDefaults
private let encoder = PropertyListEncoder()
// Used to decode codable from UserDefaults
private let decoder = PropertyListDecoder()
private let userDefaultsKey = "ScreenTimeSelection"
//save family activity selection to UserDefault
func saveFamilyActivitySelection(selection: FamilyActivitySelection) {
print("selected app updated: ", selection.applicationTokens.count," category: ", selection.categoryTokens.count)
let defaults = UserDefaults.standard
defaults.set(
try? encoder.encode(selection),
forKey: userDefaultsKey
)
//check is data saved to user defaults
getSavedFamilyActivitySelection()
}
//get saved family activity selection from UserDefault
func getSavedFamilyActivitySelection() -> FamilyActivitySelection? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: userDefaultsKey) else {
return nil
}
var selectedApp: FamilyActivitySelection?
let decoder = PropertyListDecoder()
selectedApp = try? decoder.decode(FamilyActivitySelection.self, from: data)
print("saved selected app updated: ", selectedApp?.categoryTokens.count ?? "0")
return selectedApp
}
init() {
familyActivitySelection = FamilyActivitySelection()
}
func saveToStore(apps : Set<Application>){
store.application.blockedApplications = apps
}
func startAppRestrictions() {
print("Start App Restriction")
// Pull the selection out of the app's model and configure the application shield restriction accordingly
// let applications = MyModel.shared.familyActivitySelection
let applications = getSavedFamilyActivitySelection()
if(applications == nil){
print("application not selected")
return
}
if applications!.applicationTokens.isEmpty {
print("empty applicationTokens")
}
if applications!.categoryTokens.isEmpty {
print("empty categoryTokens")
}
//lock application
store.shield.applications = applications!.applicationTokens.isEmpty ? nil : applications!.applicationTokens
store.shield.applicationCategories = applications!.categoryTokens.isEmpty ? nil : ShieldSettings.ActivityCategoryPolicy.specific(applications!.categoryTokens)
//more rules
store.media.denyExplicitContent = true
//prevent app removal
store.application.denyAppRemoval = true
//prevent set date time
store.dateAndTime.requireAutomaticDateAndTime = true
store.application.blockedApplications = applications!.applications
}
func stopAppRestrictions(){
print("Stop App Restriction")
store.clearAllSettings()
}
func isAppLocked() -> Bool {
let isShieldEmpty = (store.shield.applicationCategories == nil);
return !isShieldEmpty
}
// count selected category
func countSelectedAppCategory() -> Int {
// let applications = MyModel.shared.familyActivitySelection
let applications = getSavedFamilyActivitySelection()
if(applications == nil){
print("application not selected")
return 0
}
return applications!.categoryTokens.count
}
// count selected category
func countSelectedApp() -> Int {
let applications = getSavedFamilyActivitySelection()
if(applications == nil){
print("category not selected")
return 0
}
return applications!.applicationTokens.count
}
// start scheduling restriction with DeviceActivitySchedule
func schedulingRestrictions(scheduleInSecond: Double) {
print("Start monitor restriction, by", scheduleInSecond, "seconds")
//schedule device activity from now to n-second's
let startSchedulingTime = Date()
let endSchedulingTime = Calendar.current.date(byAdding: .second, value: Int(scheduleInSecond), to: startSchedulingTime)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
print("Scheduling monitor started on ",dateFormatter.string(from: startSchedulingTime))
print("Scheduling monitor will end on ",dateFormatter.string(from: endSchedulingTime ?? Date()))
let schedule = DeviceActivitySchedule(intervalStart: Calendar.current.dateComponents([.hour, .minute], from: startSchedulingTime),intervalEnd: Calendar.current.dateComponents([.hour, .minute], from: endSchedulingTime ?? startSchedulingTime), repeats: true, warningTime: nil)
let center = DeviceActivityCenter()
do {
try center.startMonitoring(.restrictAppActivityName, during: schedule)
print("Success Scheduling Monitor Activity")
}
catch {
print("Error Scheduling Monitor Activity: \(error.localizedDescription)")
}
}
}
extension DeviceActivityName {
static let restrictAppActivityName = Self("restrictApp")
}
extension ManagedSettingsStore.Name {
static let mySettingStore = Self("mySettingStore")
}
//
// DeviceActivityMonitorExtension.swift
// NewMonitor
//
// Created by Mochammad Yusuf Fachroni on 18/08/23.
//
import DeviceActivity
import UserNotifications
import ManagedSettings
// Optionally override any of the functions below.
// Make sure that your class name matches the NSExtensionPrincipalClass in your Info.plist.
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
let model = MyModel.shared
func showLocalNotification(title: String, desc: String) {
let countSelectedAppToken = model.countSelectedApp()
let countSelectedCategoryToken = model.countSelectedAppCategory()
let content = UNMutableNotificationContent()
content.title = title
content.body = "Selected app: "+String(countSelectedAppToken)+" category: "+String(countSelectedCategoryToken)
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "localNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to show notification: \(error.localizedDescription)")
}
}
let center = UNUserNotificationCenter.current()
center.add(request) { (error) in
if let error = error {
print("Failed to add notification: \(error)")
} else {
print("Success add notification")
}
}
}
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
// model.startAppRestrictions()
showLocalNotification(title: "My Start Restrict App", desc: "Restriction App started successfully")
let socialStore = ManagedSettingsStore(named: .mySettingStore)
socialStore.clearAllSettings()
model.startAppRestrictions()
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
model.stopAppRestrictions()
showLocalNotification(title: "My Restriction Stopped", desc: "Restriction App is stopped")
}
}
I see you're using
UserDefaults.standard. You set up the App Group entitlement correctly it seems, but when saving from your main app to storage, you have to save it to a UserDefaults group that the extension can access as well:Without that, you will not be able to retrieve the correct values from your extension, since they're different processes.