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

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

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

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

iPhone simulator (detect different device simulators)

With the upcoming release of the iPhone X, I want to be able to display a different UI layout for the iPhone X (due to round corners and bottom line, which kinda works as the home button replacement).
I am using the following nuget package to retrieve the model information:
https://github.com/dannycabrera/Get-iOS-Model
It works perfectly fine, but all the different simulators (iPhone 7, 8, X) only come up as Simulator.
Is there a way to differentiate between the different iPhone Simulators in code within my Xamarin mobile app?
Many thanks,
Nik
Since the simulator is a weird animal, the screen size is as good as any other test after the other tests for iOS version and the availability of FaceID on a physical device:
public bool iPhoneX()
{
var version = new Version(ObjCRuntime.Constants.Version);
if (version < new Version(11, 0))
return false;
if (ObjCRuntime.Runtime.Arch == ObjCRuntime.Arch.DEVICE)
{
using (var context = new LocalAuthentication.LAContext())
{
if (context.BiometryType == LABiometryType.TypeFaceId)
return true;
}
return false;
}
if (UIScreen.MainScreen.PreferredMode.Size.Height == 2436)
return true;
return false;
}
Or an optimized property for repeated (binding) calls:
static bool? iPhoneX;
public bool isPhoneX
{
get
{
if (iPhoneX == null)
{
if (new Version(ObjCRuntime.Constants.Version) < new Version(11, 0))
iPhoneX = false;
else
{
if (ObjCRuntime.Runtime.Arch == ObjCRuntime.Arch.DEVICE)
{
using (var context = new LocalAuthentication.LAContext())
{
iPhoneX = context.BiometryType == LABiometryType.TypeFaceId;
}
}
else
iPhoneX = UIScreen.MainScreen.PreferredMode.Size.Height == 2436;
}
}
return (bool)iPhoneX;
}
}
You should simply use the Safe Area Layout Guide which will automatically increase the top/bottom margin on the iPhone X.
As others have pointed out you should definitely be using the Safe Area Layout Guide.
If you really really have a need to detect the model then look for the SIMULATOR_MODEL_IDENTIFIER environment variable. iPhone X will return iPhone10,3.
let model = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]

In Swift, how to get the device orientation correctly right after it's launched?

I use following code to see if the device is in landscape mode or not:
UIDevice.currentDevice().orientation.isLandscape.boolValue
It works BUT if I put my device in landscape mode before the app is launched, and after viewDidLoad, I call this line of code, it always returns false.
If I use this instead:
interfaceOrientation.isLandscape
it returns true, which is correct, but the compiler is showing a warning that interfaceOrientation was deprecated in iOS 8.0.
What is the correct way to get the device orientation right after the app is launched?
DeviceOrientation vs. ScreenSize vs StatusBar.isLandscape?
iOS 11, Swift 4 and Xcode 9.X
Regardless of using AutoLayout or not, there are several ways to get the right device orientation, and they could be used to detect rotation changes while using the app, as well as getting the right orientation at app launch or after resuming from background.
This solutions work fine in iOS 11 and Xcode 9.X
1. UIScreen.main.bounds.size:
If you only want to know if the app is in landscape or portrait mode, the best point to start is in viewDidLoad in the rootViewController at launch time and in viewWillTransition(toSize:) in the rootViewController if you want to detect rotation changes while the app is in background, and should resume the UI in the right orientation.
let size = UIScreen.main.bounds.size
if size.width < size.height {
print("Portrait: \(size.width) X \(size.height)")
} else {
print("Landscape: \(size.width) X \(size.height)")
}
This also happens early during the app/viewController life cycles.
2. NotificationCenter
If you need to get the actual device orientation (including faceDown, faceUp, etc). you want to add an observer as follows (even if you do it in the application:didFinishLaunchingWithOptions method in the AppDelegate, the first notifications will likely be triggered after the viewDidLoad is executed
device = UIDevice.current
device?.beginGeneratingDeviceOrientationNotifications()
notificationCenter = NotificationCenter.default
notificationCenter?.addObserver(self, selector: #selector(deviceOrientationChanged),
name: Notification.Name("UIDeviceOrientationDidChangeNotification"),
object: nil)
And add the selector as follows. I split it in 2 parts to be able to run inspectDeviceOrientation() in viewWillTransition
#objc func deviceOrientationChanged() {
print("Orientation changed")
inspectDeviceOrientation()
}
func inspectDeviceOrientation() {
let orientation = UIDevice.current.orientation
switch UIDevice.current.orientation {
case .portrait:
print("portrait")
case .landscapeLeft:
print("landscapeLeft")
case .landscapeRight:
print("landscapeRight")
case .portraitUpsideDown:
print("portraitUpsideDown")
case .faceUp:
print("faceUp")
case .faceDown:
print("faceDown")
default: // .unknown
print("unknown")
}
if orientation.isPortrait { print("isPortrait") }
if orientation.isLandscape { print("isLandscape") }
if orientation.isFlat { print("isFlat") }
}
Note that the UIDeviceOrientationDidChangeNotification may be posted several times during launch, and in some cases it may be .unknown. What I have seen is that the first correct orientation notification is received after the viewDidLoad and viewWillAppear methods, and right before viewDidAppear, or even applicationDidBecomeActive
The orientation object will give you all 7 possible scenarios(from the enum UIDeviceOrientation definition):
public enum UIDeviceOrientation : Int {
case unknown
case portrait // Device oriented vertically, home button on the bottom
case portraitUpsideDown // Device oriented vertically, home button on the top
case landscapeLeft // Device oriented horizontally, home button on the right
case landscapeRight // Device oriented horizontally, home button on the left
case faceUp // Device oriented flat, face up
case faceDown // Device oriented flat, face down
}
Interestingly, the isPortrait read-only Bool variable is defined in an extension to UIDeviceOrientation as follows:
extension UIDeviceOrientation {
public var isLandscape: Bool { get }
public var isPortrait: Bool { get }
public var isFlat: Bool { get }
public var isValidInterfaceOrientation: Bool { get }
}
3. StatusBarOrientation
UIApplication.shared.statusBarOrientation.isLandscape
This also works fine to determine if orientation is portrait or landscape orientation and gives the same results as point 1. You can evaluate it in viewDidLoad (for App launch) and in viewWillTransition(toSize:) if coming from Background. But it won't give you the details of top/bottom, left/right, up/down you get with the notifications (Point 2)
This worked for me:
if UIScreen.main.bounds.width > UIScreen.main.bounds.height{
print("Portraitmode!")
}
It works on all devices based on the display dimensions:
https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html
I have tested many times about orientation, so I have summed up some experience.
In all iPhone devices, except iPhone6(s) plus, the only interface orientation is .portrait. If App is launched in landscape mode, there must be a change of orientation. One will receive the UIDeviceOrientationDidChangeNotification. It's an appropriate time to get the orientation.
Regarding the launching when in landscape with iPhone6, the orientation after the launch will change once:
The launching when in landscape with iPhone6 plus, after launch the orientation never changed:
Two different screenshot with the same app,
So before the app does change orientation, the orientation is still like in the home page.
In viewDidLoad, the orientation has not changed yet, the log will be the wrong direction.
The isValidInterfaceOrientation should be detected before checking the orientation isLandscape. Don't process the flat message with isValidInterfaceOrientation == false (when it has any value of isLandscape).
I had a hazzel with this until I read the topic more carefully. With consideration of the isValidInterfaceOrientation it works fine.
#objc func rotated() {
if (UIDevice.current.orientation.isValidInterfaceOrientation) {
if (UIDevice.current.orientation.isLandscape) {
if(!bLandscape) {
bLandscape = true
setupTabBar() // Repaint the app
}
} else { // Portait
if(bLandscape) {
bLandscape = false
setupTabBar() // Repaint the app
}
}
}
}
I had a problem to detect which orientation was before isFlat so I put this in my view controller
let orientation = UIDevice.current.orientation
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if orientation.isPortrait {
return .portrait
} else if orientation.isFlat{
if UIScreen.main.bounds.width < UIScreen.main.bounds.height{
return .portrait
} else {
return .landscape
}
} else {
return .landscape
}
}

