Check if a function is available in Swift? - ios

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

Related

How to detect if iOS devices is connected using wifi in Swift?

In an old, Objective-C based project I have been using the below code to detect if the iOS devices is currently connected using Wifi (not cellular).
My attempts to translate this code into Swift 5 failed due to the Objective-C pointers. Is there a clean way to use this solution in Swift?
Or are are there better ways to solve this nowerdays? I found solutions using the Reachability port to Swift or NWPathMonitor(). While they seem to work in general, these solution are used to monitor the connection state and send notifications on changes while one time checks are not (well) supported.
Event though these solution could be used to get the current connection state, this is done using delegate callback methods or closures. Thus it is not possible to use these solutions in existing code which was created to work "synchronously" (without callbacks/closures).
Is there a simply way to use localWiFiAvailable in Swift?
The code:
+ (BOOL)localWiFiAvailable {
struct ifaddrs *addresses;
struct ifaddrs *cursor;
BOOL wiFiAvailable = NO;
if (getifaddrs(&addresses) != 0) return NO;
cursor = addresses;
while (cursor != NULL) {
if ((cursor -> ifa_addr -> sa_family == AF_INET) && !(cursor -> ifa_flags & IFF_LOOPBACK)) { // Ignore the loopback address
// Check for WiFi adapter
#if TARGET_IPHONE_SIMULATOR
wiFiAvailable = true;
break;
#else
if (strcmp(cursor -> ifa_name, "en0") == 0) {
wiFiAvailable = YES;
break;
}
#endif
}
cursor = cursor -> ifa_next;
}
freeifaddrs(addresses);
return wiFiAvailable;
}
Details on why NWPathMonitor() cannot be used:
As #baronfac pointed out in his comment NWPathMonitor() can also deliver the current state, but this can only be done using its .pathUpdateHandler closure.
I am using a third-party library where I can override a souldSendData() -> Bool method. Sending the data should not be allowed on mobile connection but only on WiFi. The methodes requires an instant decision to return true or false. Waiting for the closure is thus not possible.
So, I am limited by the existing class here. Yes, connection could change any second, however this is a different problem. e.g. NWPathMonitor can be used to cancel the transfer when connection changes to mobile.
Solving this problem in Objectiv-C was no problem using the code shown above. The question is simply, if such a "direct" solution is possible in Swift as well. While using the Objectiv-C code in the Swift project would be possible I would prefer to keep the project Swift only.
As mentioned by Paulw11, the recommended approach is using NWPathMonitor. A common practice is the following within a UIViewController - class:
private var monitor: NWPathMonitor?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
monitor = NWPathMonitor()
monitor?.pathUpdateHandler = { [weak self] path in
if !path.isExpensive { // this means the device is connected via WiFi
// enter your code here
}
}
let queue = DispatchQueue(label: "Monitor")
monitor?.start(queue: queue) // start to monitor the connection
}
override func viewWillDisappear(_ animated: Bool) {
monitor?.cancel() // end to monitor the connection
super.viewWillDisappear(animated)
}
EDIT:
Thanks to FLichter and Rob Napier for the clarification. Maybe it helps to use this approach:
func shouldSendData() -> Bool {
let monitor = NWPathMonitor()
return !monitor.currentPath.isExpense
}

showsRouteButton from MPVolumeView is deprecated

Since iOS 13, showsRouteButton from MPVolumeView has been deprecated
let vv = MPVolumeView()
vv.showsRouteButton = false
Warning is :
'showsRouteButton' was deprecated in iOS 13.0: Use AVRoutePickerView instead.
Apple is telling me to use AVRoutePickerView for routing, which makes no sense as in my case I do not want to use any routing stuff, I only want to hide it.
It seems there's no more not deprecated way to do this.
If it's deprecated it should be hidden by default else apple should allow us to hide it...
Am I right to say it's an apple API error ?
For now just to remove the warning and the default route button I used this immediately after initializing the MPVolumeView.
if volumeView.value(forKey: #keyPath(MPVolumeView.showsRouteButton)) as? Bool == true {
volumeView.setValue(false, forKey: #keyPath(MPVolumeView.showsRouteButton))
}
I check via key value paths if the value of showsRouteButton is true and sets it to false if it is.
Safe and forward compatible
In iOS 15.0 the route button of MPVolumeView shows the route button as a subview. The safest way to get rid of it without a deprecation warning is to look for the button and hide it:
volume.subviews.first(where: { $0 is UIButton })?.isHidden = true
Unlike just ignoring the warning or setting the value with key-value coding (as suggested by #spasbil), this will not crash the app in a future iOS release where Apple may have removed the deprecated showsRouteButton. However, it has the disadvantage of leaving an empty space right of the slider.
Setting the value through key-value coding does lead to a visually more satisfying solution, where the slider extends over the full width of the MPVolumeView. Using a #keypath expression as suggested by #spasbil shows the same warning in Xcode 13.0. Using a String literal rather than a #keypath expression, as suggested in a comment, avoids this warning. It does not resolve the danger of crashing on future iOS releases where showsRouteButton will have disappeared.
Apple never tells us when they will remove a deprecated property, but they rarely do it in a minor release. So we can hope that showsRouteButton will stay around at least for iOS 15.* releases. A forward compatible way to get rid of the route button is therefore:
if #available(iOS 16, *) {
volumeView.subviews.first(where: { $0 is UIButton })?.isHidden = true
} else {
volumeView.setValue(false, forKey: "showsRouteButton")
}
We might need to get back to this during the next major release beta season, presumably next summer. In the meantime, use this at your own risk. In the unlikely case that Apple removes the property in a later release of iOS 15.*, your app will crash. In the more likely case that showsRouteButton stays available but deprecated in iOS 16, your MPVolumeView will leave some unwanted space until you bump up the availability check.
This works for me, not sure if it robust:
import MediaPlayer
extension MPVolumeView {
/// Compiler warning 'showsRouteButton' was deprecated in iOS 13.0: Use AVRoutePickerView instead.
/// But the route button will show by default if we don't set it false.
/// We can set by key value to silent the warning. But also need prevent Apple remove the key in the future.
func hideRouterButtonIfNecessary() {
let bugKey = "showsRouteButton"
var count: UInt32 = 0
guard let properties = class_copyPropertyList(MPVolumeView.self, &count) else { return }
for i in 0..<Int(count) {
let property = properties[i]
let name = property_getName(property)
let str = String(cString: name)
if str == bugKey {
self.setValue(false, forKey: bugKey)
break
}
}
free(properties)
}
}

INIntent `setImage` make runtime crash in Swift 5

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...)

