Keeping NSUserActivity backwards compatible with Xcode 9 - ios

Using Xcode 10 (beta 6) I am able to write and run the following code with no trouble:
import Intents
func test() {
let activity = NSUserActivity(activityType: "com.activtiy.type")
activity.title = "Hello World"
activity.isEligibleForSearch = true
activity.isEligibleForHandoff = false
if #available(iOS 12.0, *) {
activity.isEligibleForPrediction = true
activity.suggestedInvocationPhrase = "Say something"
}
print(activity)
}
As of iOS 12 the .isEligibleForPredictions and .suggestedInvocationPhrase properties have been added, so Xcode 10 can keep the code itself backwards compatible using the if #available conditional.
However, I want to ensure this code is backwards compatible with earlier versions of Xcode. When run in Xcode 9, I get the following errors:
if #available(iOS 12.0, *) {
// ERROR: Value of type 'NSUserActivity' has no member 'isEligibleForPrediction'
activity.isEligibleForPrediction = true
// ERROR: Value of type 'NSUserActivity' has no member 'suggestedInvocationPhrase'
activity.suggestedInvocationPhrase = "Say something"
}
This appears to be because the #available macro is actually resolved at runtime, therefore all code contained still needs to be compiled successfully.
Is there a way for me to tell the compiler to just ignore these two lines of code when building for iOS 11, or using Xcode 9?

Xcode 10 uses Swift 4.2 while Xcode 9 uses Swift 4.1. So you can use that knowledge at compile time:
func test() {
let activity = NSUserActivity(activityType: "com.activtiy.type")
activity.title = "Hello World"
activity.isEligibleForSearch = true
activity.isEligibleForHandoff = false
#if swift(>=4.2) // compile-time check
if #available(iOS 12.0, *) { // run-time check
activity.isEligibleForPrediction = true
activity.suggestedInvocationPhrase = "Say something"
predictionApiAvailable = true
}
#endif
print(activity)
}
(This answer assumes that you are using Swift 4.2 on Xcode 10.)

