How to check in Swift if an iPhone supports NFC? - ios

I need to make a request from my code where one of the parameters is if the device supports NFC or not. I don't use the NFC capability in my app. I have tried
import CoreNFC
...
guard NFCNDEFReaderSession.readingAvailable
But this check fails for even supported devices. Is checking this way unsupported if there is no intention to use it in the app and is adding keys in entitlements and info plist the only way to go? Thanks in advance.

Try this:
import CoreNFC
in viewDidLoad check if NFC is available:
if #available(iOS 11.0, *) {
if NFCNDEFReaderSession.readingAvailable {
print("NFC is avaiable")
}
else {
print("NFC is NOT avaiable")
}
}

Related

Is Process always unresolved in iOS app or Playground?

In a iOS app, I want to use a file which have an following function using Process:
public func system(_ body: String) throws {
if #available(macOS 10.0, *) {
let process = Process()
...
} else {
fatalError()
}
}
Then, I got a fallowing error even though I applied Availability Condition and I don't evoke this function:
Use of unresolved identifier 'Process'.
I tried a similar code in Playground, and I got the same error.
I learned we cannot use Process in iOS Apps with a regular way by this question: How to execute terminal commands in Swift 4? , and I have a solution that I separate these codes with files by each using platforms. But I want to use this single file if I can.
Please give me your another solution for my ideal.
if #available() does a runtime check for OS versions.
if #available(macOS 10.0, *)
evaluates to true if the code is running on macOS 10.0 or later,
or on iOS/tvOS/watchOS with an OS which is at least the minimum deployment target.
What you want is a conditional compilation, depending on the platform:
#if os(macOS)
let process = Process()
#else
// ...
#endif
Even though you already solved this problem, just for you to know, I want to tell you that actually, Process() (or CommandLine() in Swift 3.0 or newer) is available for iOS, but you'll need to use a custom Objective-C header file which creates the object Process()/CommandLine(), or rather NSTask(), and everything it needs.
Then, in order to use this code with Swift, you'll need to create a Bridging-Header, in which you'll need to import the NSTask.h file for it to be exposed to Swift and being able to use it in your Swift code.
Once done this, use NSTask() instead of Process():
let process = NSTask() /* or NSTask.init() */
Or just use the following function in your code whenever you want to run a task:
func task(launchPath: String, arguments: String...) -> NSString {
let task = NSTask.init()
task?.setLaunchPath(launchPath)
task?.arguments = arguments
// Create a Pipe and make the task
// put all the output there
let pipe = Pipe()
task?.standardOutput = pipe
// Launch the task
task?.launch()
task?.waitUntilExit()
// Get the data
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return output!
}
As you can see, NSTask() would be the equivalent to Process() in this case.
And call it like this:
task(launchPath: "/usr/bin/echo", arguments: "Hello World")
This will also return the value, so you can even display it by doing:
print(task(launchPath: "/usr/bin/echo", arguments: "Hello, World!"))
Which will print:
~> Hello, World!
For this to work and not throwing an NSInternalInconsistencyException, you'll need to set the launchPath to the executable's full path instead to just the directory containing it.
You'll also need to set all the command arguments separated by commas.
Tested on both iPad Mini 2 (iOS 12.1 ~> Jailbroken) and iPhone Xr (iOS 12.2 ~> not jailbroken).
NOTE: Even though this works both on non-jailbroken and jailbroken devices, your App will be rejected on the AppStore, as #ClausJørgensen said:
You're using private APIs, so it'll be rejected on the App Store. Also, Xcode 11 has some new functionality that will trigger a build failure when using certain private APIs.
If your app is targeting jailbroken iOS devices and will be uploaded to a third-party store like Cydia, Zebra, Thunderbolt or Sileo, then this would work correctly.
Hope this helps you.

dismissGrantingAccessToURL: failing with "didPickDocumentURLs called with nil or 0 URLS"

