Adapting layout for various iPhone and iPad screens using constraints and multipliers

My experience with autolayout tells me that it's very difficult to make app look good on whole variety of devices (from iPhone 4s to 6s especially). Size classes are useful with device position and distinguishing between iPhone and iPad.

To make my app look right on each iPhone size, I connected contraints to my viewController and introduce certain multipliers for each size

IBOutlets:

class BodyViewController: UIViewController {

@IBOutlet var mainView: UIView!
@IBOutlet weak var boyImageCenterConstraint: NSLayoutConstraint!
@IBOutlet weak var femaleMaleSegmentedControl: UISegmentedControl!
@IBOutlet weak var boyImage: UIImageView!
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var buttonHeight: NSLayoutConstraint!
@IBOutlet weak var infoLabel: UILabel!
@IBOutlet weak var organLabel: UILabel!
@IBOutlet weak var heightConstraint: NSLayoutConstraint!
@IBOutlet weak var darkBackgroundWithButtons: UIView!

@IBOutlet weak var buttonToBottom: NSLayoutConstraint!

@IBOutlet weak var buttonToTop: NSLayoutConstraint!
...


I have a method to adjust them accordingly:

 func adjustViewDesignBasedOnDevice()
{
let baseMultiplier:CGFloat = 160.0
var multiplier:CGFloat = 0.0
let recognizer = DeviceRecognizer()
let device = recognizer.determineHeightOfDarkBackgroundWithButtons(UIScreen.mainScreen().bounds.height)
switch device
{
case .ip4:
multiplier = 120.0
case .ip5:
multiplier = 140.0
case .ip6:
multiplier = 160.0
case .ip6p:
multiplier = 170.0
multiplier = 220.0
default:
multiplier = 250.0

}

self.infoLabel.font =  infoLabel.font.fontWithSize(13.0 * (multiplier / baseMultiplier))
self.organLabel.font =  organLabel.font.fontWithSize(19.0 * (multiplier / baseMultiplier))
let indexHeight = self.darkBackgroundWithButtons.constraints.indexOf(heightConstraint)
let indexButtonToBottom = self.darkBackgroundWithButtons.constraints.indexOf(buttonToBottom)
let indexButtonToTop = self.darkBackgroundWithButtons.constraints.indexOf(buttonToTop)
let indexButton = self.nextButton.constraints.indexOf(buttonHeight)
self.darkBackgroundWithButtons.constraints[indexHeight!].constant = multiplier
self.darkBackgroundWithButtons.constraints[indexButtonToTop!].constant = self.darkBackgroundWithButtons.constraints[indexButtonToTop!].constant * (multiplier / baseMultiplier)
self.darkBackgroundWithButtons.constraints[indexButtonToBottom!].constant = self.darkBackgroundWithButtons.constraints[indexButtonToTop!].constant
self.nextButton.constraints[indexButton!].constant = self.nextButton.constraints[indexButton!].constant * (multiplier / baseMultiplier)

}


This way I achieved the right layout on each device. I tried very hard with auto-layout before I came up with this approach, but failed to make things look good mainly because different width/height ratios in each device size.

Is my approach professional? It it common practice to solve layout issues in similar way?

EDIT:

Until now, I used to use auto-layout for whole iPhone family. Since it's relatively easy to make your layout responsive across iP5 to iP6+, it's very difficult to cover iP4 cause it's height/width ratio is completely different.

As I was told in comments below, I'll add screenshot and elaborate on problem:

Segmented control and girl image are done using autolayout entirely. It is perfectly sufficient solution for them.

However, the issues surfaced with this whole stuff below (dark background with two UILabels, UIButton and colorful strip UIImage)

Using mere autolayout haven't allowed me to adjust the font size correctly(Autoshrinking is a bad idea since labels can be longer, it would spoil symmetry)

Next, I need all sizes to be smaller accordingly, including button sizes, button position, labels position and finally entire dark background size and ratio to change.

Which part of my approach is bad? Detecting device, editing NSLayoutConstraints straight from code, or everything?:)