supportedInterfaceOrientations called on all iPhones except 6+

On my iPhone app I have it restricted to portrait only under the project targets deployment info
There is one page that I want only in landscape and I use the supportedInterfaceOrientations method to obtain that.
Standard implementation:
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
It works perfectly on all iPhone devices and iOS version except for iPhone 6+. The supportedInterfaceOrientations method is never called.
I cant find any reason why this might be affecting just the iPhone 6+, any tips would be greatly apreciated.
you can try this code it works for me
// you have to import foundation
import Foundation
class YourViewController : UIViewController {
var device = self.platform()
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
if device.lowercaseString.rangeOfString("iphone6plus") != nil {
supportedInterfaceOrientations()
}
}
// add this method to your view controller
func platform() -> String {
var sysinfo = utsname()
uname(&sysinfo) // ignore return value
return NSString(bytes: &sysinfo.machine, length: Int(_SYS_NAMELEN), encoding:
NSASCIIStringEncoding)! as String
}
Please note that this will not run on the simulator but will run perfectly on the actual device.
Looking at this question, try this code snippet:
- (NSUInteger) supportedInterfaceOrientations
{
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
{
// iPhone 5S and below: 320x480
// iPhone 6: 375x667
// iPhone 6 Plus: 414x736
CGSize screenSize = [UIScreen mainScreen].bounds.size;
// The way how UIScreen reports its bounds has changed in iOS 8.
// Using MIN() and MAX() makes this code work for all iOS versions.
CGFloat smallerDimension = MIN(screenSize.width, screenSize.height);
CGFloat largerDimension = MAX(screenSize.width, screenSize.height);
if (smallerDimension >= 400 && largerDimension >= 700)
return UIInterfaceOrientationMask.Landscape;
else
return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown);
}
else
{
// Don't need to examine screen dimensions on iPad
return UIInterfaceOrientationMask.Landscape;
}
}
-Herzbube
Lacking an official Apple API, this is the workaround that I've come up with:
[Code]
The snippet simply assumes that a screen with dimensions above a semi-arbitrarily chosen size is suitable for rotation. Semi-arbitrarily, because a threshold of 400x700 includes the iPhone 6 Plus, but excludes the iPhone 6.
Although this solution is rather simple, I like it exactly because of its lack of sophistication. I don't really need to distinguish exactly between devices, so any clever solutions such as the one in Jef's answer are overkill for my purposes.
All I did was change the 1st and 3rd return values from UIInterfaceOrientationMaskAll to UIInterfaceOrientationMask.Landscape.

Resources