paint-brush
Get the Same UI for All iPhone Screens Using NSLayoutConstraint Scalingby@adobysh
314 reads
314 reads

Get the Same UI for All iPhone Screens Using NSLayoutConstraint Scaling

by Andrei DobyshApril 16th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Often the UI of iOS applications is only designed for screens of one size. For example, iPhone X (375x812) or iPhone 13 (390x844), and then iOS developers make designs for screens of other sizes. With large screens, everything is simple. But placing the design of the iPhone X on the screen of the iPhone SE 2016 (1st gen.) is more difficult, some elements and indents have to be reduced manually.
featured image - Get the Same UI for All iPhone Screens Using NSLayoutConstraint Scaling
Andrei Dobysh HackerNoon profile picture


Often the UI of iOS applications is only designed for screens of one size. For example, iPhone X (375x812) or iPhone 13 (390x844), and then iOS developers make designs for screens of other sizes.


With large screens, everything is simple. But placing the design of the iPhone X on the screen of the iPhone SE 2016 (1st gen.) is more difficult, some elements and indents have to be reduced manually.

Screen design example for iPhone 13:

🥲 How it looks on different screens:

🙂 Result with UI scale:

How to scale - Simple implementation

We go through the UIView hierarchy and scale (almost) all the constraints.


extension UIView {
    
  func callRecursively(level: Int = 0, _ body: (_ subview: UIView, _ level: Int) -> Void) {
        body(self, level)
    subviews.forEach { $0.callRecursively(level: level + 1, body) }
  }
    
  func scale() {
    // 390 is an iPhone 13 screen width
    var scaleFactor = min(UIScreen.main.bounds.width / 390, 1)
    
    callRecursively { (subview, level) in
 
      // to avoid scalе of system constraints
      guard level != 0 else { return }
      
      for constraint in subview.constraints {
        if constraint.constant != 0 {
          constraint.constant = CGFloat(constraint.constant) * scaleFactor
        }
      }
    }
  }
}


class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()

    // one line is enough
    view.scale()
  }
}


And now let's make it a little more difficult for non-stretched screens


func scale(kFor5: CGFloat? = nil, kFor8: CGFloat? = nil) {
  // 390 is an iPhone 13 screen width
  // 375 is for iPhone Х
  // and 320 is for iPhone SE 2016 (1st gen.)
  var scaleFactor = min(UIScreen.main.bounds.width / 390, 1)
  if let kFor5 = kFor5,
      UIScreen.main.bounds.width <= 320 {
      scaleFactor = scaleFactor * kFor5
  } else if let kFor8 = kFor8,
              UIScreen.main.bounds.width <= 375,
              UIScreen.main.bounds.height <= 667 {
    scaleFactor = scaleFactor * kFor8
  }
    
  callRecursively { (subview, level) in
    // to avoid scalе of system constraints
    guard level != 0 else { return }
    if let label = subview as? UILabel {
      label.scaleFont(scaleFactor)
    } else if let button = subview as? UIButton {
      button.scaleFont(scaleFactor)
    }
      
    for constraint in subview.constraints {
      if constraint.constant != 0 {
        constraint.constant = CGFloat(constraint.constant) * scaleFactor
      }
    }
  }
}


extension UILabel {
    
  func scaleFont(_ k: CGFloat) {
    let fontSize = font.pointSize
    font = font.withSize(fontSize * k)
  }   
}


extension UIButton {
  
  func scaleFont(_ k: CGFloat) {
    let fontSize: Double = Double(titleLabel?.font.pointSize ?? 0.0)
    titleLabel?.font = titleLabel?.font.withSize(fontSize * k)
  }
}


class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()

    // you can play with those factors for best results
    view.scale(kFor5: 0.75, kFor8: 0.85)
  }
}


Thank you for reading!

That's all. Please write if you have any comments, I'm almost sure it can be improved further. Also, please let me know if you have solved a similar problem.