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.
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.
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?
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.
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
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:
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.
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.
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)
.
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
.
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)
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.
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.
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.
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
).
Now, we can apply the transform.
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()
}
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.