The Availability Condition (if #available) you are using as you correctly noted is evaluated at run-time, but the compiler will use this information to provide compile-time safety (It will will warn you if you are calling an API that does not exist min deployment target).
For conditionally compiling code you must use Conditional Compilation Block
#if compilation condition
statements
#endif
To conditionally build your code based on the Xcode version you can include a Active Compilation Condition in the build settings (a -D swift compile flag) that its name is dynamically created based on the Xcode version. Then use this as compilation condition. Xcode already provides the build setting XCODE_VERSION_MAJOR that resolves to the current Xcode version( e.g. 0900 for Xcode 9). So you can add an an Active Compilation Condition with name XCODE_VERSION_$(XCODE_VERSION_MAJOR) that will resolve to the flag XCODE_VERSION_0900 for Xcode 9.
Then you can conditionally compile your code using:
#if XCODE_VERSION_0900
statements
#endif
I have an example project here

Related

How to execute condition based on XCODE version using conditional compilation in Xamarin

I need to execute some conditions based on the XCODE version. Is it possible to achieve using conditional compilation symbols?
for example,
this.datePicker = new UIDatePicker();
this.datePicker.Mode = UIDatePickerMode.Date;
#if (XCODE version)
//This code should be executed for XCODE 12 and above
if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
{
this.datePicker.PreferredDatePickerStyle = UIDatePickerStyle.Wheels;
}
#endif
I need these conditions should be enabled for XCODE 12 and above. For lower XCODE versions the conditions should be disabled.
How can I get this to work?

Getting Error while upgrading to new Xcode 12

My App is using CoreLocation and CLLocationManager and is working fine in iOS 13 and iOS 12.
I have implemented new feature of Precise Location in iOS 14 using Xcode 12 and its working fine in iOS 14, iOS 13, iOS 12.
But When I execute ths Xcode 12 code in Xcode 11 version (Xcode 11.7) then I am getting error
Cannot infer contextual base in reference to member 'reducedAccuracy'
Value of type 'CLLocationManager' has no member 'accuracyAuthorization'
if #available(iOS 14.0, *) {
if authorizationStatus.accessLevel == .granted && locationManager.accuracyAuthorization == .reducedAccuracy {
return .locationAlwaysAllowPreciseLocationOff
}
if authorizationStatus.accessLevel == .denied && locationManager.accuracyAuthorization == .fullAccuracy {
return .locationDeniedPreciseLocationON
}
}
// MARK: iOS 14 location function.
#available(iOS 14.0, *)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// iOS 14 Location Delegate method, not available in iOS 13 version
}
and here the error is
Static member 'authorizationStatus' cannot be used on instance of type 'CLLocationManager'
As i Know Precise Location is feature of iOS 14 and its not available in below versions and "accuracyAuthorization", ".reducedAccuracy", ".fullAccuracy" is not available in iOS 13 versions.
My Question is how can i make my code run in Xcode 11 versions. I have already added the isAvailable check to check the device version.
Thanks in advance :)
No amount of #available or #available marking is going to help you in this situation.
Why not? Well, you're doing an unexpected thing: you are opening an Xcode 12 project in Xcode 11. Your code was compiled originally in Xcode 12, where iOS 14 is a thing. So it compiled successfully. But now you open the same project in Xcode 11, where iOS 14 is not a thing. Nothing about this environment has the slightest idea that it exists. Therefore, code that involves something unique to iOS 14 will not compile. If the compiler sees that code, you are toast.
So is all hope lost? Not quite! Suppose we were to hide the code from the compiler. If we do that — if we can arrange things so that, in Xcode 11, the compiler never sees this code at all — then we will be able to compile in Xcode 11.
Well, we can do that! We can use a compilation condition. All we need is some condition that we are allowed to check against, that will distinguish what version of Xcode this is. And there is such a condition — the Swift version.
So, we can write this, for example:
class ViewController: UIViewController {
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
#if swift(>=5.3)
let status = manager.authorizationStatus
print(status.rawValue)
#endif
}
}
That code compiles in both Xcode 12 and Xcode 11, because in Xcode 11 the compilation condition fails, and the compiler never even looks inside the #if block.
In fact, we can provide an alternative version of the code, to be used in Xcode 11. In order to make this work as we desire, we will also have to restore your #available check, because we have to make the project's deployment target iOS 13, and the Xcode 12 compiler will complain if we don't protect the iOS 14 code:
class ViewController: UIViewController {
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
#if swift(>=5.3)
if #available(iOS 14.0, *) {
let status = manager.authorizationStatus
print(status.rawValue)
}
#else
let status = CLLocationManager.authorizationStatus()
print(status.rawValue)
#endif
}
}
That code compiles and behaves correctly in either Xcode 11 or Xcode 12. Do you understand why? Let's review, because it's a bit tricky.
In Xcode 11, the whole #if section is never seen by the compiler. It sees only this:
let status = CLLocationManager.authorizationStatus()
print(status.rawValue)
That's good iOS 13 code, so all is well.
In Xcode 12, the whole #else section is never seen by the compiler. It sees only this:
if #available(iOS 14.0, *) {
let status = manager.authorizationStatus
print(status.rawValue)
}
That's good iOS 14 code, because, even though our project's deployment target is iOS 13, we have calmed the compiler's nerves by guaranteeing that this code won't execute in iOS 13 (where it would crash if it did execute).
Having said all that, the real answer is: don't. Everything I just did is way too much trouble! Once you've written code under Xcode 12, don't try to open that project in Xcode 11. That's not the way to test for backward compatibility.

iOS 11.1 feature in Xcode 9.0