How senior iOS Devs make their app responsive? I can hardly imagine using autolayout with size classes only

iPhone 4s

iPhone 6

• Is it common practice to solve layout issues in a similar way? Speaking professionally... absolutely not. However, it's hard to show you the correct way without a complete description of the layout you expect to see across all devices. Can you update this question with some information describing exactly how the layout should look? Major bonus points for screenshots. – nhgrif Mar 30 '16 at 0:06
• In your code, you are modifying a few properties. infoLabel, organLabel, darkBackgroundWithButtons, and nextButton. Have I missed any that you're not managing to set up with autolayout? And can you help me understand which property corresponds to each UI element? By my guess, the info label is the one starting with the word "Dotknij" and the organ label is the one with "GŁOWA"? The next button is the button at the bottom with the check mark? What area specifically is the darkBackgroundWithButtons? And can you elaborate on the rules? – nhgrif Mar 30 '16 at 12:00
• everything you guessed is right:) the darkBackgroundWithButtons is this dark, opaque area behind the two labels and button. Rules are as follows: font sizes, spaces and heights should change depending on device size – DCDC Mar 30 '16 at 12:04
• Right, but... do you have specific layout rules here? Or does it just need to look good? – nhgrif Mar 30 '16 at 12:06
• the latter :) Maybe I'll upload a few screenshots from different devices to clear up how I understand "looking good" in this app:) – DCDC Mar 30 '16 at 12:07

A few notes:

• You already have IBOutlet references to different constraints, such as buttonToBottom and buttonHeight. In your code, you are getting the index of those constraints, and then using the index to retrieve them from constraints array to modify their constants. However, you can / should just be modifying their constants directly on the ivar, e.g. instead of:

let indexButtonToBottom = self.darkBackgroundWithButtons.constraints.indexOf(buttonToBottom)
let indexButtonToTop = self.darkBackgroundWithButtons.constraints.indexOf(buttonToTop)
self.darkBackgroundWithButtons.constraints[indexButtonToBottom!].constant = self.darkBackgroundWithButtons.constraints[indexButtonToTop!].constant


You can just write:

buttonToBottom.constant =  buttonToTop.constant

• Regarding whether it is common to do complex layouts entirely with auto layout and size classes, in my experience it is. The most common exceptions are: handling differences in layout between iPad portrait and iPad landscape (which have the same size class), handling font sizes (as you have seen), and conditions or user interactions which require a change to constraint constants, which are then modified in code.

• I don't see anything in your layout (other than fonts) which seem to require custom code or multipliers. Keep in mind that for proportions you can always use aspect ratio constraints: for example, the dark section at the bottom that contains 2 labels, a color strip and a button could have a height that is always proportional to its width (and a width equal to the available screen width of the current device), and each item inside can be vertically placed proportionally to the overall height of the container. You would do this my creating a constraint like "Label Two's bottom edge should be constrained to the bottom edge of its superview with a multiplier of 0.28".

Also keep in mind that you can have multiple constraints with different priorities, like "item 1 should have the same height as item 2 with a priority of 999" "item 1 height should always be less than or equal to 500 with a priority of 1000". In this example, item 1 will match the height of item 2 in all cases except when item 2 is taller than 500, in which case item 1 will cap its own height at 500.

• Although I'm not sure why autoshrinking is not appropriate here, as long as you set a minimum font size as well as a starting font size, it often does make sense for fonts to be calculated, not based on the current device, but based on the size and / or aspect ratio of the window your app is running in e.g. view.window?.bounds (remember split screen on iPad!). I would suggest breaking out that code entirely into a class or helper function that exposes something like "bodyFontForCurrentDisplay" and "titleFontForCurrentDisplay". Then this and other view controllers can retrieve the right font size and set it in code with a single statement. If you are careful not to constrain your label widths and heights, they will correctly size and adjust the layout to compensate for whatever font size they are given.