I created an app using Xcode's "iOS App with Watchkit App" template, went into TARGETS and checked Complications Configuration > Supported Families > Graphic Corner. I opened ComplicationController.swift in the Extension and modified getCurrentTimelineEntry():
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimelineEntry?) -> Void) {
let cornerTemplate = CLKComplicationTemplateGraphicCornerStackText()
cornerTemplate.outerTextProvider = CLKSimpleTextProvider(text: "Outer")
cornerTemplate.innerTextProvider = CLKSimpleTextProvider(text: "Inner")
let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: cornerTemplate)
handler(entry)
}
I also modified getLocalizableSampleTemplate() to provide a sample, and this is not working either:
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTemplate?) -> Void) {
let cornerTemplate = CLKComplicationTemplateGraphicCornerStackText()
cornerTemplate.outerTextProvider = CLKSimpleTextProvider(text: "Outer")
cornerTemplate.innerTextProvider = CLKSimpleTextProvider(text: "Inner")
handler(cornerTemplate)
}
When I run the app in the simulator or on my phone/watch and select the complication as one of the graphic corners, I expect to see "Outer" and "Inner". Instead it shows the name of my app for one and "---" for the other.
What am I doing wrong?
This is some of my code that is currently working:
var graphicCornerComplication: CLKComplicationTimelineEntry? {
guard #available(watchOSApplicationExtension 5.0, *) else {
return nil
}
let innerTextProvider = CLKSimpleTextProvider(text: "Inner")
let outerTextProvider = CLKSimpleTextProvider(text: "Outer")
let template = CLKComplicationTemplateGraphicCornerStackText()
template.outerTextProvider = outerTextProvider
template.innerTextProvider = innerTextProvider
let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
return timelineEntry
}
A few considerations:
Have you implemented your getLocalizableSampleTemplate code? This should be the first thing you do when configuring complications. You should have something ready to show immediately when users scroll through complication slots and see yours. If you don't, that could be why you're seeing the dashes instead of your intended text.
Is your complication data source correctly assigned? Under Targets > Your WatchKit Extension > Complications Configuration > Data Source Class, make sure ComplicationController is assigned.
Your entry could be coming up nil if you're working on an older version of WatchOS.
EDIT - To clarify, graphicCornerComplication is just a property that I have added to some of my models so that I can quickly get a timeline entry by just calling graphicCornerComplication on them. In use, it looks something like this:
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimelineEntry?) -> Void) {
switch complication.family {
case .graphicCorner:
let graphicCornerComplication = dataModel.graphicCornerComplication
handler(graphicCornerComplication)
default:
handler(nil)
}
}
Related
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
I'm integrating Sirikit, Bill Payment using the intent:
INPayBillIntentHandling (which was released recently in iOS 10.3+, 27 Mar 2017).
Apple Documentation is here.
Note: I'm using Swift Language, XCode 8.3, Device iPhone 6 with iOS 10.3.3 & Demo Project iOS Deployment target is iOS 10.3 AND also enabled the Siri when asked the permission for the first time and also verified that In Settings, Siri is enabled.
When I launch the app on device and say "Bill Payment using DemoApp", Siri says "You can get this information on DemoApp, Do you wish to go to DemoApp?"
Please help me. Thanks in Advance!
So far I did the following steps:
1) Create a Demo Xcode project
2) In Main App Capabilities, Enabled Siri.
3) Added Sirikit extension using
File -> New -> Add Target -> Intent Extension -> Next ->Add ProductName and say Finish
Note: I've disabled the Sirikit UI Extension.
4) In Main AppDelegate.swift added the following:
import Intents
INPreferences.requestSiriAuthorization { (status) in
if status == INSiriAuthorizationStatus.authorized{
print("Authorized")
}else
{
print("Not authorized")
}
}
return true
5) In Main app Info.plist, added key NSSiriUsageDescription with usage description
6) In IntentExtension, Info.plist, NSExtension->IntentsSupported->added key INPayBillIntent
7) In IntentHandler.swift, added this:
override func handler(for intent: INIntent) -> Any {
if intent is INPayBillIntent{
print("Response: payBill")
return PayBillHandler()
}
8) In PayBillHandler.swift added all the delegate methods for INPayBillIntentHandling:
import Intents
class PayBillHandler: NSObject, INPayBillIntentHandling {
func handle(intent: INPayBillIntent, completion: #escaping (INPayBillIntentResponse) -> Void){
let userActivity = NSUserActivity(activityType: NSStringFromClass(INPayBillIntent.self))
let response = INPayBillIntentResponse(code: .ready, userActivity: userActivity)
completion(response)
}
func resolveBillType(for intent: INPayBillIntent, with completion: #escaping (INBillTypeResolutionResult) -> Void) {
let finalResult = INBillTypeResolutionResult.success(with: .water)
completion(finalResult)
}
func resolveBillPayee(for intent: INPayBillIntent, with completion: #escaping (INBillPayeeResolutionResult) -> Void) {
let speakableStr = INSpeakableString(identifier: "XXX", spokenPhrase: "XXX", pronunciationHint: "XXX")
let speakableStr1 = INSpeakableString(identifier: "YYY", spokenPhrase: "YYY", pronunciationHint: "YYY")
let billPayee = INBillPayee(nickname: speakableStr, number: "10112122112", organizationName: speakableStr1)
let finalResult = INBillPayeeResolutionResult.success(with: billPayee!)
completion(finalResult)
}
func resolveTransactionNote(for intent: INPayBillIntent, with completion: #escaping (INStringResolutionResult) -> Void) {
let finalResult = INStringResolutionResult.success(with: "Bill Payment")
completion(finalResult)
}
func resolveDueDate(for intent: INPayBillIntent, with completion: #escaping (INDateComponentsRangeResolutionResult) -> Void) {
completion(INDateComponentsRangeResolutionResult.notRequired())
}
func resolveFromAccount(for intent: INPayBillIntent, with completion: #escaping (INPaymentAccountResolutionResult) -> Void) {
let speakableStr2 = INSpeakableString(identifier: "Someone", spokenPhrase: "Someone", pronunciationHint: "Someone")
let speakableStr3 = INSpeakableString(identifier: "", spokenPhrase: "", pronunciationHint: "organisation")
let fromAccount = INPaymentAccount(nickname: speakableStr2, number: "10112122112", accountType: .credit, organizationName: speakableStr3)
let finalResult = INPaymentAccountResolutionResult.success(with: fromAccount!)
completion(finalResult)
}
func resolveTransactionAmount(for intent: INPayBillIntent, with completion: #escaping (INPaymentAmountResolutionResult) -> Void) {
let currencyAmmount = INCurrencyAmount(amount: NSDecimalNumber(string: "100"), currencyCode: "USD")
let transactionAmt = INPaymentAmount(amountType: .amountDue, amount: currencyAmmount)
let finalResult = INPaymentAmountResolutionResult.success(with: transactionAmt)
completion(finalResult)
}
func resolveTransactionScheduledDate(for intent: INPayBillIntent, with completion: #escaping (INDateComponentsRangeResolutionResult) -> Void) {
completion(INDateComponentsRangeResolutionResult.notRequired())
}
func confirm(intent: INPayBillIntent, completion: #escaping (INPayBillIntentResponse) -> Void) {
let response = INPayBillIntentResponse(code: .success, userActivity: nil)
completion(response)
}
}
By the way It's the same question as Joao Nunes's question, just modified it for swift and the question still waits for answer, Thanks :)
I am not sure if you have already found a solution for this. What worked for me was seeing the Deployment Target for SiriExtension to be 10.3+ along with Deployment target in main project 10.3+. Also changing the triggering phrase to "pay bill using demoapp".
The Apple Developer Forum link:
https://forums.developer.apple.com/thread/71488
Try installing iOS 11 beta 5 into your test phone.
I am trying out on iOS 11 beta 5 using swift and is a lot more stable.
I can't get the Apple Watch Complication to update/refresh in WatchOS 3. I I'm using the following code in my ComplicationController.swift file.
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTimeTravelDirections) -> Void) {
handler([.forward])
}
func getTimelineStartDate(for complication: CLKComplication, withHandler handler: #escaping (Date?) -> Void) {
handler(Date())
}
func getTimelineEndDate(for complication: CLKComplication, withHandler handler: #escaping (Date?) -> Void) {
handler(Date(timeIntervalSinceNow: 60 * 30))
}
I have also tried to schedule an update from the handle background task method in the ExtensionDelegate.swift but it dosen't seem to work either.
func scheduleNextRefresh() {
let fireDate = Date(timeIntervalSinceNow: 30 * 60)
let userInfo = ["lastActiveDate" : Date(),
"reason" : "updateWeekNumber"] as Dictionary
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo as NSSecureCoding) { (error) in
if error == nil {
print("Succesfully updated week number")
}
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task: WKRefreshBackgroundTask in backgroundTasks {
if WKExtension.shared().applicationState == .background {
if task is WKApplicationRefreshBackgroundTask {
print("Task received")
scheduleNextRefresh()
}
}
task.setTaskCompleted()
}
}
WKRefreshBackgroundTask do not update anything itself, it just allows your app to go to active state and run code (placed somewhere around print("Task received") line) that will update your complication. Remember that number of WKRefreshBackgroundTasks is limited.
Complication can be updated such way:
let server = CLKComplicationServer.sharedInstance()
// if you want to add new entries to the end of timeline
server.activeComplications?.forEach(server.extendTimeline)
// if you want to reload all the timeline, which according to snippets looks like your case
server.activeComplications?.forEach(server.reloadTimeline)
This will cause system to call getCurrentTimelineEntry(for:withHandler:) methods of your CLKComplicationDataSource, where you can prepare and return updated entries.
More about complications update in documentation. More about background tasks in WWDC16 session.
I updated Firebase Dynamic Link pod to 1.4 version. In this version I found very useful class named FIRDynamicLinkComponents. I decided to use it to generate dynamic links. But I have 2 problems:
Firebase doc says that dynamic links can survive installation process and open app on the content I described in dynamic link after installation from AppStore. It is not work.
When user without installed app taps on dynamic links, he will see strange screen with button "Open in App". After click AppStore appears.
Can we skip this screen?
My implementation:
static func createDynamicLinks(forChallangeId challangeId: String, authorId: String, authorEmail: String, completion: #escaping (_ dynamicLink: String?, _ error: Error?) -> Void) {
let link = URL(string: "https://CODE.app.goo.gl/challange/\(challangeId)/author/\(authorId)")!
let domain = DOMAIN
let components = FIRDynamicLinkComponents(link: link, domain: domain)
//add iOS params
let iOSParams = FIRDynamicLinkIOSParameters(bundleID: bundleId)
iOSParams.appStoreID = APP_STORE_ID
components.iOSParameters = iOSParams
//add Android params
let androidParams = FIRDynamicLinkAndroidParameters(packageName: PACKAGE_NAME)
androidParams.minimumVersion = 19
components.androidParameters = androidParams
//add social meta tag params
let socialParams = FIRDynamicLinkSocialMetaTagParameters()
socialParams.title = "You got new challenge"
socialParams.descriptionText = "\(authorEmail) sent the challenge to you."
socialParams.imageURL = IMAGE_URL
components.socialMetaTagParameters = socialParams
//add options
let options = FIRDynamicLinkComponentsOptions()
options.pathLength = .short
components.options = options
//make link shorter
components.shorten { (shortURL, warnings, error) in
if let error = error {
completion(nil, error)
return
}
guard let shortLinkString = shortURL?.absoluteString else {
completion(nil, error)
return
}
completion(shortLinkString, error)
}
}
Edit
3rd problem.
Target iOS10. Handle:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
guard let dynamicLinks = FIRDynamicLinks.dynamicLinks() else {
return false
}
let handled = dynamicLinks.handleUniversalLink(userActivity.webpageURL!) { (dynamiclink, error) in
if let url = dynamiclink?.url {
DynamicLinksManager.handleDeepLink(link: url)
}
}
return handled
}
handled is true but in closure dynamiclink and error are nil.
A lot of solutions depend on having more context than you've currently included. I'll edit this answer with updates as possible.
Dynamic Links definitely can survive the installation process in most situations. However, there are a lot of edge cases. Could you add specific reproduction steps for exactly the process you're using to test?
No, unfortunately this modal cannot be skipped. Apple made some changes in iOS 10.3 that make something like this unavoidable (read here for more on what happened, and how we handled the same problem in a slightly more elegant way at Branch)
This might be expected, if no valid Dynamic Link were triggered. Again, could you add specific reproduction steps?
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(startAudioCall: INStartAudioCallIntent, completion: (INStartAudioCallIntentResponse) -> Void)
and
func resolveContacts(forStartAudioCall: INStartAudioCallIntent, with: ([INPersonResolutionResult]) -> Void)
Use the methods of the INStartAudioCallIntentHandling 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()])
}
}