I do apologize if I'm not posting correctly since I'm a little new to posting here. I'm currently attempting to add a siri shortcut into my application. I've created the intent and I'm able to handle it properly and create a response with dummy data.
I am however, unable to access my service classes and other objects from the application despite adding my app to the intent handler class's target.
class IntentHandler: INExtension, TestIntentHandling {
#available(iOS 12.0, *)
func confirm(intent: TestIntent, completion: #escaping (TestIntentResponse) -> Void) {
print("HERE")
completion(TestIntentResponse.init(code: .ready, userActivity: nil))
}
#available(iOS 12.0, *)
func handle(intent: TestIntent, completion: #escaping (TestIntentResponse) -> Void) {
let response = TestIntentResponse.init(code: .success, userActivity: nil)
//Trying to reach into service here to get real values
response.workout = "Bench Press"
response.weight = 150
completion(response)
}
}
I would like to reach into my application services to populate my workout and weight fields in my handle function but I keep getting an error saying that my service classes do not exist and was hoping someone would be able to point me in the right direction. Thanks!
According to the documentation:
When a user makes a request of your app using Siri or Maps, SiriKit loads your Intents app extension and creates an instance of its INExtension subclass. The job of your extension object is to provide SiriKit with the handler objects that you use to handle specific intents. You provide these objects from the handler(for:) method of your extension object.
You need to call the handler(for:) method and return the appropriate handler class (this will be a class you create). Your intent handler class, e.g. TestIntentHandler, will subclass NSObject and conform to your TestIntentHandling protocol. TestIntentHandler is where you would handle your intent.
You need to create an app group and move any classes and methods you need to use in both the app and intent into a Framework shared between both. For things like small bits of data you can use a shared UserDefaults using UserDefaults(suiteName: "your.app.group").
From the docs:
If your app and app extension share services, consider structuring your code in the following way:
• Implement your core services in a private shared framework. A
private shared framework lets you place the code for accessing your
services in one code module and use that code from multiple targets.
Shared frameworks minimize the size of both executables and make
testing easier by ensuring that each executable uses the same code
path.
• Use a shared container to store common resources. Put relevant
images and data files into a shared container so your app and app
extension can use them. You enable shared container support in the
Capabilities tab of each target.
Related
I have an iOS app structured like this
Main Application (the main iOS app)
Intents Extension (Siri integration)
Shared Framework (shared library for interacting with Core Data. This allows both the main application and the intents extension to use the same Core Data store)
My issue is that when I insert something into Core Data using the Intents Extension, it doesn't appear in the Main Application's UITableView until I manually refresh the fetchedResultsController like this:
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "myCache")
try? fetchedResultsController.performFetch()
tableView.reloadData()
Is there a way to make the fetchedResultsController see the changes without having to manually refresh everything?
Note: If I insert something into core data from the Main Application, the fetchedResultsController automatically sees the change and updates the table (like expected)
To share a database between an app and extension you need to implement Persistent History Tracking. For an introduction see WWDC 2017 What's New in Core Data at 20:49 and for sample code see the documentation Consuming Relevant Store Changes.
The basic idea is to enable the store option NSPersistentHistoryTrackingKey, observe NSPersistentStoreRemoteChangeNotification, upon being notified you should fetch the changes using NSPersistentHistoryChangeRequest and then merge into the context using mergeChangesFromContextDidSaveNotification and transaction.objectIDNotification. Your NSFetchedResultsController will then update accordingly.
This is normal because the application extension and the main application are not working in the same process.
There are some ways to update the data in the main application
NSPersistentStoreRemoteChangeNotification
UserDefaults(suitename:)
Darwin Notifications
I'm using UserDefaults and refreshAllObjects function for the viewContext.
Example:
func sceneDidBecomeActive(_ scene: UIScene) {
let defaults = UserDefaults(suiteName:"your app group name")
let hasChange = defaults?.bool(forKey: "changes")
if hasChange ?? false {
refreshAllObjects()
defaults?.set(false, forKey: "changes")
}
}
refresh all objects function is like this:
viewContext.perform {
viewContext.stalenessInterval = 0.0
viewContext.refreshAllObjects()
viewContext.stalenessInterval = -1
}
TL;DR
On iOS 13 and Xcode 11, how can I configure an Intent to run in the background and just return the result, so it can be used as the input for other actions in the Shortcuts app?
Details of what I'm trying to achieve
My app has a list of songs that I want to expose via Shortcuts (actually, metadata about the song, not the song itself). The idea is to give advanced users a way to integrate this database of music with other apps or actions they want. For example, one may find useful to get a list of upcoming music for the next month, and use it to create Calendar events for each song. Having access to this list on the Shortcuts app can enable them to do this.
I have created an Intent called "List All Unread Music Releases" and defined its response as a list of objects that contains information about each song. The problem is, when I go to the Shortcuts app, create a new shortcut using this Intent, and run it, it opens my app instead of running in the background.
Steps I've done to create and configure Intents
Here's a high level definition of what I did to configure Intents in the project. The next section will have the actual source code and screenshots.
Created a new SiriKit Intent Definition File.
Created a new Intent.
Defined it's Title, Description, Parameters, and disabled the "Intent is eligible for Siri Suggestions" checkbox.
Defined the response property as an Array (because it's going to be a list of songs), and configured the Output to be this array property.
Created a new Intents Extension, with the checkbox "Include UI Extension" disabled. The idea here is to process the user request in the background and return a list with the results - no UI required.
In the Intents Extension target, defined the IntentsSupported array inside Info.plist with the name of the intent created in step 2.
Made the IntentHandler class implement the protocol generated for the intent created in step 2.
Code samples and screenshots
My SiriKit Intent Definition File and the GetUnreadReleases Intent:
The GetUnreadReleases Intent response:
The Intents Extension IntentHandler class:
import Intents
class IntentHandler: INExtension, GetUnreadReleasesIntentHandling {
func handle(intent: GetUnreadReleasesIntent, completion: #escaping (GetUnreadReleasesIntentResponse) -> Void) {
let response = GetUnreadReleasesIntentResponse(code: .success, userActivity: nil)
let release1 = IntentRelease(identifier: "1", display: "Teste 1")
release1.name = "Name test 1"
release1.artist = "Artist test 1"
response.releases = [release1]
completion(response)
}
func resolveMediaType(for intent: GetUnreadReleasesIntent, with completion: #escaping (IntentMediaTypeResolutionResult) -> Void) {
if intent.mediaType == .unknown {
completion(.needsValue())
} else {
completion(.success(with: intent.mediaType))
}
}
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
}
The Intents Extension Info.plist file:
Conclusion
So, I would like this intent to run in the background, assemble the list of songs based on the user defined parameters, and return this list to be used as an input to other actions in the Shortcuts app.
It looks like previous versions of the Intents editor (Xcode < 11 / iOS < 13.0) had a checkbox "Supports background execution" that did just that, but I can't find it anymore on Xcode 11.
Thanks to edford from Apple Developer Forums, I was able to make it work. In the intents definition file, the "Intent is eligible for Siri Suggestions" checkbox must be checked for the background execution to work.
I am working on a swift project, here is what I am trying to do:
I have a service class, responsible for saving data to firestore (bulk insert or single insert).
The service is used in a couple of viewControllers.
When using my dev schema I would like the app not writing anything to Firestore.
At the moment I have a env variable that act as a flag and in each function in my service I need to check if whether is set or not for saving data
func singleInsert(collection: String, data: [String: Any], id: String?) {
if !isLoggingEnabled {
// just print some stuff
return
}
// save my data in firestore
}
It works.. but it is really ugly, I was wondering if there is a better way to do it. It is worth notice that I want to disable firestore only within my service class. There are instances in the app (which don't use the service) where firestore need to be always enabled.
I end up using something close to a factory pattern.
class RealTimeEventFactory {
var realTimeEventServiceImpl: RealTimeEventProtocol
init(isLoggingEnabled: Bool, errorHandler: ErrorHandlerProtocol) {
if isLoggingEnabled {
self.realTimeEventServiceImpl = RealTimeEventService(errorHandler: errorHandler)
} else {
self.realTimeEventServiceImpl = RealTimeEventServiceMock()
}
}
}
So I have two services both conforming to same protocol. Based on the isLoggingEnabled flag the factory will generate the required instance.
It's not perfect but seems to work fine
I'm working on a standard Action Extension in my iOS app and the Xcode template contains the lines:
#IBAction func done() {
// Return any edited content to the host app.
// This template doesn't do anything, so we just echo the passed in items.
self.extensionContext!.completeRequest(returningItems: self.extensionContext!.inputItems, completionHandler: nil)
}
Here is the documentation for completeRequest.
Questions
What is the purpose of passing returningItems to completeRequest?
Do apps actually receive edited content from an action extension?
If yes, where can I find the API on the receiving end?
What are the consequences for me to pass an empty array?
I made small research for other question, that may be found helpful here.
Shortly:
There is callback in UIActivityViewController with parameter:
returnedItems - An array of NSExtensionItem objects containing any modified data. Use the items in this array to get any changes made to the original data by an extension
My iOS 8.0 + app is essentially a dictionary app, presenting a read-only data set to the user in an indexed, easily navigable format. I have explored several strategies for loading the static data, and I have decided to ship the app with several JSON data files that are serialized and loaded into a Core Data store once when the app is first opened. The call to managedObjectContext.save(), therefore, will happen only once in the lifetime of the app, on first use.
From reading Apple's Core Data Programming Guide in the Mac Developer Library (updated Sept. 2015), I understand that Apple's recommended practice is to 1) separate the Core Data stack from the AppDelegate into a dedicated DataController object (which makes it seem odd that even in Xcode 7.2 the Core Data stack is still put in the AppDelegate by default, but anyway...); and
2) open (and, I assume, seed/load) the persistent store in a background thread with a dispatch_async block, like so :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
//(get URL to persistent store here)
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
//presumably load the store from serialized JSON files here?
} catch { fatalError("Error migrating store: \(error)") }
}
I'm just getting started learning about concurrency and GCD, so my questions are basic:
1) If the data set is being loaded in a background thread, which could take some non-trivial time to complete, how does the initial view controller know when the data is finished loading so that it can fetch data from the ManagedObjectContext to display in a UITableView ?
2) Along similar lines, if I would like to test the completely loaded data set by running some fetches and printing debug text to the console, how will I know when the background process is finished and it's safe to start querying?
Thanks!
p.s. I am developing in swift, so any swift-specific tips would be tremendous.
Instead of trying to make your app import the read-only data on first launch (forcing the user to wait while the data is imported), you can import the data yourself, then add the read-only .sqlite file and data model to your app target, to be copied to the app bundle.
For the import, specify that the persistent store should use the rollback journaling option, since write-ahead logging is not recommended for read-only stores:
let importStoreOptions: [NSObject: AnyObject] = [
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
In the actual app, also specify that the bundled persistent store should use the read-only option:
let readOnlyStoreOptions: [NSObject: AnyObject] = [
NSReadOnlyPersistentStoreOption: true,
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
Since the bundled persistent store is read-only, it can be accessed directly from the app bundle, and would not even need to be copied from the bundle to a user directory.
Leaving aside whether loading a JSON at the first startup is the best option and that this question is four years old, the solution to your two questions is probably using notifications. They work from all threads and every listening class instance will be notified. Plus, you only need to add two lines:
The listener (your view controller or test class for question 2) needs to listen for notifications of a specific notification name:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleMySeedNotification(_:)), name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
where #objc func handleMySeedNotification(_ notification: Notification) is the function where you are going to implement whatever should happen when a notification is received.
The caller (your database logic) the sends the notification on successful data import. This looks like this:
NotificationCenter.default.post(name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
This is enough. I personally like to use an extension to Notification.Name in order to access the names faster and to prevent typos. This is optional, but works like this:
extension Notification.Name {
static let MyCustomName1 = Notification.Name("com.yourwebsite.MyCustomSeedNotificationName1")
static let MyCustomName2 = Notification.Name("CustomNotificationName2")
}
Using them now becomes as easy as this: NotificationCenter.default.post(name: .MyCustomSeedNotificationName1, object: nil) and even has code-completion after typing the dot!