Check if device supports UIFeedbackGenerator in iOS 10 - ios

In iOS 10, there is a new api which allows developers to make use of the taptic engine, UIFeedbackGenerator.
While this api is available in iOS 10, it only works on the new devices, iPhone 7 and 7 plus. It does not works on older devices including the 6S or 6S Plus, even those have a taptic engine. I guess the taptic engine on the 7 and 7 plus is a different more powerful one.
I can't seem to find a way to see if the device supports using the new api. I would like to replace some vibrate code with taptic code, where it makes sense.
Edit:
Adding the 3 concrete subclasses for search purposes:
UIImpactFeedbackGenerator
UINotificationFeedbackGenerator
UISelectionFeedbackGenerator
Edit 2:
I have a theory but no iPhone 7 device to test it so if you have one, give it a shot. UIFeedbackGenerator has a methods called prepare(). When printing out an instance of UIImpactFeedbackGenerator, I noticed that it printed a property named "prepared" which would show 0. Calling prepare() in simulator or on iPhone 6S and then printing out the instance still shows prepared as 0. Can someone call prepare() on an instance of UIImpactFeedbackGenerator from an iPhone7 and then print the instance to console to see if prepared is set to 1? This value is not exposed but there may be a way to get this info w/o using private apis.

So, apparently this can be done with a private API call.
Objective-C:
[[UIDevice currentDevice] valueForKey:#"_feedbackSupportLevel"];
Swift:
UIDevice.currentDevice().valueForKey("_feedbackSupportLevel");
... These methods seem to return:
0 = Taptic not available
1 = First generation (tested on an iPhone 6s) ... which does NOT support UINotificationFeedbackGenerator, etc.
2 = Second generation (tested on an iPhone 7) ... which does support it.
Unfortunately, there are two caveats here:
Using these could get your app rejected by Apple during the App Store's App Review, but there doesn't seem to be any other way currently.
We don't know what the actual values represent.
Special thanks to Tim Oliver and Steve T-S for helping test this with different devices. https://twitter.com/TimOliverAU/status/778105029643436033

Currently, the best way is to check the device's model using:
public extension UIDevice
public func platform() -> String {
var sysinfo = utsname()
uname(&sysinfo) // ignore return value
return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters)
}
}
The platform names for iPhone 7 and 7 plus are: "iPhone9,1", "iPhone9,3", "iPhone9,2", "iPhone9,4"
Source: iOS: How to determine the current iPhone/device model in Swift?
You can create a function:
public extension UIDevice {
public var hasHapticFeedback: Bool {
return ["iPhone9,1", "iPhone9,3", "iPhone9,2", "iPhone9,4"].contains(platform())
}
}

I have extended chrisamanse's answer. It extraxts the generation number from the model identifier and checks if it is equal or greater than 9. Should work with future iPhone models unless Apple decides to introduce a new internal naming scheme.
public extension UIDevice {
var modelIdentifier: String {
var sysinfo = utsname()
uname(&sysinfo) // ignore return value
return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters)
}
var hasHapticFeedback: Bool {
// assuming that iPads and iPods don't have a Taptic Engine
if !modelIdentifier.contains("iPhone") {
return false
}
// e.g. will equal to "9,5" for "iPhone9,5"
let subString = String(modelIdentifier[modelIdentifier.index(modelIdentifier.startIndex, offsetBy: 6)..<modelIdentifier.endIndex])
// will return true if the generationNumber is equal to or greater than 9
if let generationNumberString = subString.components(separatedBy: ",").first,
let generationNumber = Int(generationNumberString),
generationNumber >= 9 {
return true
}
return false
}
}
Use it like so:
if UIDevice.current.hasHapticFeedback {
// work with taptic engine
} else {
// fallback for older devices
}

class func isFeedbackSupport() -> Bool {
if let value = UIDevice.current.value(forKey: "_feedbackSupportLevel") {
let result = value as! Int
return result == 2 ? true : false
}
return false
}

