INIntent `setImage` make runtime crash in Swift 5 - ios

I've used INIntent object since Siri shortcut is added. For that I made an intent definition and it generated a INIntent object automatically.
#available(iOS 12.0, watchOS 5.0, *)
#objc(SpotConditionIntent)
public class SpotConditionIntent: INIntent {
#NSManaged public var spotId: String?
#NSManaged public var spotName: String?
}
To customize the Siri shortcut voice record screen, I added a convenience initializer. It was basically to add suggestedInvocationPhrase and the top icon image.
#available(iOS 12, *)
extension SpotConditionIntent {
convenience init(spotId: String, spotName: String) {
self.init()
self.spotId = spotId
self.spotName = spotName
self.suggestedInvocationPhrase = "\(NSLocalizedString("how_are_waves_text", comment: "How are the waves at")) \(spotName)?"
if let uiimage = UIImage(named: "check-surf-icon"), let data = uiimage.pngData() {
let inImage = INImage(imageData: data)
setImage(inImage, forParameterNamed: \.spotName)
}
}
}
Today, I tried to convert the entire project to Swift 5 and there was no issue on building. (There was no actual change in the code.) However it crashes on runtime with very weird message.
Thread 1: Fatal error: could not detangle key path type from XXXX9SpotConditionIntentCXD
and it pointed the setImage(inImage, forParameterNamed: \.spotName).
I just found that the setImage(,forParameterNamed) doesn't exist in the documentation.
https://developer.apple.com/documentation/sirikit/inintent
Seems like I need to use func keyImage() -> INImage? which is added in iOS 12.
But I have no idea why it works in Swift 4.X and can't find any documentation for the deprecation. Anyone knows about this issue?

As far as I checked...
The two methods are available in Objective-C.
- imageForParameterNamed:
- setImage:forParameterNamed:
And the generated interface for these methods shown as...
// Set an image associated with a parameter on the receiver. This image will be used in display of the receiver throughout the system.
#available(iOS 12.0, *)
open func __setImage(_ image: INImage?, forParameterNamed parameterName: String)
#available(iOS 12.0, *)
open func __image(forParameterNamed parameterName: String) -> INImage?
The docs or code suggestion of Xcode will not show you these, but you can use them in Swift 5 code:
intent.__setImage(image, forParameterNamed: "spotName")
Though, using underscore-leaded methods may be taken as using private APIs when submitting your app to App Store.
This sort of things sometimes happens when Apple is swiftifying some methods and not completed yet.
As for now, what you can do are...
Send a bug report to Apple, immediately
Write an Objective-C wrapper for these methods and import it into Swift
or
Delay submitting your up until new SDK with this issue fixed will be provided
(Can be years...)

Related

Unable to get INSendPaymentIntent work with SiriKit on watchOS (3.2 and 4)

I need to finish watchOS sirikit payment app (as a companion app for the iPhone app obviously).
The same framework, logic for intent and handler are working on iOS.
I developed the intent and handler. When I ask Siri the below sentence,
using MyApp send Albert $40
Siri replies...
Sorry I can't do that. and give a button Open MyApp
I saw some posts saying Payment Intent for Siri is not available on watchOS. But I see this in SDK.
Please give pointers.
#available(watchOS 3.2, *)
open class INSendPaymentIntent : INIntent {
public init(payee: INPerson?, currencyAmount: INCurrencyAmount?, note: String?)
#NSCopying open var payee: INPerson? { get }
#NSCopying open var currencyAmount: INCurrencyAmount? { get }
open var note: String? { get }
}
The Plists and projects have right permissions and capabilities.
Payment Intent Plist has the below keys.
Image Link for Plist Keys for Payment Intent
// WatchIntentsExtension.swift
import Intents
import PaymentsFrameworkWatchOS
class WatchIntentsExtension: INExtension {
let paymentProvider = PaymentProvider()
let contactLookup = ContactLookup()
override func handler(for intent: INIntent) -> Any? {
print("Watch Overriding")
guard intent is INSendPaymentIntent else { fatalError("Unhandled intent type \(intent)") }
return WatchSendPaymentIntentHandler(paymentProvider: paymentProvider, contactLookup: contactLookup)
}
}
I cannot post code for Handler as I have compliance issues. And... I do not do much on there.
The same code works for iPhone Siri though.

