Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DeviceActivityMonitor Extension methods not being triggered

Tags:

ios

I did a lot of researches, but cannot understand why methods inside DeviceActivityMonitor extension are not calling.

What I've seen related to this question and already checked:

Do you starting monitoring for schedules?

Yes, I do by calling center.startMonitoring() as it has been explained on WWDC2021-10123.

I was searching an issue while creating the extension. Walk through steps that are described here.

Everything looks good for me.

Did you add Family Control Capability to extension's target?

Yes, I did.

Is your device authorized with FamilyControls?

Yes. The only doubt I have here that I'm authorising FamilyControls for .individual . But as I've asked previously, it shouldn’t be a reason.

I check logs from the extension in Xcode -> Debug -> Attach to Process -> ... then in Console App trying to find logs from the extension there?

What could be wrong?

like image 747
Roman Romanenko Avatar asked Oct 29 '25 17:10

Roman Romanenko


1 Answers

I've been through this - just posting the issues I had:

  • The extension has to be an actual extension, can't just be defined in the code (may be obvious - I was implementing this as part of a Flutter app, and didn't know SwiftUI)
  • Both the extension and the app should be in an app group. This was the final thing that sorted it out for me.
  • Print statements don't work, should send notifications instead.

Here's my code:

In the ViewController.swift:

import UIKit
import FamilyControls
import SwiftUI
import DeviceActivity
import Combine

let schedule = DeviceActivitySchedule(
    intervalStart: DateComponents(hour: 0, minute: 0, second: 0),
    intervalEnd: DateComponents(hour: 23, minute: 59, second: 59),
    repeats: true,
    warningTime: DateComponents(minute: 14)
)

func description(for selection: FamilyActivitySelection) -> String {
    var result = "Include Entire Category: \(selection.includeEntireCategory ? "Yes" : "No")\n"
    // Assuming you can access the names or descriptions of applications, categories, etc.
    result += "Application Tokens: \(selection.applicationTokens.count)\n"
    result += "Category Tokens: \(selection.categoryTokens.count)\n"
    result += "Web Domain Tokens: \(selection.webDomainTokens.count)"
    return result
}


class ScreenTimeSelectAppsModel: ObservableObject {
    @Published var activitySelection = FamilyActivitySelection()

    init() { }
}

struct ScreenTimeSelectAppsContentView: View {

    @State private var pickerIsPresented = false
    @ObservedObject var model: ScreenTimeSelectAppsModel

    var body: some View {
        VStack {
            Button {
                pickerIsPresented = true
            } label: {
                Text("Select Apps")
            }
            .familyActivityPicker(
                isPresented: $pickerIsPresented,
                selection: $model.activitySelection
            )
            Text("Selected Activities: \(description(for: model.activitySelection))")
        }

    }
}

class ViewController: UIViewController {
    
    let model = ScreenTimeSelectAppsModel()
    
    private var cancellables = Set<AnyCancellable>()
        
    // Used to encode codable to UserDefaults
    private let encoder = PropertyListEncoder()

    // Used to decode codable from UserDefaults
    private let decoder = PropertyListDecoder()

    private let userDefaultsKey = "ScreenTimeSelection"
    

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let ac = AuthorizationCenter.shared
        Task {
            do {
                try await ac.requestAuthorization(for: .individual)
            }
            catch {
                print("Error getting auth for Family Controls")
            }
        }
        
//        DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
//            print("5 seconds have passed!")
//        }
        
        
        let rootView = ScreenTimeSelectAppsContentView(model: model)
        let controller = UIHostingController(rootView: rootView)
        addChild(controller)
        view.addSubview(controller.view)
        controller.view.frame = view.frame
        controller.didMove(toParent: self)
        
        // Set the initial selection
        model.activitySelection = savedSelection() ?? FamilyActivitySelection()
        
        model.$activitySelection.sink { selection in
            self.saveSelection(selection: selection)
        }
        .store(in: &cancellables)
        
        let selection: FamilyActivitySelection = savedSelection()!
        print("Selection is", selection)
        
        let event = DeviceActivityEvent(
            applications: selection.applicationTokens,
            categories: selection.categoryTokens,
            webDomains: selection.webDomainTokens,
            threshold: DateComponents(minute: 15)
        )
        
        print("Event is", event)
        print("Event applications", event.applications)
        print("Schedule is", schedule)
        
        let center = DeviceActivityCenter()
        center.stopMonitoring()

        let activity = DeviceActivityName("MyApp.ScreenTime")
        let eventName = DeviceActivityEvent.Name("MyApp.SomeEventName")
        
        print("Starting monitoring")
        
        do {
            try center.startMonitoring(
                activity,
                during: schedule,
                events: [
                    eventName: event
                ]
            )
        } catch {
            print("Error in do catch block")
        }
    }
    
    
    func saveSelection(selection: FamilyActivitySelection) {
        let defaults = UserDefaults.standard
        defaults.set(
            try? encoder.encode(selection),
            forKey: userDefaultsKey
        )
    }
    
    func savedSelection() -> FamilyActivitySelection? {
        let defaults = UserDefaults.standard
        guard let data = defaults.data(forKey: userDefaultsKey) else {
            return nil
        }
        return try? decoder.decode(
            FamilyActivitySelection.self,
            from: data
        )
    }
}

And in DeviceActivityMonitorExtension.swift:

import DeviceActivity
import UserNotifications


// Optionally override any of the functions below.
// Make sure that your class name matches the NSExtensionPrincipalClass in your Info.plist.
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    
    func scheduleNotification(with title: String) {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                let content = UNMutableNotificationContent()
                content.title = title // Using the custom title here
                content.body = "Here is the body text of the notification."
                content.sound = UNNotificationSound.default
                
                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false) // 5 seconds from now
                
                let request = UNNotificationRequest(identifier: "MyNotification", content: content, trigger: trigger)
                
                center.add(request) { error in
                    if let error = error {
                        print("Error scheduling notification: \(error)")
                    }
                }
            } else {
                print("Permission denied. \(error?.localizedDescription ?? "")")
            }
        }
    }

    override func intervalDidStart(for activity: DeviceActivityName) {
        super.intervalDidStart(for: activity)
        
        // Handle the start of the interval.
        print("Interval began")
        scheduleNotification(with: "interval did start")
    }
    
    override func intervalDidEnd(for activity: DeviceActivityName) {
        super.intervalDidEnd(for: activity)
        
        // Handle the end of the interval.
        print("Interval ended")
        scheduleNotification(with: "interval did end")
    }
    
    override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventDidReachThreshold(event, activity: activity)
        
        // Handle the event reaching its threshold.
        print("Threshold reached")
        scheduleNotification(with: "event did reach threshold warning")
    }
    
    override func intervalWillStartWarning(for activity: DeviceActivityName) {
        super.intervalWillStartWarning(for: activity)
        
        // Handle the warning before the interval starts.
        print("Interval will start")
        scheduleNotification(with: "interval will start warning")
    }
    
    override func intervalWillEndWarning(for activity: DeviceActivityName) {
        super.intervalWillEndWarning(for: activity)
        
        // Handle the warning before the interval ends.
        print("Interval will end")
        scheduleNotification(with: "interval will end warning")
    }
    
    override func eventWillReachThresholdWarning(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventWillReachThresholdWarning(event, activity: activity)
        
        // Handle the warning before the event reaches its threshold.
        print("Interval will reach threshold")
        scheduleNotification(with: "event will reach threshold warning")
    }
}

Hope this helps! Took me a while to figure out and not much on the internet.

like image 74
joshmitchell99 Avatar answered Oct 31 '25 08:10

joshmitchell99



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!