How to delete a donated shortcut in Swift Siri - swift

I have a simple To do style list app, where an added item can have an intent donated so that user can find and mark the item as "completed" without opening the app.
In the Note class I have this function to donate the intent, which works as expected
public func donateMarkNoteAsCompleteIntent() {
let intent = MarkNoteAsCompleteIntent()
intent.content = self.content
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/YYYY"
intent.addedDate = dateFormatter.string(from: self.addedDate)
intent.id = self.id
let interaction = INInteraction(intent: intent, response: nil)
interaction.groupIdentifier = self.id
interaction.donate(completion: nil)
}
My only issue is, when the user uses the shortcut and triggers the app to update the Note item, I want to remove the shortcut so that the user can't trigger it again.
In my intent handle function, I end up calling this function
public func removeMarkNoteAsCompleteIntent() {
INInteraction.deleteAll(completion: nil)
let index = CSSearchableIndex()
index.deleteAllSearchableItems(completionHandler: nil)
}
No matter what combination of things I do here I can't seem to remove the donated shortcut. As soon as a user accepts marking the task as complete, I want the shortcut to no longer be visible in searches from Spotlight, etc. Everything else in the intent handling code is working, its updating the Note item in my database perfectly.
Help would be greatly appreciated.

Related

Swift: press button in external app via accessibility

In my function, I run the following code, when a specific event shows up and Safari is in foreground:
if win.safariIsForeground() {
let el = AXUIElementCreateApplication(win.getSafariPid())
var ptr: CFArray?
_ = AXUIElementCopyAttributeNames(el, &ptr)
}
The pointer returns an array that looks like this:
["AXFunctionRowTopLevelElements", "AXFrame", "AXChildren",
"AXFocusedUIElement", "AXFrontmost", "AXRole", "AXExtrasMenuBar",
"AXMainWindow", "AXFocusedWindow", "AXTitle",
"AXChildrenInNavigationOrder", "AXEnhancedUserInterface",
"AXRoleDescription", "AXHidden", "AXMenuBar", "AXWindows", "AXSize",
"AXPosition"]
I'd like to make Safari go one site back in the history. I think I will need AXUIElementCopyAttributeValue and AXUIElementPerformAction to do that but how do I find out the button's attribute and how do I call check AXUIElementCopyAttributeValue for that?
The easiest way to do that is by accessing the menu item. Using AXUIElementCopyAttributeValue works best with the provided constants:
// get menu bar
var menuBarPtr: CFTypeRef?
_ = AXUIElementCopyAttributeValue(safariElement, kAXMenuBarRole as CFString, &menuBarPtr)
guard let menuBarElement = menuBarPtr as! AXUIElement? else {
fatalError()
}
Accessibility Inspector shows me, what items are child of the menu bar:
so lets get the children using kAXChildrenAttribute:
// get menu bar items
var menuBarItemsPtr: CFTypeRef?
_ = AXUIElementCopyAttributeValue(menuBarElement, kAXChildrenAttribute as CFString, &menuBarItemsPtr)
guard let menuBarItemsElement = menuBarItemsPtr as AnyObject as! [AXUIElement]? else {
fatalError()
}
And so on all the way down to the menu item. Items also have an unique identifier that can look like _NS:1008. I'm not sure how to access them directly but by using AXUIElementPerformAction I can simply simulate pressing the menu item (action will be kAXPressAction).
I can use kAXRoleAttribute to identify the type of the item and where it occurs in the Accessibility hierarchy (see "Role:")
As I'm still a beginner at Swift, that was a quite challenging task as this is also not documented very well. Thanks a lot to Dexter who also helped me to understand this topic: kAXErrorAttributeUnsupported when querying Books.app
In your case you don't need necessarily need to use AXUI accessibility API. It's arguably simpler to send key strokes. You can go back in Safari history to previous page with CMD[. As an added bonus Safari does not need to be in foreground anymore.
let pid: pid_t = win.getSafariPid();
let leftBracket: UInt16 = 0x21
let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)
let keyDownEvent = CGEvent(keyboardEventSource: src, virtualKey: leftBracket, keyDown: true)
keyDownEvent?.flags = CGEventFlags.maskCommand
let keyUpEvent = CGEvent(keyboardEventSource: src, virtualKey: leftBracket, keyDown: false)
keyDownEvent?.postToPid(pid)
keyUpEvent?.postToPid(pid)
Your app running on Mojave and newer MacOS versions will need the System Preferences -> Security & Privacy -> Accessibility permisions granted to be eligible for sending keystrokes to other apps.
The keyboard codes can be looked up:
here & here visually

Better way to get the frontmost window of an Application - Swift

its not hard to get a specified application by name
NSWorkspace.shared.runningApplications.filter{$0.localizedName == "Safari"}.first
but how to get the first window of this application, and perform miniaturize with this window?
something similar with this
let app = NSWorkspace.shared.runningApplications.filter{$0.localizedName == "Safari"}.first
app.frontmostWindow.miniaturize()
You can do this using the ScriptingBridge
import ScriptingBridge
let safari = SBApplication(bundleIdentifier: "com.apple.Safari")
let windowClass = appleEvent(keyword: "cwin")
let miniaturized = appleEvent(keyword: "pmnd")
let windows = safari?.elementArray(withCode: windowClass)
let frontMostWindow = (windows?.firstObject as? SBObject)?.get() as? SBObject
frontMostWindow?.property(withCode: miniaturized).setTo(true)
func appleEvent(keyword: StaticString) -> AEKeyword {
keyword
.utf8Start
.withMemoryRebound(to: DescType.self, capacity: 1, \.pointee)
.bigEndian
}
To be able to run this code you will need a code signed app with the com.apple.security.automation.apple-events entitlement set to true (which allows posting of AppleEvents to other applications)

