Leonardo Maia Pugliese
Holy Swift

Holy Swift

How to Center a View in the Superview with UIKit using Swift

How to Center a View in the Superview with UIKit using Swift

Leonardo Maia Pugliese's photo
Leonardo Maia Pugliese
·Apr 21, 2022·

8 min read

Subscribe to my newsletter and never miss my upcoming articles

Hallo vrienden, Leo hier.

Today we will explore interesting facts about UIKit coordinate system. The goal today is to center a subview within its superview. It's been a very busy week for me and the article today will be a very fast one!

Centering a view within its superview is a very common UI that we iOS developers try to achieve. This article will explain four ways you can achieve that and we will try four different approaches and how they differ from each other.

The most important thing to focus on is how your UI is already built to adapt the best technique for you.

No more talking, let's code! But first...

Painting of the Day

Today we featured the an 2001 painting called The Golden Jar from Israel Tsvaygenbaum. He was born in February 1, 1961, and is a Russian-American artist of Jewish descent. A number of his works are in the Museum of Imitative Arts, Derbent.

The culture and dynamic of the life of the area where Tsvaygenbaum grew up and his Jewish background are reflected in his paintings.

I chose this painting because looks like the flowers are not centered, maybe he forgot to convert those coordinated systems?

The Problem

You have two views. One view is a subview of the other and you want that subview to be centered within the superview.

The first thing to follow this article is to start a new project in Xcode with Storyboard. Now open the generated ViewController class and copy/paste this code:

class ViewController: UIViewController {
    let redSquare: UIView = {
        var view = UIView(frame: CGRect(x: 100, y: 200, width: 200, height: 200))
        view.backgroundColor = .red
        return view
    }()

    let yellowSquare: UIView = {
        var view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        view.backgroundColor = .yellow
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(redSquare)
        redSquare.addSubview(yellowSquare)
    }
}

We created two view objects. One is a red square( the redSquare property) and the other one is a yellow square ( the yellowSquare property). Should look like the image below:

Simulator Screen Shot - iPhone 13 Pro - 2022-04-21 at 10.51.33.png

But we want the yellow square to be centered on the red square. How can we do that?

Disclaimer: The first three solutions implies that you already have the frame of both views.

Solution 1 - Converting Frames

You could think: "Well to center both views I just need to set the center property of the yellow view to be equal to the center property of the red view". Let's do that:

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(redSquare)
    redSquare.addSubview(yellowSquare)
    yellowSquare.center = redSquare.center 
}

The result is:

Simulator Screen Shot - iPhone 13 Pro - 2022-04-21 at 10.54.22.png

But why?

Now begin the lesson about coordinate systems. Each view has its coordinate system, that begins from the top left corner. Each time you add a subview to another view, the subview now is within the coordinate system of the superview. This means that the frame X and Y positions values are relative to the superview, also the center property.

In other words, think that the world for the view is its superview. The view needs to be localized in the world, so it uses the superview "world" to do that.

Now that you know that, we can easily explain why our code doesn't work. Let's print it out to make it more clear:

Screenshot 2022-04-21 at 10.58.25.png

As you can check in the image above, the red square center is in x = 200 and y = 300 IN RELATION to its superview, which means in relation to the UIViewController standard view. And the center position of the yellow view is x = 50 and y = 50 IN RELATION to the red square view.

After we set the new center of the yellow subview to x = 200 and y = 300 that means we will move our view 200 points to the right and 300 points down, and that is outside the size of the red square view, because the Y value is bigger than the height of the red square view.

To solve that we need to convert the center position of the red square in relation to its super view. Like this:

yellowSquare.center = redSquare.convert(redSquare.center, from: view)

And you got it centered!

Simulator Screen Shot - iPhone 13 Pro - 2022-04-21 at 11.18.40.png

Solution 2 - Using Mathematics and Bounds

Now you know how the center property works within the super view coordinates system, you can use some math on your side. Instead of converting things, which can be a little tricky, let's just use the middle of the height and the width of the red view to set our new center:

yellowSquare.center = CGPoint(x: redSquare.frame.size.width/2,
                                      y: redSquare.frame.size.height/2)

Build and run to check if works... And it does! But it is not the best alternative to use the redSquare frame for this, it is better to use bounds. But why? Imagine that we apply a transformation and check the code below:

print("redSquare.frame -> \(redSquare.frame)")
redSquare.transform = CGAffineTransform(scaleX: 2, y: 0.4)
print("redSquare.frame \(redSquare.frame)")

yellowSquare.center = CGPoint(x: redSquare.frame.size.width/2,
                                      y: redSquare.frame.size.height/2)

The result is:

Simulator Screen Shot - iPhone 13 Pro - 2022-04-21 at 11.40.09.png

Well... the view is not centered anymore.

To solve that we should use bounds. And to explain let's print what is happening:

Screenshot 2022-04-21 at 11.41.46.png

As you can see, before the transforming function the redSquare frame was (x=100.0, y=200.0, width=200.0, height=200.0) and now after the transform is (x=0.0, y=260.0, width=400.0, height=80.0). Look that transforming functions necessarily change the frame because now it has new coordinates within the super view "world" (coordinate system). But as you can see, the bounds don't change. This means you can reliability trust in the bounds to get the width and height to set the new center.

