Get the Same UI for All iPhone Screens Using NSLayoutConstraint Scaling

Written by adobysh | Published 2022/04/16
Tech Story Tags: ios | uikit | nslayoutconstraint | iphone-app-development | software-development | ui | ui-ux | iphone-ui

TLDROften 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.via the TL;DR App

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.


Written by adobysh | iOS/SwiftUI engineer
Published by HackerNoon on 2022/04/16