Having a hard time trying to filter out the hits in my Algolia InstantSearch searchController

My goal is to filter out hits by schoolID. School users on my app have unique schoolIDs and I want them to be able to search through events that only they created and not see any other school's events. I’ve been wanting to figure this out ever since I got the Algolia implemented into my app and I just can’t wrap my head around it, the concept seems so easy to explain but when i try to implement it, there is literally no change in my search results.
I copied the code that one of the Algolia team members in the community forum replied with, but it still doesn’t filter out the hits by schoolID, I will attach my block of code in the updateSearchResults() method, please point out any mistakes you see in my code, I’ve been gunning to fix this big issue in my app for a while. Thanks in advance.
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
let settings = Settings()
.set(\.searchableAttributes, to: [.default("eventName")])
.set(\.attributesForFaceting, to: [.filterOnly("schoolID")])
.set(\.attributesToRetrieve, to: ["*"])
searchResultsIndex.setSettings(settings) { (result) in
if case .success(let response) = result {
print("Response: \(response.wrapped)")
}
}
getTheSchoolsID { (schoolID) in
if let id = schoolID {
let schoolID: String = id
}
let query = Query(searchBar.searchTextField.text!).set(\.filters, to: "schoolID:\(schoolID)")
searchResultsIndex.search(query: query) { (result) in
if case .success(let response) = result {
print("Query Success!")
}
}
}
}

If I go to another view controller and return to the same one using a segue, does the snapshotListener reread all the documents?

This is a little snip it from my code.
db.collection("tasks").document(Auth.auth().currentUser?.uid ?? "").collection("currentUser").whereField("Date", isEqualTo: date).addSnapshotListener{ (querySnapshot, err) in
self.task = []
if querySnapshot!.documents.isEmpty{
self.firstView.alpha = 1
self.labelText.alpha = 1
}
else{
self.firstView.alpha = 0
self.labelText.alpha = 0
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
print("xx")
if let descS = data["Task Description"] as? String, let dateS = data["Date"] as? String, let titleS = data["Task Title"] as? String, let type1 = data["Type"] as? String{
let newTask = Tasks(title: titleS, desc: descS, date: dateS, type: type1, docId: doc.documentID)
self.task.append(newTask)
DispatchQueue.main.async {
self.taskTableView.reloadData()
let indexPath = IndexPath(row: self.task.count - 1, section: 0)
self.taskTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
The problem is that when I go to add another task I perform a segue to another view controller. From there I add a document but I need to perform another segue to go back because I am using a hamburger menu.
I did try using getDocuments(source: cache), which did reduce writes when the user did not add a task. But when they did add a task it reloads all the documents, adding tons of reads. The goal of using a snapshotListner is to reduce reads, however, I'm not sure if it will reread data when I perform a segue to the screen again. Thank-You!
The snapshot listener doesn't care at all about anything going on the UI, such as segues. The snapshot listener only fires when you first add it and when there are updates in the remote data. You can finetune the snapshot listener to omit or include cached or presumed data (using the snapshot's metadata property). But this is the nature of realtime data, it may get updated a lot as the user does things. The only workaround is coming up with the most efficient data architecture possible (on the server side) to reduce your read cost.
As an aside, your code is very dangerously written. You should first check if there is a valid userId before attaching a listener to a userId that even your code suggests could be an empty string. You should never force unwrap snapshots like you are doing because it will crash the entire app when there is even a slight network error.

Swift saving new comments using CloudKit

I'm trying to make an app which stores a user's comment on CloudKit and then shows it to the other users. User simply enters his/her comment on a text field and clicks on a submit button to submit his/her comment (just like a restaurant app). However, I can't seem to find the correct way no matter what I try. Here is my code, I'd be very glad for any help as I've been stuck on this problem for some time now. Thank you very much in advance!
#IBAction func OnSubmitTouched(_ sender: UIButton) {
if (textField.text != ""){
let newComment = CKRecord(recordType: "Users")
let publicDB = CKContainer.default().publicCloudDatabase
newComment.setValue(textField.text!, forKey: "comment")
publicDB.save(newComment){
rec ,err in
if let error = err {
print(err.debugDescription)
return
}
publicDB.fetch(withRecordID: newComment.recordID){
rec, err in
print(rec!["comment"]!)
return
}
}
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "comment", predicate: predicate)
let operation = CKQueryOperation(query: query)
var commentRecords: [CKRecord] = []
operation.recordFetchedBlock = { record in
commentRecords.append(record)
}
operation.queryCompletionBlock = { cursor, error in
print(commentRecords)
}
CKContainer.default().publicCloudDatabase.add(operation)
}
}
You are getting a permission error because Users is a protected record type that CloudKit creates automatically for users of your app. You should name it something else and then it should work.
For example, you could make a Comment record type. This might need a field that references the current user. You can get the current userID with:
CKContainer fetchUserRecordIDWithCompletionHandler:
Here is the Apple documentation for this method.
It is also possible to use the Users record type, but you would have to find the existing userID from CloudKit as above then build a record around that.
See also this answer.