Related

How to detect current device using SwiftUI?

I am trying to determine whether the device being used is iPhone or iPad.
Please see this question: Detect current device with UI_USER_INTERFACE_IDIOM() in Swift
That solution works if you use UIKit. But
What is the equivalent method if you are using SwiftUI ?
You can find the device type, like this:
if UIDevice.current.userInterfaceIdiom == .pad {
...
}
You can use this:
UIDevice.current.localizedModel
In your case, an implementation method could be:
if UIDevice.current.localizedModel == "iPhone" {
print("This is an iPhone")
} else if UIDevice.current.localizedModel == "iPad" {
print("This is an iPad")
}
Obviously, you can use this for string interpolation such as this (assuming the current device type is an iPhone):
HStack {
Text("Device Type: ")
Text(UIDevice.current.localizedModel)
}
//Output:
//"Device Type: iPhone"
It will return the device type. (iPhone, iPad, AppleWatch) No further imports are necessary aside from SwiftUI which should have already been imported upon creation of your project if you selected SwiftUI as the interface.
NOTE: It does not return the device model (despite the ".localizedModel")
Hope this helps!
UIDevice is no longer available from WatchKit 2 on
WKInterfaceDevice could be helpful: https://developer.apple.com/documentation/watchkit/wkinterfacedevice
i.e.:
let systemName = WKInterfaceDevice.current().systemName
let systemVersion = WKInterfaceDevice.current().systemVersion
let deviceModel = WKInterfaceDevice.current().model

Is there a way to tell if a MIDI-Device is connected via USB on iOS?

I'm using CoreMIDI to receive messages from a MIDI-Keyboard via Camera Connection Kit on iOS-Devices. My App is about pitch recognition. I want the following functionality to be automatic:
By default use the microphone (already implemented), if a MIDI-Keyboard is connected use that instead.
It's could find out how to tell if it is a USB-Keyboard using the default driver. Just ask for the device called "USB-MIDI":
private func getUSBDeviceReference() -> MIDIDeviceRef? {
for index in 0..<MIDIGetNumberOfDevices() {
let device = MIDIGetDevice(index)
var name : Unmanaged<CFString>?
MIDIObjectGetStringProperty(device, kMIDIPropertyName, &name)
if name!.takeRetainedValue() as String == "USB-MIDI" {
return device
}
}
return nil
}
But unfortunately there are USB-Keyboards that use a custom driver. How can I tell if I'm looking at one of these? Standard Bluetooth- and Network-Devices seem to be always online. Even if Wifi and Bluetooth are turned of on the device (strange?).
I ended up using the USBLocationID. It worked with any device I tested so far and none of the users complained.But I don't expect many users to use the MIDI-Features of my app.
/// Filters all `MIDIDeviceRef`'s for USB-Devices
private func getUSBDeviceReferences() -> [MIDIDeviceRef] {
var devices = [MIDIDeviceRef]()
for index in 0..<MIDIGetNumberOfDevices() {
let device = MIDIGetDevice(index)
var list: Unmanaged<CFPropertyList>?
MIDIObjectGetProperties(device, &list, true)
if let list = list {
let dict = list.takeRetainedValue() as! NSDictionary
if dict["USBLocationID"] != nil {
devices.append(device)
}
}
}
return devices
}

How to tell which device I'm on in Xcode UI Testing?