yellowSquare.center = CGPoint(x: redSquare.bounds.size.width/2, y: redSquare.bounds.size.height/2)

Simulator Screen Shot - iPhone 13 Pro - 2022-04-21 at 11.56.21.png

Nice, isn't it?

Solution 3 - Using CGRect API

One good thing is that Apple does a good job with API. The CGRect has two properties that we can use here, they are midX and midY. They do exactly what you are thinking: Return the x or y- coordinate that establishes the center of a rectangle.

yellowSquare.center = CGPoint(x: redSquare.bounds.midX, y: redSquare.bounds.midY)

As we are using the red square bounds property, it will also work with any transformation process.

Solution 4 - Auto Layout

You can use auto layout in the subview. Let's see:

yellowSquare.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
   yellowSquare.centerXAnchor.constraint(equalTo: redSquare.centerXAnchor),
   yellowSquare.centerYAnchor.constraint(equalTo: redSquare.centerYAnchor)
])

But now the yellow square is missing. Why?

Well, now the yellowSquare.translatesAutoresizingMaskIntoConstraints property is set to false. We are not relying on the frame we previously set in the anonymous closure anymore, now the auto layout constraints will take care of everything. It's way more complicated than just taking over the frame, but for the sake of simplicity let's assume that.

If you print the yellow frame from viewDidLoad and after from didAppear :

Screenshot 2022-04-21 at 13.43.40.png

You can see that our view, is in the right position but with 0 height and 0 widths. Now we just change the layout to the new constraints to set the correct height and width:

NSLayoutConstraint.activate([
   yellowSquare.centerXAnchor.constraint(equalTo: redSquare.centerXAnchor),
   yellowSquare.centerYAnchor.constraint(equalTo: redSquare.centerYAnchor),
   yellowSquare.heightAnchor.constraint(equalToConstant: 100),
   yellowSquare.widthAnchor.constraint(equalToConstant: 100)
])

Now everything should be ok!

If you want to test or see everything we wrote.

import UIKit

class ViewController: UIViewController {

    let redSquare: UIView = {
        var view = UIView(frame: CGRect(x: 100, y: 200, width: 200, height: 200))
        view.backgroundColor = .red
        return view
    }()

    let yellowSquare: UIView = {
        var view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        view.backgroundColor = .yellow
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(redSquare)
        // 1 - first child.center
        redSquare.addSubview(yellowSquare)
//        print("Before set")
//        print("yellowSquare.center -> \(yellowSquare.center) redSquare.center -> \(redSquare.center)")
//        print("yellowSquare.frame -> \(yellowSquare.frame)  redSquare.frame -> \(redSquare.frame) \n")

        //yellowSquare.center = redSquare.center

//        print("After set [yellowSquare.center = redSquare.center]")
//        print("yellowSquare.center -> \(yellowSquare.center) redSquare.center -> \(redSquare.center)")
//        print("yellowSquare.frame -> \(yellowSquare.frame)  redSquare.frame -> \(redSquare.frame) \n")

        //yellowSquare.center = redSquare.convert(redSquare.center, from: view)

        // 2 - Using Math with frame sizes explain why to use bounds instead of frame here
//        print("redSquare.frame -> \(redSquare.frame) redSquare.bounds -> \(redSquare.bounds)")
//        redSquare.transform = CGAffineTransform(scaleX: 2, y: 0.4)
//        print("redSquare.frame -> \(redSquare.frame) redSquare.bounds -> \(redSquare.bounds)")
//
//        yellowSquare.center = CGPoint(x: redSquare.bounds.size.width/2,
//                                      y: redSquare.bounds.size.height/2)
        // 3 - Using bounds Mid Points -  As the bounds refers to the views within itself the middle of that coordinate is the point we want.

       // yellowSquare.center = CGPoint(x: redSquare.bounds.midX, y: redSquare.bounds.midY)

        // 4 - Using Auto Layout
        //print("yellowSquare.frame -> \(yellowSquare.frame) yellowSquare.bounds -> \(yellowSquare.bounds)")

//        yellowSquare.translatesAutoresizingMaskIntoConstraints = false
//        print("[viewDidLoad] yellowSquare.frame -> \(yellowSquare.frame) yellowSquare.bounds -> \(yellowSquare.bounds)")
//
//        NSLayoutConstraint.activate([
//            yellowSquare.centerXAnchor.constraint(equalTo: redSquare.centerXAnchor),
//            yellowSquare.centerYAnchor.constraint(equalTo: redSquare.centerYAnchor),
//            yellowSquare.heightAnchor.constraint(equalToConstant: 100),
//            yellowSquare.widthAnchor.constraint(equalToConstant: 100)
//        ])
    }
}

And we are done!

Summary

Today we checked four ways to center a view within others. Also we explained how center property works and how different coordinate systems can work together.

That's all my people, I hope you liked it as I enjoyed writing this article. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello. You can also sponsor posts and I'm open to writing freelancing! Just reach me in LinkedIn or Twitter for details.

Thanks for the reading and... That's all folks.

credits: image

Did you find this article valuable?

Support Leonardo Maia Pugliese by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this