SiriKit Intents Extension: Avoid / auto resolve parameter when values are provided dynamically - ios

I am building an app that uses Intents Extension to track various metrics, like weight, steps, hearth rate, etc. I want to offer to the user units of measurement to pick from, based on the metric he wants to track. For example, if the user tracks drinked water, she can choose between liter, fluid ounces, mililiters or cups and I will present her the units. If the user tracks steps, the only unit used is "steps" and it doesn't make sense to provide a disambiguation or dynamic options for it.
I noticed that when using Dynamic Options, the generated provideUnitOptions function for it will be called every time, even when in the resolve function I send a notRequired resolution result.
Here are the functions from my intent handler:
func resolveMetric(for intent: MeasurementIntent, with completion: #escaping (INStringResolutionResult) -> Void) {
let requestedMetric = intent.metric
if requestedMetric == nil {
completion(INStringResolutionResult.needsValue())
} else {
metricService.retrieveUserMetrics { (success) in
let metricNames = self.metricService.metricNamesMap.keys
if metricNames.contains(requestedMetric!){
completion(.success(with: requestedMetric!))
} else {
completion(.disambiguation(with: Array(metricNames)))
}
}
}
}
func provideMetricOptions(for intent: MeasurementIntent, with completion: #escaping ([String]?, Error?) -> Void) {
metricService.retrieveUserMetrics { (success) in
let metricNames = self.metricService.metricNamesMap.keys
completion(Array(metricNames), nil)
}
}
func resolveUnit(for intent: MeasurementIntent, with completion: #escaping (INStringResolutionResult) -> Void) {
let metric = self.metricService.metricNamesMap[intent.metric!]!
let units = metric.units.getSystemUnits(defaultUnit: metric.defaultUnit).map { $0.name }
if units.count == 1 {
completion(.notRequired())
} else if intent.unit == nil {
completion(.disambiguation(with: units))
} else {
completion(.success(with: intent.unit!))
}
}
//This is called even if in resolveUnit I sent .notRequired resolution. Same for .success
func provideUnitOptions(for intent: MeasurementIntent, with completion: #escaping ([String]?, Error?) -> Void) {
let metric = self.metricService.metricNamesMap[intent.metric!]!
let units = metric.units.getSystemUnits(defaultUnit: metric.defaultUnit).map { $0.name }
completion(units, nil)
}
The resolveUnit function is called both before and after provideUnitOptions
I can disable Dynamic Options but in this case the user won't have the possibility to use predefined units from a Shortcut. How do you think I should proceed? Am I missing something? Thank you!

Found a work around that helped me overcome this issue.
So provideOptions will be called, at least first time for resolution completions like success and notRequired but not from disambiguation or confirmationRequired. Disambiguation won't help here but if I use confirmationRequired I can automatically pass my only option. In the confirmation prompt I don't have to include the field value, anything works.
if units.count == 1 {
completion(.confirmationRequired(with: units[0]))
}
Hopefully will have an even more dynamic way for handling user parameters in a later iOS update, but this is good enough for now

Related

When does PHAsset.canPerform(.delete) return false?

I have an application in which, at some point, I want to delete some pictures/videos from the user's Photo Library. I know that I can perform such deletions as:
func delete(_ phAsset: PHAsset, completion: #escaping (Bool, Error?) -> Void) {
guard phAsset.canPerform(.delete) else {
completion(false, nil); return
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.deleteAssets([phAsset] as NSArray)
}, completionHandler: completion)
}
but I am primarily interested in the call to PHAsset.canPerform(_:).
Which are the cases in which that call returns false, passing PHAssetEditOperation.delete as an argument? I dove into the official documentation, the developer forum and here but I am unable to find any references to a possible case in which I can't delete a PHAsset from the Photos Library.
I am assuming some cases, related to iCloud sync, for example, but I don't think that's all. Can you help me? Thanks in advance!

URLSession.shared.dataTask vs dataTaskPublisher, when to use which?

