I built an iPad game and am now trying to port it to the iPhone. One of my interfaces in the game has buttons that are laid in specific places on top of an image, so I'd like to simply scale what I have built for the iPad down to iPhone size.
Thus far, I have had a little success simply defining my buttons in terms of positions and sizes on the iPad, then simply scaling each button proportionally in code.
//
// ExerciseMenuView.swift
// Reading Expressway
//
// Created by Montana Burr on 7/29/16.
// Copyright © 2016 Montana. All rights reserved.
//
import UIKit
import QuartzCore
#IBDesignable #objc class ExerciseMenuView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
// Adjust subviews according to device screen size.
let iPadProWidth = CGFloat(1024);
let iPadProHeight = CGFloat(768);
let deviceWidth = UIScreen.main.bounds.width;
let deviceHeight = UIScreen.main.bounds.height;
for subview in self.subviews {
let subviewWidthMultiplier = subview.frame.width / iPadProWidth;
let subviewHeightMultiplier = subview.frame.height / iPadProHeight;
let subviewCenterXMultiplier = subview.frame.origin.x / iPadProWidth;
let subviewCenterYMultiplier = subview.frame.origin.y / iPadProHeight;
let subviewHeight = subviewHeightMultiplier * deviceHeight;
let subviewWidth = deviceWidth * subviewWidthMultiplier;
let subviewX = subviewCenterXMultiplier * deviceWidth;
let subviewY = subviewCenterYMultiplier * deviceHeight;
subview.frame = CGRect.init(x: subviewX, y: subviewY, width: subviewWidth, height: subviewHeight)
}
}
}
This is roughly what the screen looks like now, on iPad:
This is what I want it to look like on an iPhone 8:
This is what it actually looks like on an iPhone 8 simulator:
I think layoutSubviews called 2 or more time so your result very small.
You can change frame in init method.
OR add flag for check is this first calling
var isFirstTime = true
override func layoutSubviews() {
super.layoutSubviews()
if isFirstTime {
isFirstTime = false
// Adjust subviews according to device screen size.
...
}
}
I have a scroll view and a subview(which is UIView) and am just wanted to know whether the view is in center of the scrollview. I was just trying to compare the view.center for both but which are not equal. Please let me know if there are any other possible approach.
Try following comparison(Xcode 9.3 with Swift4.1):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let centerSubView = scrollView.convert(subView.center, to: scrollView.superview)
//Convert CGFloat to Int to avoid decimal problem before comparison
if Int(centerSubView.x) == Int(scrollView.center.x) && Int(centerSubView.y) == Int(scrollView.center.y) {
print("equal center")
}
}
In iOS 9, is it possible to detect when an app is running in iOS 9's Slide Over or Split View mode?
I've tried reading through Apple's documentation on iOS 9 multitasking, but haven't had any luck with this…
I ask because I might have a feature in my app that I'd like to disable when the app is opened in a Slide Over.
Just check if your window occupies the whole screen:
BOOL isRunningInFullScreen = CGRectEqualToRect([UIApplication sharedApplication].delegate.window.frame, [UIApplication sharedApplication].delegate.window.screen.bounds);
If this is false, then you're running in a split view or a slide over.
Here is the code snipped which will automatically maintain this flag irrespective of rotation
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
// simply create a property of 'BOOL' type
isRunningInFullScreen = CGRectEqualToRect([UIApplication sharedApplication].delegate.window.frame, [UIApplication sharedApplication].delegate.window.screen.bounds);
}
Just another way to repackage all of this
extension UIApplication {
public var isSplitOrSlideOver: Bool {
guard let w = self.delegate?.window, let window = w else { return false }
return !window.frame.equalTo(window.screen.bounds)
}
}
then you can just
UIApplication.shared.isSplitOrSlideOver in Swift
UIApplication.sharedApplication.isSplitOrSlideOver in Objective-C
Note that, in Swift, the window object is a double optional... WTF!
For iOS 13+ (note, I haven't tested the iOS 13 code myself yet)
extension UIApplication {
public var isSplitOrSlideOver: Bool {
guard let window = self.windows.filter({ $0.isKeyWindow }).first else { return false }
return !(window.frame.width == window.screen.bounds.width)
}
}
I'm late to the party, but if you want a property that works independent of the orientation, try this one:
extension UIApplication
{
func isRunningInFullScreen() -> Bool
{
if let w = self.keyWindow
{
let maxScreenSize = max(UIScreen.mainScreen().bounds.size.width, UIScreen.mainScreen().bounds.size.height)
let minScreenSize = min(UIScreen.mainScreen().bounds.size.width, UIScreen.mainScreen().bounds.size.height)
let maxAppSize = max(w.bounds.size.width, w.bounds.size.height)
let minAppSize = min(w.bounds.size.width, w.bounds.size.height)
return maxScreenSize == maxAppSize && minScreenSize == minAppSize
}
return true
}
}
Like the solution by Dan Rosenstark, but changed to work on the new iPad Pro's that seem to report a different frame and screen.bounds height based on if it's ran directly on the device through Xcode, or if it is compiled and released through TestFlight or App Store. The height would return 980 when through AS or TF, rather than 1024 as it was supposed to like through Xcode causing it to be impossible to return true.
extension UIApplication {
public var isSplitOrSlideOver: Bool {
guard let w = self.delegate?.window, let window = w else { return false }
return !(window.frame.width == window.screen.bounds.width)
}
}
I recently had to determine display style of an application based including, not only if it changed to split view or slide-over, but also what portion of the screen was being utilized for the application (full, 1/3, 1/2, 2/3). Adding this to a ViewController subclass was able to solve the issue.
/// Dismisses this ViewController with animation from a modal state.
func dismissFormSheet () {
dismissViewControllerAnimated(true, completion: nil)
}
private func deviceOrientation () -> UIDeviceOrientation {
return UIDevice.currentDevice().orientation
}
private func getScreenSize () -> (description:String, size:CGRect) {
let size = UIScreen.mainScreen().bounds
let str = "SCREEN SIZE:\nwidth: \(size.width)\nheight: \(size.height)"
return (str, size)
}
private func getApplicationSize () -> (description:String, size:CGRect) {
let size = UIApplication.sharedApplication().windows[0].bounds
let str = "\n\nAPPLICATION SIZE:\nwidth: \(size.width)\nheight: \(size.height)"
return (str, size)
}
func respondToSizeChange (layoutStyle:LayoutStyle) {
// Respond accordingly to the change in size.
}
enum LayoutStyle: String {
case iPadFullscreen = "iPad Full Screen"
case iPadHalfScreen = "iPad 1/2 Screen"
case iPadTwoThirdScreeen = "iPad 2/3 Screen"
case iPadOneThirdScreen = "iPad 1/3 Screen"
case iPhoneFullScreen = "iPhone"
}
private func determineLayout () -> LayoutStyle {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .iPhoneFullScreen
}
let screenSize = getScreenSize().size
let appSize = getApplicationSize().size
let screenWidth = screenSize.width
let appWidth = appSize.width
if screenSize == appSize {
return .iPadFullscreen
}
// Set a range in case there is some mathematical inconsistency or other outside influence that results in the application width being less than exactly 1/3, 1/2 or 2/3.
let lowRange = screenWidth - 15
let highRange = screenWidth + 15
if lowRange / 2 <= appWidth && appWidth <= highRange / 2 {
return .iPadHalfScreen
} else if appWidth <= highRange / 3 {
return .iPadOneThirdScreen
} else {
return .iPadTwoThirdScreeen
}
}
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
respondToSizeChange(determineLayout())
}
Here is a simpler and less fragile way with no constants, that I use in an iPhone/iPad iOS app.
This code also distinguishes between slide over and split view.
I'm returning String values here for clarity, feel free to use enum values and to merge the two cases of fullscreen as suits your app.
func windowMode() -> String {
let screenRect = UIScreen.main.bounds
let appRect = UIApplication.shared.windows[0].bounds
if (UIDevice.current.userInterfaceIdiom == .phone) {
return "iPhone fullscreen"
} else if (screenRect == appRect) {
return "iPad fullscreen"
} else if (appRect.size.height < screenRect.size.height) {
return "iPad slide over"
} else {
return "iPad split view"
}
}
You can watch both -willTransitionToTraitCollection:withTransitionCoordinator: for the size class and viewWillTransitionToSize:withTransitionCoordinator: for the CGSize of your view. Hardcoding in size values isn't recommended though.
The horizontal size class will be compact when in slide over or 33% split view. I don't think you can detect once you go to 50% or 66% though.
I made an edit to #Michael Voccola solution which fixed the problem for orientation
I used this way in my situation to detect all iPad split screen state and handling layout
Just call determineLayout() to get current layoutStyle
private func getScreenSize() -> CGRect {
let size = UIScreen.main.bounds
return size
}
private func getApplicationSize() -> CGRect {
let size = UIApplication.shared.windows[0].bounds
return size
}
enum LayoutStyle: String {
case iPadFullscreen = "iPad Full Screen"
case iPadHalfScreen = "iPad 1/2 Screen"
case iPadTwoThirdScreeen = "iPad 2/3 Screen"
case iPadOneThirdScreen = "iPad 1/3 Screen"
case iPhoneFullScreen = "iPhone"
}
func determineLayout() -> LayoutStyle {
if UIDevice.current.userInterfaceIdiom == .phone {
return .iPhoneFullScreen
}
let screenSize = getScreenSize().size
let appSize = getApplicationSize().size
let screenWidth = screenSize.width
let appWidth = appSize.width
if screenSize == appSize {
// full screen
return .iPadFullscreen
}
let persent = CGFloat(appWidth / screenWidth) * 100.0
if persent <= 55.0 && persent >= 45.0 {
// The view persent between 45-55 that's mean it's half screen
return .iPadHalfScreen
} else if persent > 55.0 {
// more than 55% that's mean it's 2/3
return .iPadTwoThirdScreeen
} else {
// less than 45% it's 1/3
return .iPadOneThirdScreen
}
}
extension UIApplication {
func isRunningInFullScreen() -> Bool {
if let w = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?.windows
.filter({$0.isKeyWindow}).first {
let maxScreenSize = max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
let minScreenSize = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
let maxAppSize = max(w.bounds.size.width, w.bounds.size.height)
let minAppSize = min(w.bounds.size.width, w.bounds.size.height)
return maxScreenSize == maxAppSize && minScreenSize == minAppSize
}
return true
}
}
here is the code for those who don't want to see swift lint complaints for deprecated keyWindow
After much 'tinkering', I have found a solution for my App that may work for you:
In AppDelegate.swift, create the following variable:
var slideOverActive: Bool = false
Then, in ALL of your view controllers, add the UIApplicationDelegate to the Class definition, create an appDelegate variable, and then add the below traitCollectionDidChange function:
class myViewController: UIViewController, UIApplicationDelegate {
var appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
let screenWidth = UIScreen.mainScreen().bounds.width
if previousTraitCollection != nil {
let horizontalSizeClass: Int = previousTraitCollection!.horizontalSizeClass.rawValue
if screenWidth == 1024 || screenWidth == 768 { // iPad
if horizontalSizeClass == 2 { // Slide Over is ACTIVE!
appDelegate.slideOverActive = true
} else {
appDelegate.slideOverActive = false
}
}
}
}
}
Then, wherever in your code you wish to check whether the slide-over is active or not, simply check:
if appDelegate.slideOverActive == true {
// DO THIS
} else {
// DO THIS
}
It's a bit of a workaround, but it works for me at the moment.
Happy trails!
Adding to #Tamas's answer:
Here is the code snippet that will automatically maintain this flag irrespective of rotation.
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
// simply create a property of 'BOOL' type
isRunningInFullScreen = CGRectEqualToRect([UIApplication sharedApplication].delegate.window.frame, [UIApplication sharedApplication].delegate.window.screen.bounds);
}
Trying [UIScreen mainScreen].bounds,
self.window.screen.bounds,
self.window.frame,
UIApplication.sharedApplication.keyWindow.frame and so on, the only working solution was deprecated method
CGRect frame = [UIScreen mainScreen].applicationFrame;
Which I fixed this way
CGRect frame = [UIScreen mainScreen].applicationFrame;
frame = CGRectMake(0, 0, frame.size.width + frame.origin.x, frame.size.height + frame.origin.y);
self.window.frame = frame;
And I'm really late to the party! But nonetheless, here's a simple, swifty solution to the problem. Using let width = UIScreen.mainScreen().applicationFrame.size.width we can detect the width of my app's window, and then have things occur when it is smaller than a certain number (i.e. on iPhone screen or in split view), useful to make different things happen on smaller screens. To have the computer check the width over and over again, we can run an NSTimer every hundredth of a second, then do stuff if the width is higher/lower than something.
Some measurements for you (you have to decide what width to make stuff occur above/below):
iPhone 6S Plus: 414.0mm
iPhone 6S: 375.0mm
iPhone 5S: 320.0mm
iPad (portrait): 768.0mm
iPad (1/3 split view): 320.0mm
iPad Air 2 (1/2 split view): 507.0mm
iPad (landscape): 1024.0mm
Here's a code snippet:
class ViewController: UIViewController {
var widthtimer = NSTimer()
func checkwidth() {
var width = UIScreen.mainScreen().applicationFrame.size.width
if width < 507 { // The code inside this if statement will occur if the width is below 507.0mm (on portrait iPhones and in iPad 1/3 split view only). Use the measurements provided in the Stack Overflow answer above to determine at what width to have this occur.
// do the thing that happens in split view
textlabel.hidden = false
} else if width > 506 {
// undo the thing that happens in split view when return to full-screen
textlabel.hidden = true
}
}
override func viewDidAppear(animated: Bool) {
widthtimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "checkwidth", userInfo: nil, repeats: true)
// runs every hundredth of a second to call the checkwidth function, to check the width of the window.
}
override func viewDidDisappear(animated: Bool) {
widthtimer.invalidate()
}
}
I hope this can help anyone who comes peeking!
By using below code you can check splitViewController is Collapsed or Not
if splitViewController?.isCollapsed ?? false {
// splitview collapsed
} else {
// splitview not collapsed
}
How to design iPad Landscape and Portrait screens with different Layouts using Size class.
I could find only w-regular and h-regular for both orientations. Example: I need to align 2 views vertically in portrait and horizontally in landscape using Size Class
Finally I found a solution :
if traitCollection.verticalSizeClass == .Regular && traitCollection.horizontalSizeClass == .Regular {
var orientation:UIInterfaceOrientation = UIApplication.sharedApplication().statusBarOrientation;
if orientation == UIInterfaceOrientation.LandscapeLeft || orientation == UIInterfaceOrientation.LandscapeRight {
// orientation is landscape
} else {
// orientation is portrait
}
}
It appears to be Apple's intent to treat both iPad orientations as the same -- but as a number of us are finding, there are very legitimate design reasons to want to vary the UI layout for iPad Portrait vs. iPad Landscape.
However, please see this answer for another approach to adapting size classes to do what we need:
https://stackoverflow.com/a/28268200/4517929
For Swift 3 it would look like this:
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UI_USER_INTERFACE_IDIOM() == .pad &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
It will use wRhC instead of wRhR for iPad devices in landscape mode.
Put this code to your base view controller, i.e. this rule will work for all controllers that were presented by this one.
You can put any additional conditions here... For example, if you want this rule to be working only for specific view controller, your if operator would look like this:
if UI_USER_INTERFACE_IDIOM() == .pad &&
childViewController is YourSpecificViewController &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
Swift 4
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
I like to create a custom subclass of navigationController and then set a storyboards initial Navigation controller to that class. You can also do something similar with a ViewController.
Example:
import UIKit
class NavigationControllerWithTraitOverride: UINavigationController {
// If you make a navigationController a member of this class the descendentVCs of that navigationController will have their trait collection overridden with compact vertical size class if the user is on an iPad and the device is horizontal.
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
}
Note: You should not override traitCollection as per the docs
Important
Use the traitCollection property directly. Do not override it. Do not provide a custom implementation.
I have a use case that seems to fall into the cracks between asset catalogs and size classes. My Universal app needs a full screen background image that is different in Landscape and Portrait orientations, and Landscape is only supported for the iPad & iPhone 6.
Since I can't add landscape images to the asset catalog for a new image set, my current solution looks like this (supports iOS 7 & 8):
// permit landscape mode only for iPads & iPhone 6 (would prefer to use size classes, but...?)
override func shouldAutorotate() -> Bool {
let size = view.frame.size
let maxv = max(size.width, size.height)
return ((maxv > 700.0) || (maxv == 512.0)) ? true : false
}
// this will be triggered only for iOS 7, as long as viewWillTransitionToSize:withTransitionCoordinator: is also implemented!
override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
adjustToOrientation(toInterfaceOrientation)
}
// this will be triggered only for iOS 8
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
let orientation = UIApplication.sharedApplication().statusBarOrientation
// need to reverse the sense of the orientation here; Application still has the previous orientation
switch orientation {
case .Portrait, .PortraitUpsideDown:
adjustToOrientation(.LandscapeLeft)
case .LandscapeLeft, .LandscapeRight:
adjustToOrientation(.Portrait)
default:
adjustToOrientation(.Portrait)
}
}
func adjustToOrientation(newInterfaceOrientation: UIInterfaceOrientation) {
let size = view.frame.size
// rotation already permitted only for iPad & iPhone 6, but need to know which one (size classes useless here?)
switch (max(size.width, size.height)) {
case 1024.0:
if UIInterfaceOrientationIsLandscape(newInterfaceOrientation) {
backgroundImage.image = UIImage(named: "Background-Landscape#2x~ipad.png")
} else {
backgroundImage.image = UIImage(named: "Background#2x~ipad.png")
}
case 736.0:
if UIInterfaceOrientationIsLandscape(newInterfaceOrientation) {
backgroundImage.image = UIImage(named: "Background-Landscape#3x~iphone.png")
} else {
backgroundImage.image = UIImage(named: "Background#3x~iphone.png")
}
case 512.0:
if UIInterfaceOrientationIsLandscape(newInterfaceOrientation) {
backgroundImage.image = UIImage(named: "Background-Landscape~ipad.png")
} else {
backgroundImage.image = UIImage(named: "Background~ipad.png")
}
default:
break
}
}
This works, but seems fragile. Is there a more correct way to identify the device as something that supports a regular size class in Landscape, before actually being in Landscape mode? Or some way that I've missed to specify the Landscape equivalents for an image set?
You can use size classes in assets catalog or you can have two image sets: one for portrait and one for landspace mode. Then you can access images by asset name. This will reduce your code a little. But I'd recommend to cosider using size classes everywhere it's possible.
So the final solution was:
-Set the Width property on the image asset to Any+Regular and added a couple of Landscape images for the iPhone 6+ and iPad, and UIImage automatically does the rest
-A more elegant way to permit rotation only for those two devices, in both iOS 7 & iOS 8:
override func shouldAutorotate() -> Bool {
let scale = UIScreen.mainScreen().scale
let idiom = UIDevice.currentDevice().userInterfaceIdiom
return ((idiom == .Pad) || (scale > 2.0)) ? true : false
}
So all of the above down to 5 lines of code!