What I've realized is that the FocusFilterIntent usually should be called twice (on activating Focus once and on deactivating once). However, my Extension is only called when activating. Did somebody else experience similar issues?
Post
Replies
Boosts
Views
Activity
Hey, I'm experiencing similar issues with inconsistencies when includesPastActivity is false, the only (simple) solution I came up with was a version check, since then it works for above iOS 17.4 at least
func activityEvent(_ key: String, threshold: DateComponents? = nil) -> DeviceActivityEvent {
loadSelection(for: key)
let applications = selectionToRestrict
if #available(iOS 17.4, *) {
return DeviceActivityEvent(
applications: applications.applicationTokens,
categories: applications.categoryTokens,
webDomains: applications.webDomainTokens,
threshold: threshold ?? DateComponents(minute: 0),
includesPastActivity: true
)
} else {
return DeviceActivityEvent(
applications: applications.applicationTokens,
categories: applications.categoryTokens,
webDomains: applications.webDomainTokens,
threshold: threshold ?? DateComponents(minute: 0)
)
}
}
If you don't want to set it to true, I unfortunately cannot help you. This seems to be one of many issues with DeviceActivity.
Thanks for your answer @DTS Engineer,
I've created a minimum reproducable example project.
Here is the GitHub Repo.
Keep in mind that you need to test this on a real device, since it wouldn't block the apps on the simulator and therefore won't show the ShieldConfiguration.
Thanks in advance,
Maxi
Yes, using == to compare Tokens is reliable. When looking in the docs, you can also see why that is the case.
All Tokens are defined as following:
public typealias ActivityCategoryToken = Token<ActivityCategory>
public typealias ApplicationToken = Token<Application>
public typealias WebDomainToken = Token<WebDomain>
where
public struct Token<T> : Codable, Equatable, Hashable
Therefore any Token you are comparing is Equatable. As you may know any struct that is implementing Equatable need to have the function
static func == (lhs: Self, rhs: Self) -> Bool
which is the reason why it must work.
Regarding your second question
Could there be issues with tokens stored in UserDefaults not matching due to reference or serialization differences?
It depends on the implementation. If you're storing the tokens correctly inside the UserDefaults (using an AppGroup) there shouldn't be any problem, at least I myself haven't seen any. However when you're not using an AppGroup, the DeviceActivityMonitorExtension won't have access to the tokens due to it being sandboxed.
Hope I could help you!
I have been able to solve this issue. It may vary for you, but mostly it will result to some value to be unexpectedly nil inside the ShieldConfiguration.
TLDR
You need to make sure that when overriding the ShieldConfiguration for Category (Application and Web) you don't use the WebDomainToken or ApplicationToken, but instead the CategoryToken. Any other token will result to nil (I think), which causes the ShieldConfiguration to crash and show the default screen.
If this isn't your issue, maybe some other value will result to nil and cause your Configuration to crash.
Here are the details:
I have a function to create a ShieldConfiguration, which takes a token (the underlying Application, WebDomain or CategoryToken). I have created an enum for the tokens as following:
public enum ShieldToken {
case applicationToken(ApplicationToken)
case webDomainToken(WebDomainToken)
case categoryToken(ActivityCategoryToken)
}
Now inside the ShieldConfiguration when overriding the shielding for categories the issue was:
override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
return customShieldConfiguration(
token: .applicationToken(application.token!), // this was the issue: using the applicationToken when overriding the function for the category. Instead use .categoryToken(category.token!)
displayName: application.localizedDisplayName)
}
I'm experiencing the same issue, it obviously seems to be restricted by apple. I confirmed it after activating Screen Time on the simulator in the settings. It is possible to select apps there, but not inside your own apps.
Unfortunately it doesn't seem like there is a workaround and we have to stick to testing on a real device.
I finally found a suitable solution, though it may not be the best approach. I'm storing the selection in shared UserDefaults with the Schedule ID as the key. This way, I can always retrieve the selected apps when I have the Schedule ID.
This solved the problem of not having access to the apps that should be restricted.
The other issue is actually pausing the monitoring, for which this solution is quite elegant:
When the pausing action starts, all restrictions for this activity should be cleared, and the activity should no longer be monitored.
Start a new activity with the identifier "pause+(oldID)" using the same selection and schedule times. The new schedule should contain a proper warningTime. Learn more about it here (it's not as simple as you might think): Apple Developer Documentation.
In your DeviceActivityMonitor, your intervalDidStart(for activity:) function will now be called. But since the new schedule is started with the ID "pause+(oldID)", you can react to the name of the activity.
You can now add logic like this:
if !activity.rawValue.contains("pause+") {
startMonitoring()
}
This will ensure that monitoring is not started when the ID contains "pause".
In your DeviceActivityMonitor, override the function intervalWillEndWarning(for activity:) and add logic like this:
override func intervalWillEndWarning(for activity: DeviceActivityName) {
super.intervalWillEndWarning(for: activity)
let activityName = activity.rawValue.contains("pause+") ? activity.rawValue.replacingOccurrences(of: "pause+", with: "") : activity.rawValue
// Clears the old monitoring and store
let center = DeviceActivityCenter()
let store = ManagedSettingsStore(named: .init(activity.rawValue))
// Start new schedule when the pausing reaches its end time
if let schedule = center.schedule(for: .init(rawValue: activity.rawValue)) {
center.stopMonitoring([.init(activity.rawValue)])
store.clearAllSettings()
// Start new monitoring
startMonitoring()
}
}
This way, you have successfully paused your schedule while maintaining the original begin and end times.
I found the solution, with an EntityQuery you can achieve the expected result. The con is that you actually need to have a list of Apps for it to work. Here is the code:
Step 1: Create an AppEntity
struct EntityName: AppEntity, Identifiable {
var id = UUID()
let url: String
let name: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "App"
static var defaultQuery = AppQuery()
}
Step 2: Create AppQuery
struct AppQuery: EntityQuery, EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [EntityName] {
identifiers.compactMap { id in
apps.first { $0.id == id }
}
}
func entities(matching string: String) async throws -> [EntityName] {
return apps.filter { $0.name.localizedCaseInsensitiveContains(string) }
}
func suggestedEntities() async throws -> [EntityName] {
// assuming you defined apps somewhere
return apps
}
}
Step 3: Use Entity in AppIntent
struct SomeIntent: AppIntent {
static let title: LocalizedStringResource = .init("T", defaultValue: "Some Name")
static var openAppWhenRun: Bool = false
@Parameter(title: "Select App Here!")
var selectedApp: EntityName
@MainActor
func perform() async throws -> some IntentResult & OpensIntent {
// handle
}
}
Hope, this may help someone!
I figured it out, in the func initiateMonitoring() add the following:
let setWebDomain: Set<WebDomain> = [WebDomain(domain: "https://www.instagram.com/")]
store.webContent.blockedByFilter = .specific(setWebDomain)
This will block private browsing and the specified website.
Since a lot of people are asking:
Do I need to ask permissions for every app target?
Yes, it seems like you need permissions for all your app targets. I've received approval for my app main target, but still got an error that the app couldn't be distributed (got the error 3 times, which is exactly the amount of targets my app has). After I've removed the targets, the app could be distributed.
How long does it take to get a response?
It depends, I myself didn't get an email response. You can check your access in the Main App Target < Signing & Capabilities. If there's Family Controls (Distribution) you have got access.
After you've purchased your account, you should immediately receive an E-Mail, which says "Thank you for joining the Apple Developer program" and additionally receive an email for the access to App Store Connect. Did you search in your spam folder for such emails? Maybe you should directly reach out to Apples Support if not.
I've encountered the same issue before and found a solution. If you remove the extension and integrate your TWTAppIntent code directly into the main app target without the @main TWTAppIntentExtension, you should be able to resolve it. This is the approach I previously used and it worked effectively.
However, if you prefer to continue using the AppIntent in the custom extension, I suspect the error may originate from the TWTAppIntentExtension itself. It appears to be an empty main-entry, but this is just speculation on my part.
It appears there's a solution to a problem, but I've yet to find it. The One-Sec App seems to utilize precisely what is described here. Currently, I've implemented the suggested solution, which technically works by throwing an error, but it's not ideal as it could be irritating for users to encounter it repeatedly.
I've experimented with various methods, but nothing is working as intended: on one hand, it seems impossible to eliminate the error when attempting to use two different intents without throwing an error due to the mismatch in underlying types, and on the other hand, you can't catch the error to prevent the user from seeing it.
It's clear there must be a solution to this problem, but I've been unable to discover it. Any guidance or insights would be greatly appreciated.
There should be a workaround, however I couldn't figure it out yet. The one-sec App for example is using exactly what is described here. It seems like the app launches itself through the shielding with a custom deeplink. I have tried opening my own app through the openURL property in the Environment and with NSExtensionContext.
Here's what I've tried:
Opening through Environment
@Environment(\.openURL) var openURL
override func handle(action: ShieldAction, for application: ApplicationToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
// Handle the action as needed.
switch action {
case .primaryButtonPressed:
openURL.callAsFunction(YourAppDeeplink)
// other code
}
Opening with NSExtensionContext
private func openApp(with url: URL) {
let context = NSExtensionContext()
context.open(url, completionHandler: nil)
}
However, neither approach has been successful in launching the app as intended. The one-sec App using this feature implies, that there must be a workaround or alternative method that we're missing.
I finally found a solution to your Problem. Instead of reading the AppIcon and AppName through the BundleID, you can retrieve it in a View using
Label(selection.applicationTokens.first!).labelStyle(.titleAndIcon)
You just need to get the applicationTokens and can retrieve the AppIcon and AppName through this Label.
Hope this helps!