Optimizing build time for an iOS project

Hero image for Optimizing build time for an iOS project

This document presents in detail some of the best practices when writing code in a way that can help the team improve build time drastically on iOS. It includes:

  • Environment used and test code for performing the build time statistic recording.
  • Dos and don’ts on several implementation points in iOS development with actual build time results.
  • Good resources and tools for later reference.

Some code that performs well in build time doesn’t necessarily provide a good run time.

Environment Specifications

All the build time performance recordings are performed on the following environment specifications:

  • Laptop: MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports).

    Processor: 2 GHz Quad-Core Intel Core i5.

    Memory: 16 GB 3733 MHz LPDDR4X.

    Graphic: Intel Iris Plus Graphics 1536 MB.

  • IDE: Xcode Version 13.4.1 (13F100).

Best Practices

Here are some of the common implementation points that iOS developers usually run into and they often choose to write code in a shorter way. Sometimes, shorter code poses some adverse effects on the build time that can be avoided with the following practices:

Complex Operations

  • Separate complex operations into a new variable and define the value type for array/dictionary item explicitly.

Example - mapping array with calculation

  // Cumulative build time: 79.1 ms
  indexedChartViewModel.update(
    withXYs: values.enumerated().map { (Double($1.x * 7.0), $0) },
    updatedAt: updatedAt,
    animated: animated
  )
  // Cumulative build time: 8.0 ms
  let xyPoints = values.enumerated().map { (Double($1.x * 7.0), $0) }
  indexedChartViewModel.update(
    withXYs: xyPoints,
    updatedAt: updatedAt,
    animated: animated
  )

Example - calculating numbers

  // Cumulative build time: 41.5 ms
  NSLayoutConstraint
    .activate([
    imageView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 0.0),
    imageView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 4.0),
    imageView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor, constant: 4.0),
    imageView.heightAnchor.constraint(equalToConstant: floor((parentView.bounds.width - 6.0) * 282.0 / 343.0))
  ])
  // Cumulative build time: 3.5 ms
  let ratio: CGFloat = 282.0 / 343.0
  let imageWidth: CGFloat = parentView.bounds.width - 6.0
  let calculatedHeight: CGFloat = floor(imageWidth * ratio)
  NSLayoutConstraint
    .activate([
    imageView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 0.0),
    imageView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 4.0),
    imageView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor, constant: 4.0),
    imageView.heightAnchor.constraint(equalToConstant: calculatedHeight)
  ])

Example - handling condition

  // Cumulative build time: 13.0 ms
  let activities = activity.activities ?? []
  activities.forEach { storeUserActivity(with: $0) }
  if !activities.contains(where: { $0 == .dolfinWalletCoachMark }) {
    userDefault.removeObject(forKey: UserActivity.ActivityType.dolfinWalletCoachMark.rawValue)
  }
  if !activities.contains(where: { $0 == .dolfinWalletOnboarding }) {
    userDefault.removeObject(forKey: UserActivity.ActivityType.dolfinWalletOnboarding.rawValue)
  }
  // Cumulative build time: 8.8 ms
  let activities: [UserActivity.ActivityType] = activity.activities ?? []
  var isContainsDolfinWalletCoachMark = false
  var isContainsDolfinWalletOnboarding = false
  for activity in activities {
    storeUserActivity(with: activity)
    if activity == .dolfinWalletCoachMark { isContainsDolfinWalletCoachMark = true }
    if activity == .dolfinWalletOnboarding { isContainsDolfinWalletOnboarding = true }
  }
  if !isContainsDolfinWalletCoachMark { userDefault.removeObject(forKey: UserActivity.ActivityType.dolfinWalletCoachMark.rawValue) }
  if !isContainsDolfinWalletOnboarding { userDefault.removeObject(forKey: UserActivity.ActivityType.dolfinWalletOnboarding.rawValue) }

String Addition

  • Use String interpolation instead of concatenation when adding new texts to an existing string.
  // Cumulative build time: 44.9 ms
  imageView.image = UIImage(named: path + "/Normal")
  // Cumulative build time: 4.4 ms
  imageView.image = UIImage(named: "\(path)/Normal")

Array Item Addition

  • Prefer to use array concatenation instead of appending when adding a new item to an existing array.
  // Cumulative build time: 60.9 ms
  let nameIndexes = [1, 2, 3, 4, 5]
  let count = nameIndexes.count - 1
  var names = nameIndexes.map { String(format: arrayNameFormat, $0) }
  names.append(NSLocalizedString("everything", comment: ""))
  arrayNames = Array(names[0..<count])
  if let lastName = names.last {
    arrayNames.append(lastName)
  }
  // Cumulative build time: 3.1 ms
  let nameIndexes = [1, 2, 3, 4, 5]
  let count = nameIndexes.count - 1
  let names = nameIndexes.map { String(format: arrayNameFormat, $0) } + [NSLocalizedString("everything", comment: "")]
  if let lastName = names.last {
    arrayNames = Array(names[0..<count]) + [lastName]
  }

Nil Coalescing Operator

  • Unwrap the variables or views instead of using optional chaining for lengthy operations.
  // Cumulative build time: 127.3 ms
  return CGSize(width: bounds.size.width, height: bounds.size.height + (topView?.bounds.size.height ?? 0) + (bottomView?.bounds.size.height ?? 0) + 10)
  // Cumulative build time: 3.9 ms
  var verticalPadding: CGFloat = 10
  if let topView = topView {
    verticalPadding += topView.bounds.size.width
  }
  if let bottomView = bottomView {
    verticalPadding += bottomView.bounds.size.width
  }
  return CGSize(width: bounds.size.width, height: bounds.size.height + verticalPadding)

Ternary Operator

  • Prefer to use if else block instead of ternary operator, especially for complex assignments or operations with nested calculations.
  // Cumulative build time: 27.8 ms
  systemNames = systemType == 0 ? (1...9).map { String(format: systemType0StringFormat, $0) } : (2...9).map { String(format: systemType1StringFormat, $0) }
  // Cumulative build time: 2.7 ms
  if systemType == 0 {
    systemNames = (1...9).map { String(format: systemType0StringFormat, $0) }
  } else {
    systemNames = (2...9).map { String(format: systemType1StringFormat, $0) }
  }

Casting CGFloat

  • Avoid redundant CGFloat casting whenever possible.
  // Cumulative build time: 107.6 ms
  return CGFloat(Double.pi) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180
  // Cumulative build time: 19.1 ms
  return CGFloat(Double.pi) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180

Round() Function

  • Only use Round() function when it is really needed.
  // Cumulative build time: 41.9 ms
  let finalValue = tValue - wValue - xValue + round(yValue * 0.66) + zValue
  // Cumulative build time: 2.6 ms
  let finalValue = tValue - wValue - xValue + (yValue * 0.66) + zValue

Build Time Recordings Report

Here is an actual report on the accumulative build time for all the listed cases above:

All build time recordings report

There is also a repository setup with actual code for the above examples. Please don’t hesitate to give it a try to build and follow the instructions from the Build Time Analyzer App to see the generated reports. 💪

Running different build rounds can yield different time results per round. Therefore, developers might find the build time report has slightly different results than the attached report above, which is just a snapshot of the time these practices are written. Ultimately, the overall build time report pattern should remain consistent across the builds. 🚀

Resources

These are all the dedicated resources used and referred from to come up with the above practices: