why setting bounds origin makes the subview move to another direction in iOS?

91 Views Asked by At

I'm trying to figure out the difference between frame and bounds in iOS, basically the Bounds refers to the views own coordinate system while Frame refers to the views parent coordinate system. But, I have a viewA and a viewB, ViewA is the parent of ViewB, if I change the bounds of ViewA from (0,0,100,100) -> (50,0,100,100), viewB shold move to right because it's coordinate system move to right by 10 right? but ViewB move to left! See the code and behavior:

class ViewController: UIViewController {
    @IBOutlet weak var viewA: UIView!
    @IBOutlet weak var viewB: UIView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func buttonClicked(_ sender: Any) {
        viewA.layer.bounds = CGRectMake(50, 0, 240, 377)
    }
}

before click after click

Shouldn't the center view move to right not left because we +30?

1

There are 1 best solutions below

1
DonMag On BEST ANSWER

If you head over to Google (or your favorite search engine) and search for UIView frame vs bounds you'll find lots and lots of discussion, along with plenty of articles/blogs that go into great detail on the differences.

But -- to give you a quick answer to why your view moves left instead of right (as you expected)...

Suppose I have a 200x200 UIView. I add a 400x200 UIImageView as a subview, with this image:

enter image description here

and I constrain the image view Top and Leading to the "container" view (make sure .clipsToBounds = true on the container view).

It will look like this (on a yellow background):

enter image description here

which should be no surprise.

Now, let's change the container view's bounds to:

containerView.bounds.origin.x = 50.0

we will see this:

enter image description here

The image view does not get "pushed" 50-points to the right... nor does it get "pulled" 50-points to the left.

What we've done is told the container view to: "display the bounds rectangle from your content (your subview(s))":

enter image description here

That's how a UIScrollView works... if the scroll view's content is larger than the scroll view's frame, we can drag the content... which changes the scroll view's .contentOffset which, in turn, changes the scroll view's bounds.

Quick UIScrollView example showing that:

class ScrollBoundsVC: UIViewController, UIScrollViewDelegate {
    
    let imageView = UIImageView()
    let scrollView = UIScrollView()
    let infoLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemYellow
        
        guard let img = UIImage(named: "img400x200") else {
            fatalError("Could not load image!")
        }
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        infoLabel.translatesAutoresizingMaskIntoConstraints = false
        
        scrollView.addSubview(imageView)
        view.addSubview(scrollView)
        view.addSubview(infoLabel)
        
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            
            imageView.widthAnchor.constraint(equalToConstant: img.size.width),
            imageView.heightAnchor.constraint(equalToConstant: img.size.height),
            
            scrollView.widthAnchor.constraint(equalToConstant: 200.0),
            scrollView.heightAnchor.constraint(equalTo: imageView.heightAnchor, constant: 0.0),
            
            scrollView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            scrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            imageView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            imageView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            imageView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            imageView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),

            infoLabel.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 8.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

        ])
        
        imageView.image = img
        
        scrollView.delegate = self
        scrollView.backgroundColor = .systemRed
        
        infoLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        infoLabel.textAlignment = .center
        infoLabel.numberOfLines = 0
        infoLabel.font = .systemFont(ofSize: 14, weight: .regular)
        
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateInfo()
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        updateInfo()
    }
    func updateInfo() {
        infoLabel.text = "\nscrollView bounds:\n\(scrollView.bounds)\n\nimageView frame:\n\(imageView.frame)\n"
    }
    
}

Looks like this as I scroll to the left:

enter image description here enter image description here

enter image description here enter image description here

Here is about the same thing, but instead of a UIScrollView we'll use a "container" UIView ... each tap anywhere will increment the container view's .bounds.origin.x by 20-points:

class ContainerBoundsVC: UIViewController {
    
    let imageView = UIImageView()
    let containerView = UIView()
    let infoLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemYellow
        
        guard let img = UIImage(named: "img400x200") else {
            fatalError("Could not load image!")
        }
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        infoLabel.translatesAutoresizingMaskIntoConstraints = false

        containerView.addSubview(imageView)
        view.addSubview(containerView)
        view.addSubview(infoLabel)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            imageView.widthAnchor.constraint(equalToConstant: img.size.width),
            imageView.heightAnchor.constraint(equalToConstant: img.size.height),
            
            containerView.widthAnchor.constraint(equalToConstant: 200.0),
            containerView.heightAnchor.constraint(equalTo: imageView.heightAnchor, constant: 0.0),
            
            containerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),

            imageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
            imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
            
            infoLabel.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
        ])

        imageView.image = img

        containerView.backgroundColor = .systemRed
        
        containerView.clipsToBounds = true

        infoLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        infoLabel.textAlignment = .center
        infoLabel.numberOfLines = 0
        infoLabel.font = .systemFont(ofSize: 14, weight: .regular)

    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateInfo()
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        containerView.bounds.origin.x += 10.0
        updateInfo()
    }
    func updateInfo() {
        infoLabel.text = "\ncontainerView bounds:\n\(containerView.bounds)\n\nimageView frame:\n\(imageView.frame)\n"
    }
}

Of course, this has only addressed your specific condition. If you spend some time reading up on frame vs bounds you'll see that it comes into play in a much more critical context when you start applying scaling and rotational transforms to your views.