While an Xcode UI Test is running, I want to know which device/environment is being used (e.g. iPad Air 2, iOS 9.0, Simulator).
How can I get this information?
Using Swift 3 (change .pad to .phone as necessary):
if UIDevice.current.userInterfaceIdiom == .pad {
// Ipad specific checks
}
Using older versions of Swift:
UIDevice.currentDevice().userInterfaceIdiom
Unfortunately there is no direct way of querying the current device. However you can work around by querying the size classes of the device:
private func isIpad(app: XCUIApplication) -> Bool {
return app.windows.elementBoundByIndex(0).horizontalSizeClass == .Regular && app.windows.elementBoundByIndex(0).verticalSizeClass == .Regular
}
As you can see in the Apple Description of size classes, only iPad devices (currently) have both vertical and horizontal size class "Regular".
You can check using the windows element frame XCUIApplication().windows.element(boundBy: 0).frame and check the device type.
You can also set an extension for XCUIDevice with a currentDevice property:
/// Device types
public enum Devices: CGFloat {
/// iPhone
case iPhone4 = 480
case iPhone5 = 568
case iPhone7 = 667
case iPhone7Plus = 736
/// iPad - Portraite
case iPad = 1024
case iPadPro = 1366
/// iPad - Landscape
case iPad_Landscape = 768
case iPadPro_Landscape = 0
}
/// Check current device
extension XCUIDevice {
public static var currentDevice:Devices {
get {
let orientation = XCUIDevice.shared().orientation
let frame = XCUIApplication().windows.element(boundBy: 0).frame
switch orientation {
case .landscapeLeft, .landscapeRight:
return frame.width == 1024 ? .iPadPro_Landscape : Devices(rawValue: frame.width)!
default:
return Devices(rawValue: frame.height)!
}
}
}
}
Usage
let currentDevice = XCUIDevice.currentDevice
Maybe someone would be come in handy the same for XCTest on Objective C:
// Check if the device is iPhone
if ( ([[app windows] elementBoundByIndex:0].horizontalSizeClass != XCUIUserInterfaceSizeClassRegular) || ([[app windows] elementBoundByIndex:0].verticalSizeClass != XCUIUserInterfaceSizeClassRegular) ) {
// do something for iPhone
}
else {
// do something for iPad
}
Swift: 5.2.4
Xcode: 11.6
var isiPad: Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
With iOS13+, you can now use UITraitCollection.current to get the complete set of traits for the current environment. (This is the "iOS interface environment for your app, including traits such as horizontal and vertical size class, display scale, and user interface idiom." doc)
In your case, you can access its property .userInterfaceIdiom to check for one of the device types in the UIUserInterfaceIdiom enumeration.
As an aside, if you just want to get the horizontal/vertical size classes of the trait collection, you can be more backwards compatible with your tests (Xcode 10.0+) just by accessing myXCUIElement.horizontalSizeClass and .verticalSizeClass within your test, as they are exposed via the XCUIElementAttributes protocol that all UI elements adopt. (Note though that I was getting .unspecified when calling off of XCUIApplication(); best to use a real UI element in a window. If you don't have one on hand, you can always still use something like app!.windows.element(boundBy: 0).horizontalSizeClass == .regular as mentioned in the past.)

Check if device supports blur

My app uses UIBlurEffect, however older devices (specifically iPads 2 and 3, that support iOS 8) don't have blur support.
I'd like to check if the user's device has blur support or not. How do I do it?
UIDevice has an internal method [UIDevice _graphicsQuality] that seems promising, but of course your app will be rejected by Apple. Let's create our own method:
First of all, we need to know the exact device type we're working on:
#import <sys/utsname.h>
NSString* deviceName()
{
struct utsname systemInfo;
uname(&systemInfo);
return [NSString stringWithCString:systemInfo.machine
encoding:NSUTF8StringEncoding];
}
This should return iPad2,1 for iPad 2, for example. Here's an updated list of iDevice models: https://theiphonewiki.com/wiki/Models
So, let's classify our device models in two groups: those that have poor graphics quality (and thus don't support blur), and those with great graphics quality. According to my investigation, these are the devices that Apple considers with "poor" graphics (these may change in the future):
iPad iPad1,1 iPhone1,1 iPhone1,2 iPhone2,1 iPhone3,1 iPhone3,2
iPhone3,3 iPod1,1 iPod2,1 iPod2,2 iPod3,1 iPod4,1 iPad2,1 iPad2,2
iPad2,3 iPad2,4 iPad3,1 iPad3,2 iPad3,3
So we write the following code:
NSSet *graphicsQuality = [NSSet setWithObjects:#"iPad",
#"iPad1,1",
#"iPhone1,1",
#"iPhone1,2",
#"iPhone2,1",
#"iPhone3,1",
#"iPhone3,2",
#"iPhone3,3",
#"iPod1,1",
#"iPod2,1",
#"iPod2,2",
#"iPod3,1",
#"iPod4,1",
#"iPad2,1",
#"iPad2,2",
#"iPad2,3",
#"iPad2,4",
#"iPad3,1",
#"iPad3,2",
#"iPad3,3",
nil];
if ([graphicsQuality containsObject:deviceName()]) {
// Device with poor graphics, blur not supported
} else {
// Blur supported
}
Be careful because even though the device may support blur, the user may have disabled advanced visual effects from Settings, Accessibility.
Alternative method
https://gist.github.com/conradev/8655650
Here is a Swift version of Daniel Martin's answer. Some of the code was adapted from here:
func isBlurSupported() -> Bool {
var supported = Set<String>()
supported.insert("iPad")
supported.insert("iPad1,1")
supported.insert("iPhone1,1")
supported.insert("iPhone1,2")
supported.insert("iPhone2,1")
supported.insert("iPhone3,1")
supported.insert("iPhone3,2")
supported.insert("iPhone3,3")
supported.insert("iPod1,1")
supported.insert("iPod2,1")
supported.insert("iPod2,2")
supported.insert("iPod3,1")
supported.insert("iPod4,1")
supported.insert("iPad2,1")
supported.insert("iPad2,2")
supported.insert("iPad2,3")
supported.insert("iPad2,4")
supported.insert("iPad3,1")
supported.insert("iPad3,2")
supported.insert("iPad3,3")
return !supported.contains(hardwareString())
}
func hardwareString() -> String {
var name: [Int32] = [CTL_HW, HW_MACHINE]
var size: Int = 2
sysctl(&name, 2, nil, &size, &name, 0)
var hw_machine = [CChar](count: Int(size), repeatedValue: 0)
sysctl(&name, 2, &hw_machine, &size, &name, 0)
let hardware: String = String.fromCString(hw_machine)!
return hardware
}
Many thanks do Daniel Martin for the great answer.
I have made a UIDevice category that combines the following checks for availability of the blur effect:
iOS version
Device type (thanks to Daniel Martin)
Private UIDevice method _graphicsQuality when building for the simulator.
(This is done since the device type check does not work in the simulator - will be compiled out when building for arm architectures)
'Reduce Transparency' accessibility setting
I've made a gist containing the category:
https://gist.github.com/mortenbekditlevsen/5a0ee16b73a084ba404d
And a small writeup about the issue as a whole:
http://mojoapps.dk/?p=1
Note: When using the gist, don't forget to subscribe to the UIAccessibilityReduceTransparencyStatusDidChangeNotification notification to refresh the UI when the accessibility setting changes.
I hope that someone finds this useful. :-)
Here are a few ways to do this:
if objc_getClass("UIBlurEffect") != nil {
// UIBlurEffect exists
} else {
// UIBlurEffect doesn't exist
}
Or with access to the blurEffect class
if let blurEffect: AnyClass = NSClassFromString("UIBlurEffect") {
// UIBlurEffect exists
} else {
// UIBlurEffect doesn't exist
}
There's actually a duplicate question here, although the title probably doesn't make it easy to find: How do I do weak linking in Swift?
-- Edit --
Misunderstood the question. There appears to be no way to find this out short of checking the device name:
Detect if device properly displays UIVisualEffectView?
I would rely on whatever Apple's fallback implementation is. It looks like they still apply the tint color so that is probably fine in most cases. If you really must change the appearance when no blur will be applied, I think device name is not terrible since they specifically outlined which devices do not support blur.

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