How to detect current device using SwiftUI? - ios

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

Related

In Swift, is there a way to access the native property from the getter overridden by an Extension?

I have a iOS app that uses a library. This library used to have problems when the iOS or iPadOS device or simulator was set to be in Zoom mode in the system settings. The developer of the library has provided a fix, but he has told me that in order to benefit from the fix, I have to use UIScreen nativeScale instead of scale. I tried to solve the problem without affecting the rest of the project code by declaring an extension like this:
extension UIScreen {
open var scale: CGFloat {
get {
return nativeScale
}
}
}
This works, but I would like somehow to make this extension effective only when the device is set to zoom mode. I have found a way to detect that, but now I don't know how to proceed. I am aware that I cannot access the scale property from within its getter, so I was wondering if there is a possible implementation to achieve that. Thanks for your attention.
The comment from Cristik made me think of a solution. Cristik asked for the code to detect Zoom mode. I have found a good implementation in the DeviceKit repository. The solution is the following:
public var isZoomed: Bool? {
guard isCurrent else { return nil }
if Int(UIScreen.main.scale.rounded()) == 3 {
// Plus-sized
return UIScreen.main.nativeScale > 2.7 && UIScreen.main.nativeScale < 3
} else {
return UIScreen.main.nativeScale > UIScreen.main.scale
}
}
To achieve my goal, I added to DeviceKit this property:
public var originalDeviceScale: CGFloat? {
guard isCurrent else { return nil }
return UIScreen.main.scale
}
Being in a completely different target, my extension to UIScreen in the main project does not apply, so I can now write:
import UIKit
import DeviceKit
extension UIScreen {
open var scale: CGFloat {
get {
let device = Device()
guard let isZoomed = device.isZoomed else {
return -1
}
if isZoomed {
return nativeScale
}
guard let originalDeviceScale = device.originalDeviceScale else {
return -1
}
return originalDeviceScale
}
}
If you are not using DeviceKit, you could add a static library target to your project containing just the originalNativeScale property.
I understand this is not a complete solution but could be possibly useful to others. Thanks for your attention.

Detecting if iPhone or iPad without opting for Universal App

I really want this to work:
if UIDevice.current.userInterfaceIdiom == .pad {
print("iPad")
} else {
print("not iPad")
}
However, my app only prints "not iPad" even though I am using an iPad. I have Devices (under Deployment Info) set to iPhone. If I change this to Universal it works, but I don't want a universal app, I just want to be able to detect if an iPhone or iPad is being used (even though the app is for iPhones, due to compatibility mode it still can be run on iPads).
So how can I detect if the device is an iPad or iPhone without changing my app to Universal? Thanks
You can check the model:
if UIDevice.current.model.hasPrefix("iPad") {
print("it is an iPad")
} else {
print("it is not an iPad")
}
One thing you can do is get the inner-screen width of the page. Phones are generally below 786 px and you can call everything else an iPad. Use can do something like this
var width = window.innerWidth;
If (width > 786px) {
print(‘ipad’);
} else {
print(‘not ipad’)
}
iPhone Only app can be downloaded to iPad. But in current scenario, we doesn't have device that's much smaller resolution(deployment target: 9).
OBJ-C
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && [[[UIDevice currentDevice] model] hasPrefix:#"iPad"]) {
// This app is an iPhone app running on an iPad
NSLog(#"This app is an iPhone app running on an iPad");
}
Swift
if UIDevice.current.userInterfaceIdiom == .phone, UIDevice.current.model.hasPrefix("iPad") {
print("iPad")
}
Try this,
print("Model - \(UIDevice.current.model)")

Check if device supports UIFeedbackGenerator in iOS 10

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
}

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