I recently encounter two data fetching (download) API that performs seemingly the same thing to me. I cannot see when should I use one over the other.
I can use URLSession.shared.dataTask
var tasks: [URLSessionDataTask] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
let task = URLSession.shared.dataTask(with: tuple.imageURL, completionHandler :
{ data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() { [weak self] in
self?.displayFlag(data: data, title: tuple.name)
}
})
tasks.append(task)
task.resume()
}
deinit {
tasks.forEach {
$0.cancel()
}
}
Or I can use URLSession.shared.dataTaskPublisher
var cancellables: [AnyCancellable] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
URLSession.shared.dataTaskPublisher(for: tuple.imageURL)
.sink(
receiveCompletion: {
completion in
switch completion {
case .finished:
break
case .failure( _):
return
}},
receiveValue: { data, _ in DispatchQueue.main.async { [weak self] in self?.displayFlag(data: data, title: tuple.name) } })
.store(in: &cancellables)
}
deinit {
cancellables.forEach {
$0.cancel()
}
}
I don't see their distinct differences, as both also can fetch, and both also provide us the ability to cancel the tasks easily. Can someone shed some light on their differences in terms of when to use which?
The first one is the classic. It has been present for quite some time now and most if not all developers are familiar with it.
The second is a wrapper around the first one and allows combining it with other publishers (e.g. Perform some request only when first two requests were performed). Combination of data tasks using the first approach would be far more difficult.
So in a gist: use first one for one-shot requests. Use second one when more logic is needed to combine/pass results with/to other publishers (not only from URLSession). This is, basically, the idea behind Combine framework - you can combine different ways of async mechanisms (datatasks utilising callbacks being one of them).
More info can be found in last year's WWDC video on introducing combine.

UITableView loads cells before API call is completed

