Mastering UIView Geometry: A Dive into UIKit's Foundational Class and Geometry Concepts for iOS

Written by psharanda | Published 2023/12/13
Tech Story Tags: ios | ios-development | uikit | geometry | computer-graphics | ios-app-development | ios-application-development | mobile-app-development

TLDRThis article explains the key aspects of UIView geometry, essential for iOS developers to master. *Provides an interactive tool for exploring UIView properties* Available at https://github.com/psharanda/UIViewGeometry. *Defines and Illustrates key concepts* Frame: Determines the view's location and size in its superview's coordinates. Bounds: Represents the view's size and location within its own coordinate system. Center: The center point of the view's frame, influenced by the anchorPoint. Transform: Allows for affine transformations like scaling, rotation, and translation. AnchorPoint: The reference point for transformations within the view. *Explores Advanced Topics* Frame Computation: Frame is computed by applying transformations to bounds, taking anchorPoint and center into account. AutoLayout: It focuses on calculating bounds and center, excluding frame, transform, and anchorPoint. via the TL;DR App

Introduction

UIView is a foundational class in UIKit and iOS development. Everything we see on the screen is constructed from UIView`s and their subclasses.

One topic often considered basic is the geometry of UIView. It's easy to grasp initially, but complete understanding is often elusive, even for very experienced iOS engineers. In this article, we will dive deeply into the world of UIView geometry. We will explain the main concepts, such as frame, bounds, center, anchorPoint, transform, how they relate to each other, and answer more advanced questions about computing the frame and Autolayout specifics.

Playground

To better understand the geometry, I've also developed a small iOS app: https://github.com/psharanda/UIViewGeometry.

In the app, you can experiment with different UIView properties in real time by adjusting sliders. Such an interactive approach is really helpful for grasping the nuances.

frame

The definition of UIView.frame in Apple documentation is:

The frame rectangle, which describes the view’s location and size in its superview’s coordinate system.

Most of the time, we operate just with frames, either during manual layout or when printing the view's debug description. The designated initializer for UIView also uses the frame as the only param.

This seems simple and straightforward. However, the reality is more complex. The frame is actually a computed property of UIView. It is not the definitive source of the view's geometry. So, what is it exactly?

bounds

Apparently, UIView has many more properties related to geometry, and we’re now going to check the real, non-computed ones. Let’s examine the first of them: bounds.

The definition of UIView.bounds is:

The bounds rectangle, which describes the view’s location and size in its own coordinate system.

Usually, both bounds.origin.x and bounds.origin.y are simply equal to 0.0, and bounds.size is the same as frame.size (however, it is not always true).

What happens if bounds.origin is not equal to .zero? In that case, bounds basically act as a viewport, defining which part of the view coordinate system is visible to the outside world (superview). bounds.origin can also be seen as an additional translation (with a minus sign) for all subviews and view’s content rendered using drawRect.

This might remind you of UIScrollView. It is for a reason: the contentOffset property of a UIScrollView is directly linked to its bounds.origin.

It's important to note that border, background, and shadow are rendered without considering bounds.origin.

Previously, we mentioned that sometimes frame.size cannot be equal to bounds.size. It may happen if transform is not equal to .identity.

According to the documentation, setting frame to any value when transform is not .identity is considered undefined behavior (getting is still fine). However, setting bounds when transform is not .identity is valid. In that case, frame describes the rectangle that fits the view after all transformations in its superview’s coordinate system.

center

The definition of UIView.center:

The center point of the view's frame rectangle in its superview’s coordinate system.

However, the term "center" can be a bit misleading. It only represents the actual center of the view if the view.anchorPoint (more on this in the next section) is positioned at the center, specifically at (0.5, 0.5). A more accurate definition for center would be:

The coordinates of the view’s anchor point in its superview’s coordinate system.

UIView’s center is exactly the same as CALayer.position.

The combination of bounds and center can fully replace the use of frame

transform

The UIView transform property in iOS development is a fundamental concept that enables developers to apply geometric transformations to views.

The transform property of UIView is of the type CGAffineTransform, which represents a matrix used for affine transformations. Affine transformations include:

Translation

To move a view, you set the transform property with CGAffineTransform(translationX:y:). For instance, view.transform = CGAffineTransform(translationX: 50, y: 75) moves the view 50 points to the right and 75 points down.

Scaling

To scale a view, use CGAffineTransform(scaleX:y:). For example, view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) halves the size of the view in both width and height.

Rotation

To rotate a view, apply CGAffineTransform(rotationAngle:). The angle is in radians, so to rotate 30 degrees, you would use view.transform = CGAffineTransform(rotationAngle: .pi / 6).

Combining Transforms

One of the powerful aspects of the transform property is the ability to combine transformations. You can combine scaling, rotation, and translation into a single transform using concatenation, for example:

var t = CGAffineTransform.identity // 0
t = t.concatenating(CGAffineTransform(scaleX: 0.5, y: 0.5)) //1
t = t.concatenating(CGAffineTransform(rotationAngle: .pi / 6)) //2
t = t.concatenating(CGAffineTransform(translationX: 50, y: 150)) //3
view.transform = t

To reset a transformation, set the transform property to CGAffineTransform.identity, which is also the default for a newly created UIView.

Perspective

UIView’s transform is baked by CALayer.transform which has the type CATransform3D and can be used for even more advanced transformations like changing the perspective

var transform = CATransform3DIdentity;
transform.m34 = 1.0 / 500.0;
view.layer.transform = CATransform3DRotate(transform, .pi / 4, 0, 1, 0)

anchorPoint

The anchor point defines how a view behaves during transformations. It's specified using the unit coordinate space, where (0, 0) is the top-left corner of the view's bounds rectangle, and (1, 1) is the bottom-right corner. By default, the anchor point of a UIView is set to (0.5, 0.5), which is the center of the view’s bounds rectangle.

Interestingly, the anchorPoint property was only added to UIView in iOS 16, but it has always been accessible through view.layer.anchorPoint.

Geometric manipulations, like rotation or scaling, occur around the anchor point. For example, if you apply a rotation transform to a view with the default anchor point, the view will rotate around its center.

If you change the anchor point to a different location, the view will rotate around this new point.

How to calculate the frame?

We mentioned in the beginning that frame is a computed property. Now, with enough knowledge, let’s compile all the observations into actual code that computes the frame.

First, let’s follow the positioning logic that is used during the render process.

  1. We start with a rectangle, with origin set to .zero and size set to bounds.size. bounds.origin is not involved in calculations since it only affects subviews and the drawRect output.

  1. Before applying the transform, we must translate the view according to its anchorPoint. We apply the transform only when the view, with its anchorPoint, is aligned with the starting point coordinates (0,0).

  1. Now, we can apply the transform.

  1. The last step is to translate the view by its center value, positioning it correctly.

These transformations can be concatenated into a single transform (i.e., absolute one), which is then applied to zero-origin bounds by using CGRectApplyAffineTransform function.

Here is the final function which computes the frame:

func computeFrame(bounds: CGRect,
                  center: CGPoint,
                  anchorPoint: CGPoint,
                  tranform: CGAffineTransform) -> CGRect {

    // set the initial rectangle to bounds with origin set to (0, 0)
    let zeroOriginBounds = CGRect(origin: .zero, size: bounds.size) // 1

    // combine transformations
    let absoluteTransform = CGAffineTransform(
            translationX: -anchorPoint.x * bounds.width,
            y: -anchorPoint.y * bounds.height
        ) // 2
        .concatenating(tranform) // 3
        .concatenating(CGAffineTransform(translationX: center.x, y: center.y)) // 4

    // apply transform to the initial rectangle
    return CGRectApplyAffineTransform(zeroOriginBounds, absoluteTransform)
}

How can this knowledge be useful for us? Apart from just satisfying curiosity and gaining a better understanding of the internals, this algorithm has some real-world applications. A classic example is an image editor. Imagine you are building an application that creates custom stories. You want to place stickers on top of a photo, move, scale, and rotate them with your finger. For the editing mode, you use the actual UIImageView and layout it by modifying its properties like center, bounds, and transform. Eventually, you’d like to render your story project into a bitmap and share it in high resolution, and that’s where understanding how UIView geometry works becomes really useful. The image rendering code can look like the following:

let renderer = UIGraphicsImageRenderer(size: imageSize)
let image = renderer.image { context in
    let cgContext = context.cgContext
    // render photo…
    // render other things…
    
    // render our sticker
    let absoluteTransform = CGAffineTransform(
        translationX: -sticker.anchorPoint.x * sticker.bounds.width,
        y: -sticker.anchorPoint.y * sticker.bounds.height
    )
        .concatenating(sticker.transform)
        .concatenating(
            CGAffineTransform(translationX: sticker.center.x, y: sticker.center.y)
        )
    
    cgContext.saveGState()
    cgContext.concatenate(absoluteTransform)
    sticker.image.draw(in: CGRect(origin: .zero, size: sticker.bounds.size))
    cgContext.restoreGState()
}

What about Autolayout?

Autolayout doesn't work with frame. It also ignores the transform and anchorPoint properties during its calculations. For each view, Autolayout calculates and sets only the bounds and center. The transform and anchorPoint are still used, but only for the final render on screen, and are applied afterward.

To reiterate, in the Autolayout world, frame, transform, and anchorPoint don't exist. The Autolayout process is: constraints in, bounds and center out.

Additional Resources


Written by psharanda | Software Engineer @Snap. Building things for iOS / macOS / Android / Figma. Specializing in UI / UX / Graphics
Published by HackerNoon on 2023/12/13