Compile-time test for availability of UIApplication.shared? - ios

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 👨‍💻

Related

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

How to detect whether custom keyboard is activated from the keyboard's container app?

I was wondering if there is a method that would allow me to detect from the keyboard container app whether the associated keyboard has been activated in the the device's Settings app.
For example, I am interested in adding a simple "steps" feature inside the container app where step 1 would be "activate the keyboard", and step 2 would be contingent on step 1's completion. As such, I am interested in figuring out whether there is a way to detect whether the keyboard extension is activated?
Thanks!
Here is a method I have used in one of my projects. I think it is what you asked for, hope it helps you.
- (BOOL)isCustomKeyboardEnabled {
NSString *bundleID = #"com.company.app.customkeyboard"; // Replace this string with your custom keyboard's bundle ID
NSArray *keyboards = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] objectForKey:#"AppleKeyboards"]; // Array of all active keyboards
for (NSString *keyboard in keyboards) {
if ([keyboard isEqualToString:bundleID])
return YES;
}
return NO;
}
Just in case here is Swift version of Kurt's brilliant and awesome answer:
func isKeyboardExtensionEnabled() -> Bool {
guard let appBundleIdentifier = Bundle.main.bundleIdentifier else {
fatalError("isKeyboardExtensionEnabled(): Cannot retrieve bundle identifier.")
}
guard let keyboards = UserDefaults.standard.dictionaryRepresentation()["AppleKeyboards"] as? [String] else {
// There is no key `AppleKeyboards` in NSUserDefaults. That happens sometimes.
return false
}
let keyboardExtensionBundleIdentifierPrefix = appBundleIdentifier + "."
for keyboard in keyboards {
if keyboard.hasPrefix(keyboardExtensionBundleIdentifierPrefix) {
return true
}
}
return false
}
The current documentation states By default, your extension and its containing app have no direct access to each other’s containers.
It is also stating that the container app can share data with the keyboard in the following fashion:
// Create and share access to an NSUserDefaults object.
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc]
initWithSuiteName:#"com.example.domain.MyShareExtension"];
// Use the shared user defaults object to update the user's account.
[mySharedDefaults setObject:theAccountName forKey:#"lastAccountName"];
Read more on this: Communicating and persisting data between apps with App Groups
Obstacle no 1: According to the documentation, for this to work, the RequestsOpenAccess in the plist needs to be set to YES as it would gain the following capability:
Option to use a shared container with the keyboard’s containing app,
which enables features such as providing a custom lexicon management
UI in the containing app
Requesting full access for a simple case like this is definitely not preferred on my side.
Obstacle no 2: Using this knowledge of setting a NSUserDefault, leaves me to think of a method where this can be set in place. But there's no public method indicating an extension is installed. So this is a dead end for now.
--
[Update 1]
Not super relevant but still worth stating: the shouldAllowExtensionPointIdentifier app delegate method in combination with the constant UIApplicationKeyboardExtensionPointIdentifier can deal with disallowing custom keyboards. The extension point identifiers are not unique identifiers of the extension but of their type.
Read more on this: Can I disable custom keyboards (iOS8) for my app?
--
[Update 2]
Another question with same issue, but w/o solution: How to detect an app extension is enabled in containing app on iOS 8?
--
This is a work-in-progress answer stating my findings so far which I hope to be updating coming days should I find a solution.
You can use this function (Swift 3 and 4) to check your custom keyboard extension have open access or not:
func isOpenAccessGranted() -> Bool{
if #available(iOS 10.0, *) {
let originalString = UIPasteboard.general.string
UIPasteboard.general.string = "Sour LeangChhean"
if UIPasteboard.general.hasStrings {
UIPasteboard.general.string = originalString ?? ""
return true
}else{
UIPasteboard.general.string = ""
return false
}
} else {
// Fallback on earlier versions
if UIPasteboard.general.isKind(of: UIPasteboard.self) {
return true
}else{
return false
}
}
}

Check for class existence in Swift

I want to use NSURLQueryItem in my Swift iOS app. However, that class is only available since iOS 8, but my app should also run on iOS 7. How would I check for class existence in Swift?
In Objective-C you would do something like:
if ([NSURLQueryItem class]) {
// Use NSURLQueryItem class
} else {
// NSURLQueryItem is not available
}
Related to this question is: How do you check for method or property existence of an existing class?
There is a nice section in https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/AdvancedAppTricks/AdvancedAppTricks.html#//apple_ref/doc/uid/TP40007072-CH7-SW4 called Supporting Multiple Versions of iOS, which explains different techniques for Objective-C. How can these be translated to Swift?
Swift 2.0 provides us with a simple and natural way to do this.It is called API Availability Checking.Because NSURLQueryItem class is only available since iOS8.0,you can do in this style to check it at runtime.
if #available(iOS 8.0, *) {
// NSURLQueryItem is available
} else {
// Fallback on earlier versions
}
Simplest way I know of
if NSClassFromString("NSURLQueryItem") != nil {
println("NSURLQueryItem exists")
}else{
println("NSURLQueryItem does not exists")
}
Try this:
if objc_getClass("NSURLQueryItem") != nil {
// iOS 8
} else {
// iOS 7
}
I've also done it like this too:
if let theClass: AnyClass = NSClassFromString("NSURLQueryItem") {
// iOS 8
} else {
// iOS 7
}
Or, you can also check system version like so, but this isn't the best practice for iOS dev - really you should check if a feature exists. But I've used this for a few iOS 7 hacks... pragmatism over purity.
switch UIDevice.currentDevice().systemVersion.compare("8.0.0", options: NSStringCompareOptions.NumericSearch) {
case .OrderedSame, .OrderedDescending:
iOS7 = false
case .OrderedAscending:
iOS7 = true
}

Resources