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 that on iOS, when the app goes into the background mode, all processes, including the countdown, halt, rendering the restriction function unusable. I have attempted various mechanisms, such as:
- DeviceActivityMonitor extension => Not triggered even though scheduling is set.
- Cron => Stops when in background mode.
- Local notification (Alarm) => Only triggered when the user interacts with the notification, not automatically execute function in background
Is there a solution to address this matter?
import DeviceActivity
import FamilyControls
import Flutter
import ManagedSettings
import SwiftUI
import UIKit
import workmanager
import BackgroundTasks
import UserNotifications
var globalMethodCall = ""
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func applicationWillTerminate(_ application: UIApplication) {
@StateObject var model = MyModel.shared
print("App is terminating")
model.startAppRestrictions()
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let METHOD_CHANNEL_NAME = "flutter_screentime"
let methodChannel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: controller as! FlutterBinaryMessenger)
@StateObject var model = MyModel.shared
@StateObject var store = ManagedSettingsStore()
methodChannel.setMethodCallHandler {
(call: FlutterMethodCall, result: @escaping FlutterResult) in
Task {
do {
if #available(iOS 16.0, *) {
print("try requestAuthorization")
// request authorization
try await AuthorizationCenter.shared.requestAuthorization(for: FamilyControlsMember.individual)
print("requestAuthorization success")
switch AuthorizationCenter.shared.authorizationStatus {
case .notDetermined:
print("not determined")
case .denied:
print("denied")
case .approved:
print("approved")
@unknown default:
break
}
} else {
// Fallback on earlier versions
}
} catch {
print("Error requestAuthorization: ", error)
}
}
switch call.method {
case "selectAppsToDiscourage":
globalMethodCall = "selectAppsToDiscourage"
let vc = UIHostingController(rootView: ContentView(doneButtonAction: {
print("Finish selected")
methodChannel.invokeMethod("updateSelectedApp", arguments: nil) { (result) in
if let message = result as? String {
print("Balasan dari Flutter: \(message)")
}
}
})
.environmentObject(model)
.environmentObject(store))
controller.present(vc, animated: false, completion: nil)
// Setel nilai dynamicTitle dan dynamicCancelButtonTitle di AppDelegate
if let arguments = call.arguments as? [String: Any],
let selectAppTitle = arguments["selectAppTitle"] as? String,
let cancelTitle = arguments["cancelTitle"] as? String,
let doneTitle = arguments["doneTitle"] as? String {
model.selectAppTitle = selectAppTitle
model.cancelButtonTitle = cancelTitle
model.doneButtonTitle = doneTitle
}
print("selectAppsToDiscourage")
result(nil)
case "startLockApp":
globalMethodCall = "startLockApp"
model.startAppRestrictions()
result("startLockApp started")
case "stopLockApp":
globalMethodCall = "stopLockApp"
model.stopAppRestrictions()
print("stopLockApp")
result("stopLockApp started")
case "isLocked":
print("method channel result ", model.isAppLocked())
result(model.isAppLocked())
case "countSelectedAppCategory":
result(model.countSelectedAppCategory())
case "startSchedulingApp":
model.schedulingRestrictions()
result("startSchedulingApp started")
case "countSelectedApp":
result(model.countSelectedApp())
default:
print("no method")
result(FlutterMethodNotImplemented)
}
}
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)
// ask permission to show notification
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
if granted {
UNUserNotificationCenter.current().delegate = self
self.scheduleLocalNotification()
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func scheduleLocalNotification() {
print("start scheduling local notification")
let content = UNMutableNotificationContent()
content.title = "Time to execute"
content.body = "This function will executed at 14:20"
var dateComponents = DateComponents()
dateComponents.hour = 14
dateComponents.minute = 20
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let request = UNNotificationRequest(identifier: "execStartRestriction", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print("Failed to add notification \(error)")
} else {
print("Success register notification")
}
}
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
if response.notification.request.identifier == "execStartRestriction" {
print("execute function from notification")
@StateObject var model = MyModel.shared
model.startAppRestrictions()
}
completionHandler()
}
}
MyModel class
import Foundation
import FamilyControls
import ManagedSettings
import DeviceActivity
private let _MyModel = MyModel()
class MyModel: ObservableObject {
let store = ManagedSettingsStore()
@Published var familyActivitySelection: FamilyActivitySelection
var selectAppTitle: String = ""
var cancelButtonTitle: String = ""
var doneButtonTitle: String = ""
init() {
familyActivitySelection = FamilyActivitySelection()
}
class var shared: MyModel {
return _MyModel
}
func startAppRestrictions() {
print("setShieldRestrictions")
// Pull the selection out of the app's model and configure the application shield restriction accordingly
let applications = MyModel.shared.familyActivitySelection
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
print("deny app removal: ", store.application.denyAppRemoval ?? false)
//prevent set date time
store.dateAndTime.requireAutomaticDateAndTime = true
}
func stopAppRestrictions(){
//lock application
store.shield.applications = nil
store.shield.applicationCategories = nil
//more rules
store.media.denyExplicitContent = false
//prevent app removal
store.application.denyAppRemoval = false
print("deny app removal: ", store.application.denyAppRemoval ?? false)
//prevent set date time
store.dateAndTime.requireAutomaticDateAndTime = false
}
func isAppLocked() -> Bool {
let isShieldEmpty = (store.shield.applicationCategories == nil);
return !isShieldEmpty
}
func countSelectedAppCategory() -> Int {
let applications = MyModel.shared.familyActivitySelection
return applications.categoryTokens.count
}
func countSelectedApp() -> Int {
let applications = MyModel.shared.familyActivitySelection
return applications.applicationTokens.count
}
func schedulingRestrictions() {
print("Start monitor restriction")
// Schedule restriction 15 minutes after started
let now = Date()
let fiveMinutesLater = Calendar.current.date(byAdding: .minute, value: 15, to: now)
let schedule = DeviceActivitySchedule(intervalStart: Calendar.current.dateComponents([.hour, .minute], from: now),
intervalEnd: Calendar.current.dateComponents([.hour, .minute], from: fiveMinutesLater ?? now),
repeats: true,
warningTime: nil)
let center = DeviceActivityCenter()
do {
try center.startMonitoring(.restrictAppActivityName, during: schedule)
print("Success with Starting Monitor Activity")
}
catch {
print("Error with Starting Monitor Activity: \(error.localizedDescription)")
}
// Setting various properties of the ManagedSettingsStore instance
let applications = MyModel.shared.familyActivitySelection
if applications.applicationTokens.isEmpty {
print("empty applicationTokens")
}
if applications.categoryTokens.isEmpty {
print("empty categoryTokens")
}
print("monitoring model started")
}
}
extension DeviceActivityName {
static let restrictAppActivityName = Self("restrictApp")
}
Print log doesn't work on extension, I'm change it with local notification and it works