I have an iOS application that is providing Document Picker feature working perfectly on iOS 10 but that on iOS 11 always calls the documentPickerWasCancelled: with this message in logs:
[UIDocumentLog] UIDocumentPickerViewController : didPickDocumentURLs
called with nil or 0 URLS
I'm correctly calling dismissGrantingAccessToURL: with a valid NSURL on the provider extension but it never calls the documentPicker:didPickDocumentsAtURLs: on the other side.
I think I'm missing something, can you give me an explanation for this bad behaviour?
I'm having the same problems. Unfortunately I think the explanation is a bug or backwards-incompatibility in iOS 11. According to the documents it should be enough with a Document Picker extension:
"The Document Picker View Controller extension can perform import and export operations on its own. If you want to support open and move operations, you must pair it with a File Provider extension."
https://developer.apple.com/documentation/uikit/uidocumentpickerextensionviewcontroller?language=objc
And indeed this worked fine in iOS 10 and earlier. iOS 11 was probably meant to be backwards compatible with the existing FileProvider-less DocumentPickers, but seems it's not. Or perhaps they forgot to update the documents.
Instead, one can implement the new updated File Provider that gives access to your files via the standard document browser UI:
https://developer.apple.com/documentation/fileprovider
This does work with an iOS 11 FileProvider backing the iOS10 picker. You probably want to create a new FileProvider using the new Xcode template, then use :
#available(iOSApplicationExtension 11.0, *)
on the FileProviderItem and FileProviderEnumerator classes, then :
if #available(iOSApplicationExtension 11.0, *) {
in the methods on your FileProviderExtension
I find that my iOS 10 picker does correctly call this method, but note the completionHandler?(nil) was required to make it work. By default, the template for iOS11 inserts a completion that reports a failure. This code works for me:
override func startProvidingItem(at url: URL, completionHandler: ((_ error: Error?) -> Void)?) {
completionHandler?(nil)
// completionHandler?(NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
}
However, that isn't the end to this iOS10/11 incompatibility. If you make an iOS10/11 compatible file provider, it won't run on some iOS10 devices as far as I can see. I can run or debug mine on a 32-bit iOS device, but the FileProvider crashes on a 64-bit iOS 10 device with this error:
dyld: Library not loaded: /System/Library/Frameworks/FileProvider.framework/FileProvider
Referenced from: /private/var/containers/Bundle/Application/61BBD1A7-EA1E-4C10-A208-CA1DFA433C8D/test.app/PlugIns/testFileProvider.appex/testFileProvider
Reason: image not found

Behaviour of new WCSessionDelegate methods on earlier versions of iOS and watchOS

I'm implementing the new WCSessionDelegate methods to support multiple device pairing.
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error;
- (void)sessionDidBecomeInactive:(WCSession *)session;
- (void)sessionDidDeactivate:(WCSession *)session;
I'm a bit unsure about how these methods will work with older versions of iOS and watchOS (the Simulator is proving very unhelpful).
My assumption is that these methods will replace the behaviour controlled through the method below, and I can exclude it?
- (void)sessionWatchStateDidChange:(WCSession *)session;
Has anyone had experience with supporting combinations of older iOS and watchOS devices with these new methods?
The new session activation methods don't replace the state change method. You will continue to receive state change notifications for the active watch since some property changes may not be related to the watch being switched.
For example, the user may install or delete the companion watch app, or enable or disable the complication on the currently active watch.
Supporting older versions of iOS:
The delegate methods themselves won't get called on older versions of the OS. You merely need to ensure you don't access any properties or call any methods which would only be available on newer versions of the OS.
You should use #if available to dynamically check the OS version that your app is running on (since activationState is only available since 9.3).
Here's an example demonstrating how you could support multiple versions of iOS:
private func isValidSession() -> Bool {
if #available(iOS 9.3, *) {
guard let session = session where session.activationState == .Activated && session.paired && session.watchAppInstalled else {
return false
}
} else {
// Fallback on earlier versions
guard let session = session where session.paired && session.watchAppInstalled else {
return false
}
}
return true
}
Sample code:
Apple has also provided QuickSwitch sample code which you may find helpful in supporting or experimenting with watch switching.