Nearby Bluetooth devices using Swift 3.0

I'm looking for a way to programmatically list any nearby Bluetooth devices (discoverable) that my device finds. I have not been able to find any information or tutorials regarding performing this call in Swift 3.0. This Q-A post discusses finding these devices using Swift 1.0 and building in Xcode 6, rather than the latest version 8.
I did my best to try to make my code into the 3.0 Syntax from the 1.0, but while running the following code, nothing is returned in the Playground:
import Cocoa
import IOBluetooth
import PlaygroundSupport
class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry, error: IOReturn, aborted: Bool) {
aborted
print("called")
let devices = sender.foundDevices()
for device : Any? in devices! {
if let thingy = device as? IOBluetoothDevice {
thingy.getAddress()
}
}
}
}
var delegate = BlueDelegate()
var inquiry = IOBluetoothDeviceInquiry(delegate: delegate)
inquiry?.start()
PlaygroundPage.current.needsIndefiniteExecution = true
Using IOBluetooth the Correct Way
The following code works flawlessly in Xcode Version 8.2.1 (8C1002), Swift 3.0. There are a few lines that aren't required, such as the entire method of deviceInquiryStarted.
Update: These usages still work as of Xcode 9.2 (9B55) and Swift 4.
Playground
import Cocoa
import IOBluetooth
import PlaygroundSupport
class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) {
print("Inquiry Started...")
//optional, but can notify you when the inquiry has started.
}
func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) {
print("\(device.addressString!)")
}
func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry!, error: IOReturn, aborted: Bool) {
//optional, but can notify you once the inquiry is completed.
}
}
var delegate = BlueDelegate()
var ibdi = IOBluetoothDeviceInquiry(delegate: delegate)
ibdi?.updateNewDeviceNames = true
ibdi?.start()
PlaygroundPage.current.needsIndefiniteExecution = true
Project-Application Usage
import Cocoa
import IOBluetooth
import ...
class BlueDelegate : IOBluetoothDeviceInquiryDelegate {
func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) {
print("Inquiry Started...")
}
func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) {
print("\(device.addressString!)")
}
}
//other classes here:
//reference the following outside of any class:
var delegate = BlueDelegate()
var ibdi = IOBluetoothDeviceInquiry(delegate: delegate)
//refer to these specifically inside of any class:
ibdi?.updateNewDeviceNames = true
ibdi?.start() //recommended under after an action-button press.
Explanation
The issue I was originally faced with was trying to access the information as the inquiry was still in process.
When I accessed it, under many different occasions my playground would hang and I would be forced to force quit both Xcode.app, and com.apple.CoreSimulator.CoreSimulatorService from the Activity Monitor. I lead myself to believe that this was just a Playground bug, only to learn that my application would crash once the inquiry finished.
As Apple's API Reference states:
Important Note: DO NOT perform remote name requests on devices from delegate methods or while this object is in use. If you wish to do your own remote name requests on devices, do them after you have stopped this object. If you do not heed this warning, you could potentially deadlock your process.
Which entirely explained my issue. Rather than directly asking for the IOBluetoothDevice information from the sender.foundDevices() method (which I believe may not have been updating..?) I simply used the parameters built into the function to mention that it was indeed an IOBluetoothDevice object, and simply to ask for that information to be printed.
Final Note
I hope that this Q/A I've created helps others in need when using IOBluetooth in Swift. The lack of any tutorials and the high amounts of outdated, Objective-C code made finding this information very challenging. I'd like to thank #RobNapier for the support on trying to find the answer to this riddle in the beginning. I'd also like to thank NotMyName for the reply on my post on the Apple Developer Forums.
I will be exploring the usage of this in an iOS device more sooner than later!

Difficulty reading UserDefaults with Fastlane Snapshot