Compile-time test for availability of UIApplication.shared?

I have some shared code that needs to work in both iOS apps and app extensions, and needs to set UIApplication.shared.isNetworkActivityIndicatorVisible — but only if the code is used in an app.
In an app extension, UIApplication.shared gives this compile error:
'shared' is unavailable: Use view controller based solutions where appropriate instead
That’s fine; I don’t want to use it in the app extension. However, I’m unable to find a way to disable that code at compile time. Sadly, if #available doesn’t seem to do the trick; it shuts off the code path, but the compiler still doesn’t like it:
if #available(iOSApplicationExtension 0, *) {
print("This is an extension")
} else {
print("This is an app")
print(UIApplication.shared) // Unreachable in extension, but still doesn’t compile
}
I don’t see any #if check that handles this.
Is there any way in Swift to conditionally compile the code that requires UIApplication.shared?
A possible solution is to avoid explicit usage of UIApplication.shared and use Objective-C selector wrap instead.
Here is an extension that might help (based on https://github.com/ephread/Instructions/issues/21)
extension UIApplication {
static var safeShared: UIApplication? {
guard UIApplication.responds(to: Selector(("sharedApplication"))) else {
return nil
}
guard let unmanagedSharedApplication = UIApplication.perform(Selector(("sharedApplication"))) else {
return nil
}
return unmanagedSharedApplication.takeRetainedValue() as? UIApplication
}
}
Usage
if let app = UIApplication.safeShared {
result = app.applicationState == .active
}
Happy Coding 👨‍💻

Detecting available API iOS vs. watchOS in Swift

#available does not seem to work when differentiating between watchOS and iOS.
Here is an example of code shared between iOS & watchOS:
lazy var session: WCSession = {
let session = WCSession.defaultSession()
session.delegate = self
return session
}()
...
if #available(iOS 9.0, *) {
guard session.paired else { throw WatchBridgeError.NotPaired } // paired is not available
guard session.watchAppInstalled else { throw WatchBridgeError.NoWatchApp } // watchAppInstalled is not available
}
guard session.reachable else { throw WatchBridgeError.NoConnection }
Seems that it just defaults to WatchOS and the #available is not considered by the compiler.
Am I misusing this API or is there any other way to differentiate in code between iOS and WatchOS?
Update: Seems like I was misusing the API as mentioned by BPCorp
Using Tali's solution for above code works:
#if os(iOS)
guard session.paired else { throw WatchBridgeError.NotPaired }
guard session.watchAppInstalled else { throw WatchBridgeError.NoWatchApp }
#endif
guard session.reachable else { throw WatchBridgeError.NoConnection }
Unfortunately there is no #if os(watchOS) .. as of Xcode 7 GM
Edit: Not sure when it was added but you can now do #if os(watchOS) on Xcode 7.2
If you want to execute that code only on iOS, then use #if os(iOS) instead of the if #available(iOS ...).
This way, you are not using a dynamic check for the version of your operating system, but are compiling a different code for one OS or the other.
In the Apple dev guide, it is said that the star, * (which is required) means that it will execute the if body for OSes not specified but listed in the minimum deployment target specified by your target.
So, if your target specifies iOS and watchOS, your statement if #available(iOS 9.0, *) means that the ifbody is available for iOS 9 and later and any watchOS version.
Also, be careful if you want to use what's described in the chapter "Build Configurations" in this Apple guide. It is used to conditionally compile your code based on the operating system. This is not dynamic at runtime.
With the GM version of Xcode7 I think they fixed that issue. For me :
if #available(watchOS 2,*) {
// Only if using WatchOS 2 or higher
}
is working fine in GM version.
Time have passed since the question. If somebody still looking for the answer, need to say that
#if os(watchOS)
is now available in Xcode 13 and later.

Resources