I am working on this app that helps me run some NLP on tweets & display results in a feed using a TableView.
Up to today, my app was running all the NLP on-device with a custom model built with CreateML & Apple's NaturalLanguage framework. When I would open the app, the tweets would be analyzed & show the results in the feed.
To increase the accuracy of results, I set up my own API & now make a call to that API to do some extra analysis. The issue now is that when I open the app, there is some kind of race condition. The feed does not show anything until I refresh. In the console, I see that the feed is done running fetchAndAnalyze() that gets the results while the API call in tripleCheckSentiment() is not completed.
Here is some explanation around the architecture.
NetworkingAPI (only the relevant code):
// This function makes a call to the Twitter API & returns a JSON of a user's tweets.
static func getUserTimeline(screenName: String, completion: #escaping (JSON) -> Void) {
client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in
if connectionError != nil {
print("Error: \(connectionError)")
}
do {
let json = try JSON(data: data!)
completion(json)
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
}
}
}
// This function makes a call to my API & checks the sentiment of a Tweet.
static func checkNegativeSentiment(tweet: Tweet, completion: #escaping (JSON) -> Void) {
let headers: HTTPHeaders = ["Content-Type": "application/json"]
AF.request(apiURL, method: .post, parameters: tweet, encoder: JSONParameterEncoder.default, headers: headers).response {
response in
do {
let json = try JSON(data: response.data!)
completion(json)
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
completion(JSON.init(parseJSON: "API OFFLINE."))
}
}
}
TweetManager (only the relevant code):
// This function is called from the app's feed to retrieve the most recent tweets.
func fetchTweets(completion: #escaping (Bool) -> Void) {
for friend in Common.listOfFriends {
NetworkingAPI.getUserTimeline(screenName: friend.handle, completion: {
success in
self.parseTweets() // This puts all the tweets returned in success in a list.
self.analyze() // Runs some NLP on the tweets.
completion(true)
})
}
}
func analyze() {
for tweet in listOfTweets {
// Does some on-device NLP using a model created with CreateML ...
if sentimentScore == "0" { // That is the tweet is negative.
doubleCheckSentiment(tweet: tweet)
}
}
}
func doubleCheckSentiment(tweet: Tweet) {
// Does some on-device NLP using Apple's generic framework NaturalLanguage.
if sentimentScore <= -0.8 { // Once again, the tweet is negative.
tripleCheckSentiment(tweet: tweet)
}
}
func tripleCheckSentiment(tweet: Tweet) {
NetworkingAPI.checkNegativeAzureSentiment(tweet: tweet, completion: {
json in
if json["value"]["sentiment"].int! == 2 { // We confirm the tweet is negative.
Common.negativeTweets.append(tweet)
}
}
}
FeedVC (only the relevant code):
// This function gets called when the view appears & at a bunch of different occasions.
func fetchAndAnalyze() {
var friendsAnalyzed = 0
tweetManager.fetchTweets(completion: {
success in
friendsAnalyzed += 1 // Every time completion hits, it means one friend was analyzed.
if friendsAnalyzed == Common.listOfFriends.count { // Done analyzing all friends.
self.tableView.reloadData() // Refresh & show the tweets in Common.negativeTweets in table.
}
}
I know this is long & I deeply apologize but if I could get some help on this, I would really appreciate it! By the way, excuse my use of #escaping & all that, I am fairly new to handling asynchronous API calls.
Thanks!
**EDIT, after implementing jawadAli's solution which works in some cases for some reason, I notice the following pattern: **
Imagine I add a friend to my listOfFriends. Then I refresh, which calls fetchAndAnalyze(). We see in the log REFRESH CALLED. & by the end of the function call that no negative tweets were found. Right after this happened, we get a result from our API call that one tweet was found negative.
If I refresh again, then that tweet is displayed. Any clue?
There is an issue with this function. On first transection of for loop your completion get fired ..
func fetchTweets(completion: #escaping (Bool) -> Void) {
let myGroup = DispatchGroup()
for friend in Common.listOfFriends {
myGroup.enter()
NetworkingAPI.getUserTimeline(screenName: friend.handle, completion: {
success in
self.parseTweets()
self.analyze()
myGroup.leave()
})
}
}
myGroup.notify(queue: DispatchQueue.main) {
completion(true)
})
Also reload data on Main thread
DispatchQueue.main.async {
self.tableView.reloadData()
}
NOTE: You need to handle success and failure case accordingly.. i am just giving an idea how to use dispatchGroup to sync calls ...

Firebase observer architecture

Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}

Type 'IntentHandler' does not conform to protocol 'INStartAudioCallIntentHandling'

I’m using INStartAudioCallIntentHandling in swift 2.3 and I’m getting this error :
Type ‘IntentHandler’ does not conform to protocol ‘INStartAudioCallIntentHandling’
I’m using Xcode 8.2.1. I put the func handle(startAudioCall intent: INStartAudioCallIntent, completion: (INStartAudioCallIntentResponse) -> Void) method into the class.Why i'm getting this error. Please help me.
you should probably also add
func confirm(start​Audio​Call:​ INStart​Audio​Call​Intent, completion:​ (INStart​Audio​Call​Intent​Response) -> Void)
and
func resolve​Contacts(for​Start​Audio​Call:​ INStart​Audio​Call​Intent, with:​ ([INPerson​Resolution​Result]) -> Void)
Use the methods of the INStart​Audio​Call​Intent​Handling protocol to
resolve, confirm, and handle requests to start an audio-only call with
the designated users. Adopt this protocol in an object of your Intents
extension that is capable of validating the call information.
source
Swift 3.1 solution.
We're passing in that user activity with Info that comes from the contacts display name. I'm going to need to say who I'm going to call.
I have this simple array that is a representation of a mock database. Maybe in your application have some kind of list of users that has all their contact information and you can check those contacts against the information that was passed into resolveContacts. The user says they want to call Dave, I make sure that that's in the database and if it is, then I call Dave. And in order to make the call, you need an INPerson, which requires a personHandle, which is basically a unique identifier for a human being.
You can either use an email or a phone number. I've chosen to go with a phone number right here. If it has the appropriate name, it creates this INPersonHandle, passes it in as a person with this phone number and whatever name, if it matches my existing contacts and then I say that the completion is successful with that person. If there is no matching contact, then we refer back to the user saying that we need a value.
import Intents
class IntentHandler: INExtension,INStartAudioCallIntentHandling {
override func handler(for intent: INIntent) -> Any? {
return self
}
func handle(startAudioCall intent: INStartAudioCallIntent, completion: #escaping (INStartAudioCallIntentResponse) -> Void) {
print("handle")
let ua = NSUserActivity(activityType: "Call")
let person:String = intent.contacts![0].displayName
ua.userInfo = ["person":person]
completion(INStartAudioCallIntentResponse(code: .continueInApp, userActivity: ua))
}
func confirm(startAudioCall intent: INStartAudioCallIntent, completion: #escaping (INStartAudioCallIntentResponse) -> Void) {
completion(INStartAudioCallIntentResponse(code: .ready, userActivity: nil))
}
func resolveContacts(forStartAudioCall intent: INStartAudioCallIntent, with completion: #escaping ([INPersonResolutionResult]) -> Void) {
print("resolveContacts")
let contacts:[String] = ["Dave","James","Herman"]
for contact in contacts {
if intent.contacts?[0].spokenPhrase?.uppercased() == contact.uppercased() {
let personHandle:INPersonHandle = INPersonHandle(value: "1-555-555-5555", type: .phoneNumber)
let person:INPerson = INPerson(personHandle: personHandle, nameComponents: nil, displayName: contact, image: nil, contactIdentifier: nil, customIdentifier: nil)
completion([INPersonResolutionResult.success(with: person)])
return
}
}
completion([INPersonResolutionResult.needsValue()])
}
}

Resources