I'm using Fastlane's snapshot to create screenshots for an app I'm about to submit to the App Store.
It works "as advertised" for the most part, but it doesn't seem to like the way I access the UserDefaults within my app. On one test, it generates an Exit status: 65 error.
UI Testing Failure - com.me.MyApp crashed in (extension in MyApp):__ObjC.NSObject.defaultTime () -> Swift.Float
I find UserDefaults.standard.value(forKey: "defaultTime") to an invitation for a syntax error, so I created an extension to access UserDefaults. Here's what the extension looks like:
class CustomExtensions: NSObject {
/*
This is blank. Nothing else in here. No really...nothing else
*/
}
extension NSObject {
// User Defaults
func defaultTime() -> Float {
return UserDefaults.standard.value(forKey: "defaultTime") as! Float
}
// a bunch of other UserDefaults
}
Wihin the app, whenever I need defaultTime, I just type defaultTime(). Using this method to access UserDefaults values works fine in the Simulator and on the devices I've tested. I only encounter a problem with snapshot.
I've tried adding in sleep(1) within the test, but that doesn't seem to do anything. I welcome suggestions re: alternative means of accessing UserDefaults that enable me to access them easily throughout MyApp.
What's probably happening is that, in your simulator and on device, you're writing a value to user defaults for the key defaultTime before it is ever read. value(forKey: returns an optional, and if you force-unwrap it (or force down-cast as your are doing here), you will crash if the value is nil. Try either returning an optional:
func defaultTime() -> Float? {
return UserDefaults.standard.value(forKey: "defaultTime") as? Float
}
or using a default value:
func defaultTime() -> Float {
return UserDefaults.standard.value(forKey: "defaultTime") as? Float ?? 0.0
}

How to make a phone call with Swift 3?

I'm building an app for a business and I've run into a dead end. I need a button that will start a phone call to a client's mobile number.
I have this, which is how it should work in Swift 2:
#IBAction func clientMobile(_ sender: AnyObject) {
UIApplication.shared.openURL(self.mobile!)
}
However openURL is deprecated and I don't see any alternative in the intellisense. What is the Swift 3 equivalent of the above line of code?
One other thing, when I run this code I have thee following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I know the error is related to self.mobile but I'm not sure how to fix it.
Declarations and extra information:
self.mobileis declared and initialised like this:
var mobile : URL?
//inside view will appear
self.mobile = URL(string: "telprompt://" + (self.dog?.client?.mobile)!)
you much check iOS version
guard let number = URL(string: "telprompt://123456789") else { return }
if #available(iOS 10.0, *) {
UIApplication.shared.open(number)
} else {
// Fallback on earlier versions
UIApplication.shared.openURL(number)
}
From the looks of it, you should be checking dog.client.mobile for some invalid characters in the phone number, or even nil (I can't tell if mobile is really optional there, if so you should check for nil before even attempting to launch the call).
Looks like your URL is coming back as nil, and you're trying to pass that to your openURL.
Your phone number should be free of symbols such as ()+-. You can easily remove those using stringByReplacingCharactersInSet (can't remember the exact name in Swift 3 right now.
openURL(:) is deprecated in iOS 10.
The new method is:
- (void)openURL:(NSURL*)url options:(NSDictionary<NSString *, id> *)options
completionHandler:(void (^ __nullable)(BOOL success))completion
Example usage to support both iOS 10 and earlier versions:
// iOS10 check
if (UIApplication.shared.respondsToSelector(#selector(UIApplication.shared.openURL(_:options:completionHandler:))) {
UIApplication.shared.openURL(self.mobile!, options: [:], completionHandler:nil)
} else {
UIApplication.shared.openURL(self.mobile!)
}
(hope this helps, sorry for any errors im on mobile atm)

Check if a function is available in Swift?

I would like to detect if the user has enabled Reduce Transparency. It's simple you just call the func UIAccessibilityIsReduceMotionEnabled() and it returns a Bool. But my app targets iOS 7 and 8 and this function isn't available on iOS 7.
In Objective-C, this is how I checked to see if that function exists:
if (UIAccessibilityIsReduceMotionEnabled != NULL) { }
In Swift, I can't figure out how to check if it exists or not. According to this answer, you can simply use optional chaining and if it's nil then it doesn't exist, but that is restricted to Obj-C protocols apparently. Xcode 6.1 doesn't like this:
let reduceMotionDetectionIsAvailable = UIAccessibilityIsReduceMotionEnabled?()
It wants you to remove the ?. And of course if you do so it will crash on iOS 7 because that function doesn't exist.
What is the proper way to check if these types of functions exist?
A proper check for availability has been added in Swift 2. This is recommended over other options mentioned here.
var shouldApplyMotionEffects = true
if #available(iOS 8.0, *) {
shouldApplyMotionEffects = !UIAccessibilityIsReduceMotionEnabled()
}
If you're okay with being a little bit cheeky, you can always open the UIKit binary using the library loader and see if it can resolve the symbol:
let uikitbundle = NSBundle(forClass: UIView.self)
let uikit = dlopen(uikitbundle.executablePath!, RTLD_LAZY)
let handle = dlsym(uikit, "UIAccessibilityIsReduceMotionEnabled")
if handle == nil {
println("Not available!")
} else {
println("Available!")
}
The dlopen and dlsym calls can be kinda expensive though so I would recommend keeping the dlopen handle open for the life of the application and storing somewhere the result of trying to dlsym. If you don't, make sure you dlclose it.
As far as I know this is AppStore safe, since UIAccessibilityIsReduceMotionEnabled is a public API.
You could check to see if you're running in iOS 8 or higher --
var reduceMotionEnabled = false
if NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 8, minorVersion: 0, patchVersion: 0)) {
reduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled()
}
I don't think there's another way to tell. So in theory, if you were able to check, trying to access the function name without the () would give you nil in iOS 7 and the () -> Bool function in iOS 8. However, in order for that to happen, UIAccessibilityIsReduceMotionEnabled would need to be defined as (() -> Bool)?, which it isn't. Testing it out yields a function instance in both versions of iOS that crashes if called in iOS 7:
let reduceMotionDetectionIsAvailable = UIAccessibilityIsReduceMotionEnabled
// reduceMotionDetectionIsAvailable is now a () -> Bool
reduceMotionDetectionIsAvailable()
// crashes in iOS7, fine in iOS8
The only way I can see to do it without testing the version is simply to define your own C function to check in your bridging header file, and call that:
// ObjC
static inline BOOL reduceMotionDetectionIsAvailable() {
return (UIAccessibilityIsReduceMotionEnabled != NULL);
}
// Swift
var reduceMotionEnabled = false
if reduceMotionDetectionIsAvailable() {
reduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled()
}
From the Apple Developer docs (Using Swift with Cocoa and Objective-C (Swift 3) > Interoperability > Adopting Cocoa Design Patterns > API Availability):
Swift code can use the availability of APIs as a condition at
run-time. Availability checks can be used in place of a condition in a
control flow statement, such as an if, guard, or while
statement.
Taking the previous example, you can check availability in an if
statement to call requestWhenInUseAuthorization() only if the method
is available at runtime:
let locationManager = CLLocationManager()
if #available(iOS 8.0, macOS 10.10, *) {
locationManager.requestWhenInUseAuthorization()
}
Alternatively, you can check availability in a guard statement,
which exits out of scope unless the current target satisfies the
specified requirements. This approach simplifies the logic of handling
different platform capabilities.
let locationManager = CLLocationManager()
guard #available(iOS 8.0, macOS 10.10, *) else { return }
locationManager.requestWhenInUseAuthorization()
Each platform argument consists of one of platform names listed below,
followed by corresponding version number. The last argument is an
asterisk (*), which is used to handle potential future platforms.
Platform Names:
iOS
iOSApplicationExtension
macOS
macOSApplicationExtension
watchOS
watchOSApplicationExtension
tvOS
tvOSApplicationExtension

Resources