Team-scoped keys introduce the ability to restrict your token authentication keys to either development or production environments. Topic-specific keys in addition to environment isolation allow you to associate each key with a specific Bundle ID streamlining key management.
For detailed instructions on accessing these features, read our updated documentation on establishing a token-based connection to APNs.
Delve into the world of built-in app and system services available to developers. Discuss leveraging these services to enhance your app's functionality and user experience.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
As of iOS 18.3 SDK, Core Bluetooth is still mostly an Objective-C framework: key objects like CBPeripheral inherit from NSObjectProtocol and does not conform to Sendable.
CBCentralManager has a convenience initializer that allows the caller to provide a dispatch_queue for delegate callbacks. I want my Swift package that implements Core Bluetooth to conform to Swift 6 strict concurrency checking.
It is unsafe to dispatch the delegate events onto my own actor, as the passed in objects are presumably not thread-safe. What is the recommended concurrency safe way to implement Core Bluetooth in Swift 6 with strict concurrency checking enabled?
We have a user that has an active subscription according to the appstore in our product but it is registering as expired. Hitting the subscribe button in the SwiftUI SK2 dialog does nothing, meaning, nothing happens.
Any ideas?
Topic:
App & System Services
SubTopic:
StoreKit
Hey everyone,
I have an app using the screen time api, I've had quite a few reports from users saying that our monitoring features stop working until they open our app. What happens is that activities and schedules set with the device activity monitor seem to disappear. This is something we check on app re-opens and so we schedule them again and that is why the monitoring starts working again.
Of course our current solution is not optimal since our app is mainly passive. Has anyone experienced these kinds of issue ?
Hello developers! I am working with a team of North Carolina State University researchers to create an app or program that can retrieve real-time traffic events, such as hazards, road closures, and road construction alerts, in a similar manner to the Apple Maps app, but with the added functionality of collecting them over a historical period given a start and end date.
I'm looking through the MapKit documentation, but there doesn't appear to be any functions exposing granular traffic data (i.e. road hazards), just travel times and level of congestion. Could anyone point me in the right direction? Is what I'm looking to do even possible using just Apple data?
Topic:
App & System Services
SubTopic:
Maps & Location
I want to monitor again from the bellow function of DeviceActivityMonitorExtension. I have the function of startMonitoring like this.
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
super.eventDidReachThreshold(event, activity: activity)
startMonitoring()
}
public func startMonitoring() {
let startTime = DateComponents(hour: 0, minute: 0, second: 0)
let endTime = DateComponents(hour: 23, minute: 59, second: 59)//DateComponents(hour: 11, minute: 0, second: 0)//
let schedule = DeviceActivitySchedule(
intervalStart: startTime,//DateComponents(hour: 0, minute: 0, second: 0),
intervalEnd: endTime,
repeats: true
//warningTime: DateComponents(minute:1)
)
let selection: FamilyActivitySelection = savedSelection() ?? FamilyActivitySelection()
let center = DeviceActivityCenter()
let selections = self.savedSelection() ?? FamilyActivitySelection()
let applications = selections.applicationTokens
let categories = selections.categoryTokens
let webCategories = selections.webDomainTokens
let store = ManagedSettingsStore()
store.shield.applicationCategories = ShieldSettings.ActivityCategoryPolicy.specific(categories, except: Set())
store.shield.applications = applications
store.shield.webDomains = webCategories
let scheduleHard = DeviceActivitySchedule(
intervalStart: startTime,//DateComponents(hour: 0, minute: 0, second: 0),
intervalEnd: endTime,
repeats: true
//warningTime: DateComponents(minute:1)
)
let event = DeviceActivityEvent(
applications: selection.applicationTokens,
categories: selection.categoryTokens,
webDomains: selection.webDomainTokens,
threshold: DateComponents(minute: 0)//timeLimitToUseApp i.e for 15 mins
)
do {
try center.startMonitoring( .weekend,
during: scheduleHard,
events: [
.weekend: event,
]
)
print("ScreenTime Monitoring Started")
} catch let error {
print(error.localizedDescription)
}
}
Please provide us with a solution about starting monitoring from DeviceActivityMonitoringExtension's eventDidReachThreshold function or if there is any other way.
This week I’m handling a DTS incident from a developer who wants to escalate privileges in their app. This is a tricky problem. Over the years I’ve explained aspects of this both here on DevForums and in numerous DTS incidents. Rather than do that again, I figured I’d collect my thoughts into one place and share them here.
If you have questions or comments, please start a new thread with an appropriate tag (Service Management or XPC are the most likely candidates here) in the App & System Services > Core OS topic area.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
BSD Privilege Escalation on macOS
macOS has multiple privilege models. Some of these were inherited from its ancestor platforms. For example, Mach messages has a capability-based privilege model. Others were introduced by Apple to address specific user scenarios. For example, macOS 10.14 and later have mandatory access control (MAC), as discussed in On File System Permissions.
One of the most important privilege models is the one inherited from BSD. This is the classic users and groups model. Many subsystems within macOS, especially those with a BSD heritage, use this model. For example, a packet tracing tool must open a BPF device, /dev/bpf*, and that requires root privileges. Specifically, the process that calls open must have an effective user ID of 0, that is, the root user. That process is said to be running as root, and escalating BSD privileges is the act of getting code to run as root.
IMPORTANT Escalating privileges does not bypass all privilege restrictions. For example, MAC applies to all processes, including those running as root. Indeed, running as root can make things harder because TCC will not display UI when a launchd daemon trips over a MAC restriction.
Escalating privileges on macOS is not straightforward. There are many different ways to do this, each with its own pros and cons. The best approach depends on your specific circumstances.
Note If you find operations where a root privilege restriction doesn’t make sense, feel free to file a bug requesting that it be lifted. This is not without precedent. For example, in macOS 10.2 (yes, back in 2002!) we made it possible to implement ICMP (ping) without root privileges. And in macOS 10.14 we removed the restriction on binding to low-number ports (r. 17427890). Nice!
Decide on One-Shot vs Ongoing Privileges
To start, decide whether you want one-shot or ongoing privileges. For one-shot privileges, the user authorises the operation, you perform it, and that’s that. For example, if you’re creating an un-installer for your product, one-shot privileges make sense because, once it’s done, your code is no longer present on the user’s system.
In contrast, for ongoing privileges the user authorises the installation of a launchd daemon. This code always runs as root and thus can perform privileged operations at any time.
Folks often ask for one-shot privileges but really need ongoing privileges. A classic example of this is a custom installer. In many cases installation isn’t a one-shot operation. Rather, the installer includes a software update mechanism that needs ongoing privileges. If that’s the case, there’s no point dealing with one-shot privileges at all. Just get ongoing privileges and treat your initial operation as a special case within that.
Keep in mind that you can convert one-shot privileges to ongoing privileges by installing a launchd daemon.
Just Because You Can, Doesn’t Mean You Should
Ongoing privileges represent an obvious security risk. Your daemon can perform an operation, but how does it know whether it should perform that operation?
There are two common ways to authorise operations:
Authorise the user
Authorise the client
To authorise the user, use Authorization Services. For a specific example of this, look at the EvenBetterAuthorizationSample sample code.
Note This sample hasn’t been updated in a while (sorry!) and it’s ironic that one of the things it demonstrates, opening a low-number port, no longer requires root privileges. However, the core concepts demonstrated by the sample are still valid.
The packet trace example from above is a situation where authorising the user with Authorization Services makes perfect sense. By default you might want your privileged helper tool to allow any user to run a packet trace. However, your code might be running on a Mac in a managed environment, where the site admin wants to restrict this to just admin users, or just a specific group of users. A custom authorisation right gives the site admin the flexibility to configure authorisation exactly as they want.
Authorising the client is a relatively new idea. It assumes that some process is using XPC to request that the daemon perform a privileged operation. In that case, the daemon can use XPC facilities to ensure that only certain processes can make such a request.
Doing this securely is a challenge. For specific API advice, see this post.
WARNING This authorisation is based on the code signature of the process’s main executable. If the process loads plug-ins [1], the daemon can’t tell the difference between a request coming from the main executable and a request coming from a plug-in.
[1] I’m talking in-process plug-ins here. Plug-ins that run in their own process, such as those managed by ExtensionKit, aren’t a concern.
Choose an Approach
There are (at least) seven different ways to run with root privileges on macOS:
A setuid-root executable
The sudo command-line tool
The authopen command-line tool
AppleScript’s do shell script command, passing true to the administrator privileges parameter
The osascript command-line tool to run an AppleScript
The AuthorizationExecuteWithPrivileges routine, deprecated since macOS 10.7
The SMJobSubmit routine targeting the kSMDomainSystemLaunchd domain, deprecated since macOS 10.10
The SMJobBless routine, deprecated since macOS 13
An installer package (.pkg)
The SMAppService class, a much-needed enhancement to the Service Management framework introduced in macOS 13
Note There’s one additional approach: The privileged file operation feature in NSWorkspace. I’ve not listed it here because it doesn’t let you run arbitrary code with root privileges. It does, however, have one critical benefit: It’s supported in sandboxed apps. See this post for a bunch of hints and tips.
To choose between them:
Do not use a setuid-root executable. Ever. It’s that simple! Doing that is creating a security vulnerability looking for an attacker to exploit it.
If you’re working interactively on the command line, use sudo, authopen, and osascript as you see fit.
IMPORTANT These are not appropriate to use as API. Specifically, while it may be possible to invoke sudo programmatically under some circumstances, by the time you’re done you’ll have code that’s way more complicated than the alternatives.
If you’re building an ad hoc solution to distribute to a limited audience, and you need one-shot privileges, use either AuthorizationExecuteWithPrivileges or AppleScript.
While AuthorizationExecuteWithPrivileges still works, it’s been deprecated for many years. Do not use it in a widely distributed product.
The AppleScript approach works great from AppleScript, but you can also use it from a shell script, using osascript, and from native code, using NSAppleScript. See the code snippet later in this post.
If you need one-shot privileges in a widely distributed product, consider using SMJobSubmit. While this is officially deprecated, it’s used by the very popular Sparkle update framework, and thus it’s unlikely to break without warning.
If you only need escalated privileges to install your product, consider using an installer package. That’s by far the easiest solution to this problem.
Keep in mind that an installer package can install a launchd daemon and thereby gain ongoing privileges.
If you need ongoing privileges but don’t want to ship an installer package, use SMAppService. If you need to deploy to older systems, use SMJobBless.
For instructions on using SMAppService, see Updating helper executables from earlier versions of macOS.
For a comprehensive example of how to use SMJobBless, see the EvenBetterAuthorizationSample sample code. For the simplest possible example, see the SMJobBless sample code. That has a Python script to help you debug your setup. Unfortunately this hasn’t been updated in a while; see this thread for more.
Hints and Tips
I’m sure I’ll think of more of these as time goes by but, for the moment, let’s start with the big one…
Do not run GUI code as root. In some cases you can make this work but it’s not supported. Moreover, it’s not safe. The GUI frameworks are huge, and thus have a huge attack surface. If you run GUI code as root, you are opening yourself up to security vulnerabilities.
Appendix: Running an AppleScript from Native Code
Below is an example of running a shell script with elevated privileges using NSAppleScript.
WARNING This is not meant to be the final word in privilege escalation. Before using this, work through the steps above to see if it’s the right option for you.
Hint It probably isn’t!
let url: URL = … file URL for the script to execute …
let script = NSAppleScript(source: """
on open (filePath)
if class of filePath is not text then
error "Expected a single file path argument."
end if
set shellScript to "exec " & quoted form of filePath
do shell script shellScript with administrator privileges
end open
""")!
// Create the Apple event.
let event = NSAppleEventDescriptor(
eventClass: AEEventClass(kCoreEventClass),
eventID: AEEventID(kAEOpenDocuments),
targetDescriptor: nil,
returnID: AEReturnID(kAutoGenerateReturnID),
transactionID: AETransactionID(kAnyTransactionID)
)
// Set up the direct object parameter to be a single string holding the
// path to our script.
let parameters = NSAppleEventDescriptor(string: url.path)
event.setDescriptor(parameters, forKeyword: AEKeyword(keyDirectObject))
// The `as NSAppleEventDescriptor?` is required due to a bug in the
// nullability annotation on this method’s result (r. 38702068).
var error: NSDictionary? = nil
guard let result = script.executeAppleEvent(event, error: &error) as NSAppleEventDescriptor? else {
let code = (error?[NSAppleScript.errorNumber] as? Int) ?? 1
let message = (error?[NSAppleScript.errorMessage] as? String) ?? "-"
throw NSError(domain: "ShellScript", code: code, userInfo: nil)
}
let scriptResult = result.stringValue ?? ""
Revision History
2025-03-24 Added info about authopen and osascript.
2024-11-15 Added info about SMJobSubmit. Made other minor editorial changes.
2024-07-29 Added a reference to the NSWorkspace privileged file operation feature. Made other minor editorial changes.
2022-06-22 First posted.
Issue Description
I am experiencing persistent 401 Unauthorized errors when attempting to access the App Store Server API using JWT authentication. Despite following Apple's documentation and regenerating keys, I am unable to successfully authenticate.
Implementation Details
I'm implementing JWT authentication for the App Store Server API to retrieve transaction information from the following endpoint:
https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionID}
My JWT generation code (in PHP/Laravel) follows Apple's documentation:
php$kid = '6W6H649LJ4';
$header = [
"alg" => "ES256",
"kid" => $kid,
"typ" => "JWT"
];
$iss = 'b8d99de7-b43b-4cbb-aada-546ec784e249'; // App Store Connect API Key Issuer ID
$bid = 'com.gitiho.learnCourse'; // Bundle ID
$payload = [
"iss" => $iss,
"iat" => time(),
"exp" => time() + 3600,
"aud" => "appstoreconnect-v1",
"bid" => $bid
];
$pathFileAuthKeyP8 = "AuthKey_6W6H649LJ4.p8";
$contentFileAuthKey = \File::get(base_path($pathFileAuthKeyP8));
$alg = "ES256";
$jwt = \Firebase\JWT\JWT::encode($payload, $contentFileAuthKey, $alg, null, $header);
Steps Taken to Troubleshoot
Verified that the Issuer ID is correct and in UUID format
Confirmed that the Key ID matches the private key filename
Regenerated the key with proper App Store Server API permissions
Ensured the private key file is properly formatted with correct headers and footers
Verified that the JWT is being properly encoded using the ES256 algorithm
Confirmed the bundle ID is correct for our application
Checked that the API endpoint URL is correct
Additional Information
This implementation previously worked correctly
We started experiencing 401 errors recently without changing our implementation
We are using the Firebase JWT library for PHP to encode the JWT
Request
Could you please help identify what might be causing these authentication failures? Is there any recent change in the authentication requirements or endpoint URLs that might be affecting our integration?
Thanks for support me.
I'm working on implementing file moving with NSFileCoordinator. I'm using the slightly newer asynchronous API with the NSFileAccessIntents. My question is, how do I go about notifying the coordinator about the item move? Should I simply create a new instance in the asynchronous block? Or does it need to be the same coordinator instance?
let writeQueue = OperationQueue()
public func saveAndMove(data: String, to newURL: URL) {
let oldURL = presentedItemURL!
let sourceIntent = NSFileAccessIntent.writingIntent(with: oldURL, options: .forMoving)
let destinationIntent = NSFileAccessIntent.writingIntent(with: newURL, options: .forReplacing)
let coordinator = NSFileCoordinator()
coordinator.coordinate(with: [sourceIntent, destinationIntent], queue: writeQueue) { error in
if let error {
return
}
do {
// ERROR: Can't access NSFileCoordinator because it is not Sendable (Swift 6)
coordinator.item(at: oldURL, willMoveTo: newURL)
try FileManager.default.moveItem(at: oldURL, to: newURL)
coordinator.item(at: oldURL, didMoveTo: newURL)
} catch {
print("Failed to move to \(newURL)")
}
}
}
Topic:
App & System Services
SubTopic:
Processes & Concurrency
Tags:
Files and Storage
Swift
iCloud Drive
Concurrency
A customer of mine signed up for a free trial. I got a apple server notification with notification type DID_RENEW. What does that mean? Does that mean that they will be charged the subscription price now?
I have a renewing monthly subscription in my app and recently added upgrade possibilities to yearly and 6 month subscriptions. Those new subscriptions were reviewed, approved and published to App Store.
I'm showing a modal for users in the app from where they can upgrade their subscription. Upgrading was tested with real devices on Sandbox and TestFlight. There has been successful purchases through the in app modal in production app, and directly upgrading from App Store.
However, for some users there seems to happen a failed transaction with paymentCancelled error code during the upgrade. The IAP is still successful, their subscription is upgraded and they haven't voluntarily canceled the IAP. The localized description of the error is "Toimintoa ei voitu suorittaa. (Virhe 2 kohteessa SKErrorDomain.)" which translates to "The operation could not be completed. (Error 2 in SKErrorDomain.)"
These users have various iPhones (iPhone 12 Pro, iPhone 14 Pro, iPhone 15 Pro, iPhone 16 Pro) with up to date iOS versions (>= 18.3.1).
I'm receiving DID_CHANGE_RENEWAL_PREF (UPGRADE) server notification of these purchases on my server.
I haven't been able to reproduce this error myself.
Any ideas why StoreKit might fail the transaction with paymentCancelled error but still successfully upgrade the subscription?
Topic:
App & System Services
SubTopic:
StoreKit
Hey there my application allows users to have video calls with each other using Agora. I have successfully set up incoming call functionality on Android but on iOS I am struggling to get the call ui to appear when the app is not running/in background/locked.
To my knowledge this is because there is much stricter security on iOS which is limiting me from calling this. When i initially set it up it worked at first when the app was in the background but I think I was failing to report the call to call kit in time and now it's not working.
I'm not sure if I need access to this entitlement:
com.apple.developer.pushkit.unrestricted-voip
Which i believe is only for the big boys or if I make sure I'm reporting the call to call kit fast enough that I won't encounter this issue and it will consistently work in the background.
It seems like find my mac is able to fetch the device location even if the device is in locked state. Why can’t other apps do the same.
Is CallKit still not available in certain countries? like China? If it is, is there a way to get a list of countries?
Is there any way to use AccessorySetupKit in the background to discover BLE devices?
Hello,
I am trying to get the elements from my SwiftData databse in the configuration for my widget.
The SwiftData model is the following one:
@Model
class CountdownEvent {
@Attribute(.unique) var id: UUID
var title: String
var date: Date
@Attribute(.externalStorage) var image: Data
init(id: UUID, title: String, date: Date, image: Data) {
self.id = id
self.title = title
self.date = date
self.image = image
}
}
And, so far, I have tried the following thing:
AppIntent.swift
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Configuration" }
static var description: IntentDescription { "This is an example widget." }
// An example configurable parameter.
@Parameter(title: "Countdown")
var countdown: CountdownEntity?
}
Countdowns.swift, this is the file with the widget view
struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)
}
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
return Timeline(entries: entries, policy: .atEnd)
}
// func relevances() async -> WidgetRelevances<ConfigurationAppIntent> {
// // Generate a list containing the contexts this widget is relevant in.
// }
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
}
struct CountdownsEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)
Text("Title:")
Text(entry.configuration.countdown?.title ?? "Default")
}
}
}
struct Countdowns: Widget {
let kind: String = "Countdowns"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
CountdownsEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
CountdownEntity.swift, the file for the AppEntity and EntityQuery structs
struct CountdownEntity: AppEntity, Identifiable {
var id: UUID
var title: String
var date: Date
var image: Data
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(title)")
}
static var defaultQuery = CountdownQuery()
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Countdown"
init(id: UUID, title: String, date: Date, image: Data) {
self.id = id
self.title = title
self.date = date
self.image = image
}
init(id: UUID, title: String, date: Date) {
self.id = id
self.title = title
self.date = date
self.image = Data()
}
init(countdown: CountdownEvent) {
self.id = countdown.id
self.title = countdown.title
self.date = countdown.date
self.image = countdown.image
}
}
struct CountdownQuery: EntityQuery {
typealias Entity = CountdownEntity
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Countdown Event")
static var defaultQuery = CountdownQuery()
@Environment(\.modelContext) private var modelContext // Warning here: Stored property '_modelContext' of 'Sendable'-conforming struct 'CountdownQuery' has non-sendable type 'Environment<ModelContext>'; this is an error in the Swift 6 language mode
func entities(for identifiers: [UUID]) async throws -> [CountdownEntity] {
let countdownEvents = getAllEvents(modelContext: modelContext)
return countdownEvents.map { event in
return CountdownEntity(id: event.id, title: event.title, date: event.date, image: event.image)
}
}
func suggestedEntities() async throws -> [CountdownEntity] {
// Return some suggested entities or an empty array
return []
}
}
CountdownsManager.swift, this one just has the function that gets the array of countdowns
func getAllEvents(modelContext: ModelContext) -> [CountdownEvent] {
let descriptor = FetchDescriptor<CountdownEvent>()
do {
let allEvents = try modelContext.fetch(descriptor)
return allEvents
}
catch {
print("Error fetching events: \(error)")
return []
}
}
I have installed it in my phone and when I try to edit the widget, it doesn't show me any of the elements I have created in the app, just a loading dropdown for half a second:
What am I missing here?
Topic:
App & System Services
SubTopic:
Widgets & Live Activities
Tags:
SwiftUI
WidgetKit
App Intents
SwiftData
Hi, I want to offer an auto-renewable subscription (e.g., $1/month) that grants users (10 document analyses per month), with the count resetting at the start of each billing cycle.
-Unused analyses will not roll over to the next month-
Additionally, any analyses generated while the subscription is active will remain accessible to the user permanently, even if they cancel the subscription.
The paywall, app description, and metadata will clearly state that the subscription grants (10 document analyses per month with no rollover)
We want this to be implemented as an auto-renewable subscription model, not as a consumable service or a token/credit system (which we want to avoid).
Is this model acceptable under Apple’s guidelines, or would it be considered a token/credit system? Any insights or alternative suggestions would be appreciated.. Thanks
Hi everyone,
I'm working on an app for parents and kids where parents can define screen time goals or restrict usage of certain app categories (like social media or games). If the kid follows those rules—for example, by using their device less or avoiding restricted categories—they would earn points or rewards in the app.
I’ve been exploring if the Apple Screen Time API allows developers to access this kind of data (like total screen time, app usage by category, etc.) so that I can track the kid’s behavior and reward them accordingly.
Is it possible to programmatically access this data and implement such a reward system within my app? If so, what’s the best way to get started or which APIs should I look into?
Thanks in advance for your help!
I would like to explore developing apps to integrate with Matter, but it seems that there are limited code examples available.
To start with I would like to create a MacOs or iOS application to control a Matter device like a light bulb. I would as an example just like to know how to turn the light on or off.
Where should I start?
I'm sitting at my house and trying to sign my test device out of my apple ID so I can sign into a Sandbox user, but now I have an hour to kill because of this terribly broken "security" feature that thinks it's in an unfamiliar location, despite being at the only location it's ever known. Looks like I'll just be disabling this feature all together.
Especially as a device with Developer Mode enabled, which gets reset regularly, there should be additional options here. Come on!
In the main app, is there a way to distinguish whether the application:didFinishLaunchingWithOptions: method is triggered by the user manually clicking the app icon, or whether it is automatically triggered by the system after Live Activities receives a remote message notification?