ABAddressBook to CNContact App Transition

I am working on an app that is close to launch but uses the ABAddressBook framework. With the deprecation of ABAddressBook in iOS9, do I need to check the user's iOS version, and use ABAddressBook for pre iOS9 users, and CNContact for iOS9 users?
How is everyone else handling this? I haven't been in a situation like this before.
I have also been dealing-with and researching this issue, what I've opted to do is as you suggest; check the users iOS version doing something like the following:
NSString *version = [[UIDevice currentDevice] systemVersion];
BOOL isVersion8 = [version hasPrefix:#"8."];
BOOL isVersion7 = [version hasPrefix:#"7."];
//...
...continuing based on the versions you've decided to support for your app.
Then I do a check to either use the Addressbook framework for earlier than iOS 9, and Contacts framework for iOS 9 and beyond.
if(isVersion7 || isVersion8){
//Use AddressBook
}
else{
//Use Contacts
}
That's the best way I could think to deal with this deprecation business...
Deprecated doesn't mean removed. Just make linking to both frameworks as optional and start to design data workflow that can handle both frameworks. Also please mind that CNContact is new and full of bugs.
Once you think your app is refactored and iOS evolved to 9.1 give it a green light
How to know if system supports functionality
1) Check if the class exists
if(NSClassFromString(#"CNContact")) {
// Do something
}
For weakly linked classes, it is safe to message the class, directly. Notably, this works for frameworks that aren't explicitly linked as "Required". For missing classes, the expression evaluates to nil.
2)
#ifned NSFoundationVersionNumber_iOS_9
#def NSFoundationVersionNumber_iOS_9 NUMBER
#endif
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9) {
// Use address book
} else {
// Use contact framework
}
Run the app in simulator to find the NSFoundationVersionNumber constant
if #available(iOS 9, *)
{
// iOS 9 - CNContact
}
else
{
// iOS 8 - ABAddressBook
}
This is the right way to check.

iOS WatchKit - how to determine if your code is running in watch extension or the app

With WatchKit you have your app that runs on the phone, and the watch app that runs as an extension.
If you create a library that contains common code to be used in both the phone app and the watch extension, is there a way to tell if the code is running in the phone app or the watch extension?
I.e.
if ([self isRunningInWatchExtension]) {
NSLog(#"this is running on watch");
} else {
NSLog(#"this is running on phone app");
}
- (BOOL)isRunningInWatchExtension {
???
}
In target conditionals there are some conditionals that may help you,
#if TARGET_OS_WATCH
//do something for watch
#else
//do something for ios ==> assuming you only support two platforms
#endif
I've accomplished this by checking the bundle identifier:
if ([[[NSBundle mainBundle] bundleIdentifier] isEqualToString:kAppBundleIdentifier]) {
// Running in main app
}
else if ([[[NSBundle mainBundle] bundleIdentifier] isEqualToString:kWatchBundleIdentifier]) {
// Running in extension
}
This can be easy if you are calling any custom methods in your common framework class. You just need to add additional method parameters to method. And if you are calling this method from iOS app or Watchkit app then add appropriate key-value pair to dictionary for parameters. And compare this in your framework methods.
To determine this from init or any other method then you can still get to know by this code,
NSLog(#"%#",[NSThread callStackSymbols]);
So, you need to parse this string and get appropriate target names. If it is called by iOS app then you will get 'UIKit' string and from watch kit app extension you will get 'YourApp WatchKit Extension' string somewhere. You can also refer this SO answer for parsing this string and compare it - https://stackoverflow.com/a/9603733/602997

Resources