This is a second part of a cycle of articles about improving the iOS app’s performance, please read the first part ==here.==
Why AutoLayout might be slowing down your app and how to fix itAutoLayout is the standard way to position and size UI elements in iOS apps. It’s flexible, powerful, and great for handling different screen sizes. But here’s the catch: it’s not always the most efficient. When your interface has dynamically adjusting elements (like labels resizing based on content) AutoLayout can introduce noticeable performance issues, especially on older devices.
\ At first, the slowdown might be subtle. A few elements? Not a big deal. But as your UI gets more complex, performance takes a hit. Scrolling stutters. Animations lag. The smoothness you expect? Gone.
\ So, what’s the alternative? Manual frame layout. Instead of relying on AutoLayout, you calculate and set frames explicitly. Yes, it means writing more code, but the tradeoff is speed. The UI feels snappier, and performance improves significantly.
\ Let’s take a common example: a weather forecast cell. Instead of stacking AutoLayout constraints, you manually position elements using frame values. This eliminates the overhead of AutoLayout’s constraint-solving process, making UI updates much faster.
\ But keep in mind: manual layouts are rigid. If your cell size or font changes, those fixed values won’t adjust automatically. You need to handle updates yourself.
\ And what about the old-school Autoresizing Mask in Interface Builder? Forget it. It was useful before AutoLayout, but now it just converts into constraints behind the scenes.
\ Before we layout, disable the creation of automatic constraints for all elements.
\ Let's say we want to fix the size of a MessageCell:
@IBOutlet weak var messageLabel: UILabel! { didSet { messageLabel.translatesAutoresizingMaskIntoConstraints = false } } @IBOutlet weak var timestampLabel: UILabel! { didSet { timestampLabel.translatesAutoresizingMaskIntoConstraints = false } } @IBOutlet weak var avatarImageView: UIImageView! { didSet { avatarImageView.translatesAutoresizingMaskIntoConstraints = false } }\ Next, we override the layoutSubviews method, where all child elements of UIView—including labels and images—are positioned. The essence of manual layout is to independently calculate each element’s size and coordinates, then assign them using the frame property.
\ Let’s add a property to define padding. It can be the same for all sides (e.g., 10 points) or different for finer control over spacing. This will help ensure proper alignment of the message text, timestamp, and avatar image within the MessageCell.
let insets: CGFloat = 10.0\ Calculate the text size in UILabel:
func getLabelSize(text: String, font: UIFont) -> CGSize { // Define the maximum width of the text - this is the width of the cell minus left and right padding let maxWidth = bounds.width - insets * 2 // Get the dimensions of the block for the label let textBlock = CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude) // Get a rectangle for the text in this block and specify the font let rect = text.boundingRect( with: textBlock, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil ) // Get the width and height of the block, rounding up to the nearest integer let width = ceil(rect.size.width) let height = ceil(rect.size.height) return CGSize(width: width, height: height) }\ In this method, we pass the dimensions of the rectangle where the text will be rendered. The width is constrained, but the height remains unlimited, allowing the text to expand as needed.
\ We then use the boundingRect method to determine the actual dimensions of the text within this block. If the text exceeds the allowed width, it will automatically wrap to the next line. The font also plays a crucial role in determining the final size.
\ To ensure precise rendering, we round the width and height up to the nearest whole number. This helps iOS efficiently convert the dimensions into pixels.
\ As a result, we obtain the exact size of the rectangle needed to fit the text when displayed in the specified font. By setting the UILabel’s frame to this size, we ensure that the entire message is visible without clipping or unnecessary extra space.
\ The next step is positioning the UILabel within the MessageCell, ensuring proper alignment alongside the avatar image and timestamp.
func messageLabelFrame() { // Get the text size by passing the text and font guard let text = messageLabel.text, !text.isEmpty else { return } let messageLabelSize = getLabelSize(text: text, font: messageLabel.font) // Calculate the X coordinate (centered horizontally) let messageLabelX = (bounds.width - messageLabelSize.width) / 2 // Get the top-left corner point of the label let messageLabelOrigin = CGPoint(x: messageLabelX, y: insets) // Set the frame for UILabel messageLabel.frame = CGRect(origin: messageLabelOrigin, size: messageLabelSize) }\ We calculate the message label size, center it horizontally, and position it using a CGRect based on the top-left offset. Assigning this frame ensures correct placement.
\ The timestamp label follows the same logic, but its Y coordinate is adjusted for proper alignment, usually in the top-right or bottom-right corner of the cell.
func timestampLabelFrame() { // Ensure text exists to avoid force unwrapping guard let text = timestampLabel.text, !text.isEmpty else { return } // Get the text size let timestampLabelSize = getLabelSize(text: text, font: timestampLabel.font) // Calculate the X coordinate (align to the right with padding) let timestampLabelX = bounds.width - timestampLabelSize.width - insets // Calculate the Y coordinate (align to the bottom with padding) let timestampLabelY = bounds.height - timestampLabelSize.height - insets // Set the frame for UILabel timestampLabel.frame = CGRect(origin: CGPoint(x: timestampLabelX, y: timestampLabelY), size: timestampLabelSize) }\ The last step is laying out the avatar image. Since its size is constant (e.g., 50 points), we simply position it without needing to calculate its dimensions dynamically. Typically, the avatar is aligned to the left side of the cell with some padding.
func avatarImageFrame() { let avatarSize: CGFloat = 50 let avatarSizeDimensions = CGSize(width: avatarSize, height: avatarSize) // Align avatar to the left with padding let avatarOrigin = CGPoint(x: insets, y: bounds.midY - avatarSize / 2) avatarImageView.frame = CGRect(origin: avatarOrigin, size: avatarSizeDimensions) }\ Let's redefine the method for calculating the position of elements.
override func layoutSubviews() { super.layoutSubviews() messageLabelFrame() timestampLabelFrame() avatarImageFrame() }\ To ensure that the message text and timestamp update dynamically, we need to add setter methods that update the text and trigger a layout recalculation. Here’s how we do it:
func setMessage(text: String) { messageLabel.text = text messageLabelFrame() // Recalculate position } func setTimestamp(text: String) { timestampLabel.text = text timestampLabelFrame() // Recalculate position }\ In these methods, we set the provided text to the corresponding label and then recalculate its frame to ensure proper positioning. The full listing of the class looks like this:
import UIKit class MessageCell: UICollectionViewCell { @IBOutlet weak var messageLabel: UILabel! { didSet { messageLabel.translatesAutoresizingMaskIntoConstraints = false } } @IBOutlet weak var timestampLabel: UILabel! { didSet { timestampLabel.translatesAutoresizingMaskIntoConstraints = false } } @IBOutlet weak var avatarImageView: UIImageView! { didSet { avatarImageView.translatesAutoresizingMaskIntoConstraints = false } } let insets: CGFloat = 10.0 let message func setMessage(text: String) { messageLabel.text = text messageLabelFrame() } func setTimestamp(text: String) { timestampLabel.text = text timestampLabelFrame() } override func layoutSubviews() { super.layoutSubviews() messageLabelFrame() timestampLabelFrame() avatarImageFrame() } func getLabelSize(text: String, font: UIFont) -> CGSize { let maxWidth = bounds.width - insets * 2 let textBlock = CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude) let rect = text.boundingRect(with: textBlock, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) return CGSize(width: ceil(rect.width), height: ceil(rect.height)) } func messageLabelFrame() { guard let text = messageLabel.text, !text.isEmpty else { return } let messageLabelSize = getLabelSize(text: text, font: messageLabel.font) let messageLabelX = avatarImageView.frame.maxX + insets let messageLabelOrigin = CGPoint(x: messageLabelX, y: insets) messageLabel.frame = CGRect(origin: messageLabelOrigin, size: messageLabelSize) } func timestampLabelFrame() { guard let text = timestampLabel.text, !text.isEmpty else { return } let timestampLabelSize = getLabelSize(text: text, font: timestampLabel.font) let timestampLabelX = bounds.width - timestampLabelSize.width - insets let timestampLabelY = bounds.height - timestampLabelSize.height - insets let timestampLabelOrigin = CGPoint(x: timestampLabelX, y: timestampLabelY) timestampLabel.frame = CGRect(origin: timestampLabelOrigin, size: timestampLabelSize) } func avatarImageFrame() { let avatarSize: CGFloat = 50 let avatarX: CGFloat = insets let avatarY: CGFloat = (bounds.height - avatarSize) / 2 avatarImageView.frame = CGRect(x: avatarX, y: avatarY, width: avatarSize, height: avatarSize) } }\ Now you need to set the text in the caption using the methods:
cell.setTimestamp(text: time) cell.setMessage(text: String(someMessage))\
ConclusionBy moving away from AutoLayout and adopting manual frame layout, we eliminate the overhead caused by dynamic constraint calculations. As a result, the collection view operates without AutoLayout, significantly boosting its performance.
\ This optimisation leads to smoother scrolling, faster rendering, and a more responsive UI, especially when dealing with complex or dynamically changing content. While manual layout requires more code and precise handling, the trade-off is well worth it for applications where every millisecond of performance matters.
All Rights Reserved. Copyright , Central Coast Communications, Inc.