(This is based on an issue here: https://github.com/dokun1/Lumina/issues/44)
Consider the following function:
fileprivate var discoverySession: AVCaptureDevice.DiscoverySession? {
var deviceTypes = [AVCaptureDevice.DeviceType]()
deviceTypes.append(.builtInWideAngleCamera)
if #available(iOS 10.2, *) {
deviceTypes.append(.builtInDualCamera)
}
if #available(iOS 11.1, *), self.captureDepthData == true {
deviceTypes.append(.builtInTrueDepthCamera)
}
return AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
}
I am running Xcode 9.0. I want to run a framework that makes use of this feature in iOS 11.1, which is only available in Xcode 9.1. The code in this function that gives an error is:
if #available(iOS 11.1, *), self.captureDepthData == true {
deviceTypes.append(.builtInTrueDepthCamera)
}
When running on Xcode 9.1 on someone else's machine, this works fine, and the application developing with this framework can set a development target of 10.0, and it compiles fine. However, I can't even build the framework on my machine. The error I get reads Type 'AVCaptureDevice.DeviceType' has no member 'builtInTrueDepthCamera' in Xcode 9.0 I thought using the #available macro would fix this, but it doesn't work that well.
I've also tried to use this:
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 111000
if #available(iOS 11.1, *), self.captureDepthData == true {
deviceTypes.append(.builtInTrueDepthCamera)
}
#endif
But this causes an error reading: Expected '&&' or '||' expression
Anyone know what to do?
#available will raise the "SDK Level" so that the compiler will allow you to use API calls above your Deployment target, but it won't prevent the compiler from compiling the lines inside the #available scope.
You need to prevent the compiler from compiling those lines because the compiler doesn't have a definition for .builtInTrueDepthCamera. You can do that using the #if build configuration statement.
In this case you want to check for swift version 4.0.2. Xcode 9.1 shipped with Swift 4.0.2.
#if swift(>=4.0.2)
if #available(iOS 11.1, *), self.captureDepthData == true {
deviceTypes.append(.builtInTrueDepthCamera)
}
#endif
source: https://www.bignerdranch.com/blog/hi-im-available/#what-it-is-not

`UTF16Index()` or `UTF16Index.init(encodedOffset:)` for Xcode 8/9 support

I'm on a development team that's supporting both Xcode 8 and Xcode 9.
I was developing a feature that used String.UTF16Index(range.location) in Xcode 8. When I upgraded to Xcode 9, that resulted in the error 'init' is deprecated.
So in Xcode 9 I changed it to UTF16Index.init(encodedOffset: range.lowerBound). However, now that doesn't work in Xcode 8 with the error Argument labels '(encodedOffset:)' do not match any available overloads.
Even if I could check the Xcode version and write different code, one of the lines would fail at compile time. How can I manage this? Or am I stuck waiting until we fully move to Xcode 9?
From Version Compatibility in the Swift documentation:
NOTE
When the Swift 4 compiler is working with Swift 3 code, it identifies its language version as 3.2. As a result, you can use conditional compilation blocks like #if swift(>=3.2) to write code that’s compatible with multiple versions of the Swift compiler.
In your case that would be
#if swift(>=3.2)
let idx = String.UTF16Index(encodedOffset: range.lowerBound)
#else
let idx = String.UTF16Index(range.location)
#endif
In Xcode 9 (Swift 4 or Swift 3.2 mode) only the first line is compiled,
and in Xcode 8 (Swift 3.1) only the second line is compiled.
Update: The use of encodedOffset is considered harmful and will be deprecated in Swift 5. Starting with Swift 4, the correct way to convert an NSRange to a Range<String.Index> is
let str = "abc"
let nsRange = NSRange(location: 2, length: 1)
let sRange = Range(nsRange, in: str)
As others have said, you can do a Swift version check to call the right API. But if you're calling this from multiple places then it might be easier to define a shim
#if swift(>=3.2)
// already correct
#else
extension String.UTF16Index {
init(encodedOffset: Int) {
self.init(encodedOffset)
}
}
#endif
Now you can just write String.UTF16Index(encodedOffset: range.lowerBound) and in Xcode 8 it will call your shim.

Swift: iOS Deployment Target Command Line Flag

How do I check the iOS deployment target in a Swift conditional compilation statement?
I've tried the following:
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
// some code here
#else
// other code here
#endif
But, the first expression causes the compile error:
Expected '&&' or '||' expression
TL;DR? > Go to 3. Solution
1. Preprocessing in Swift
According to Apple documentation on preprocessing directives:
The Swift compiler does not include a preprocessor. Instead, it takes
advantage of compile-time attributes, build configurations, and
language features to accomplish the same functionality. For this
reason, preprocessor directives are not imported in Swift.
That is why you have an error when trying to use __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 which is a C preprocessing directive. With swift you just can't use #if with operators such as <. All you can do is:
#if [build configuration]
or with conditionals:
#if [build configuration] && ![build configuration]
2. Conditional compiling
Again from the same documentation:
Build configurations include the literal true and false values,
command line flags, and the platform-testing functions listed in the
table below. You can specify command line flags using -D <#flag#>.
true and false: Won't help us
platform-testing functions: os(iOS) or arch(arm64) > won't help you, searched a bit, can't figure where they are defined. (in compiler itself maybe?)
command line flags: Here we go, that's the only option left that you can use...
3. Solution
Feels a bit like a workaround, but does the job:
Now for example, you can use #if iOSVersionMinRequired7 instead of __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0, assuming, of course that your target is iOS7.
That basically is the same than changing your iOS deployment target version in your project, just less convenient...
Of course you can to Multiple Build configurations with related schemes depending on your iOS versions targets.
Apple will surely improve this, maybe with some built in function like os()...
Tested in Swift 2.2
By saying Deployment Target you mean iOS version, or App Target? Below I'm providing the solution if you have multiple versions of the app (free app, payed app, ...), so that you use different App Targets.
You can set custom Build configurations:
1. go to your project / select your target / Build Settings / search for Custom Flags
2. for your chosen target set your custom flag using -D prefix (without white spaces), for both Debug and Release
3. do above steps for every target you have
To differentiate between targets you can do something like this:
var target: String {
var _target = ""
#if BANANA
_target = "Banana"
#elseif MELONA
_target = "Melona"
#else
_target = "Kiwi"
#endif
return _target
}
override func viewDidLoad() {
super.viewDidLoad()
print("Hello, this is target: \(target)"
}
I have come across this issue recently when building a library whose source code supports multiple iOS and macOS versions with different functionality.
My solution is using custom build flags in Xcode which derive their value from the actual deployment target:
TARGET_IOS_MAJOR = TARGET_IOS_MAJOR_$(IPHONEOS_DEPLOYMENT_TARGET:base)
TARGET_MACOS_MAJOR = TARGET_MACOS_MAJOR_$(MACOSX_DEPLOYMENT_TARGET:base)
Referring to those user defined settings in Others Swift Flags like:
OTHER_SWIFT_FLAGS = -D$(TARGET_MACOS_MAJOR) -D$(TARGET_IOS_MAJOR)
allows me to check for the actual major OS version in my Swift sources as follows:
#if os(macOS)
#if TARGET_MACOS_MAJOR_12
#warning("TARGET_MACOS_MAJOR_12")
#elseif TARGET_MACOS_MAJOR_11
// use custom implementation
#warning("TARGET_MACOS_MAJOR_11")
#endif
#elseif os(iOS)
#if TARGET_IOS_MAJOR_15
#warning("TARGET_IOS_MAJOR_15")
#elseif TARGET_IOS_MAJOR_14
#warning("TARGET_IOS_MAJOR_14")
#else
#warning("older iOS")
#endif
#endif
Currently I don't know if a similar approach would be possible in a SPM package. This is something I will try to do in a later phase.
You can't do it in a conditional compilation statement like that. "Complex macros" as Apple calls them are not supported in Swift. Generics and types do the same thing, in their mind, with better results. (Here's a link they published https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html#//apple_ref/doc/uid/TP40014216-CH8-XID_13)
Here's a function I came up with that accomplishes the same thing (and obviously just replace the string returns with whatever is useful for you like a boolean or just the option itself):
func checkVersion(ref : String) -> String {
let sys = UIDevice.currentDevice().systemVersion
switch sys.compare(ref, options: NSStringCompareOptions.NumericSearch, range: nil, locale: nil) {
case .OrderedAscending:
return ("\(ref) is greater than \(sys)")
case .OrderedDescending:
return ("\(ref) is less than \(sys)")
case .OrderedSame:
return ("\(ref) is the same as \(sys)")
}
}
// Usage
checkVersion("7.0") // Gives "7.0 is less than 8.0"
checkVersion("8.0") // Gives "8.0 is the same as 8.0"
checkVersion("8.2.5") // Gives "8.2.5 is greater than 8.0"
I know your question is been here for a while but just in case someone's still looking for an answer they should know that starting with Swift 2.0 you can do something like this:
if #available(iOS 8, *) {
// iOS 8+ code
} else {
// older iOS code
}
You can read more about it here.

Resources