frame과 bounds를 observe할 때 차이는?

frame과 bounds를 observe할 때 차이는?

UIView의 크기 변화를 관찰하여 임의로 생성한 Layer의 크기를 조정해야하는 프로젝트가 있었습니다. frame에 observer를 추가를 했고 code생성한 UI에서는 잘 동작을 했습니다. 그런데 storyboard에서 제약조건을 추가한 UI에서는 frame의 크기 변화가 관측되지 않았습니다. bounds에 observer를 추가했을 때 bounds의 크기 변화가 관측되었습니다. 왜 제약조건에서는 frame의 크기 변화가 관측되지 않을지 궁금했습니다.

이번 포스트에서는 frame과 bounds에 대해서 알아보고 각각에 대해서 관측했을 때 어떤 점이 다른지 확인해보겠습니다.


frame

frame은 부모view에 의해 조정되는 view의 위치와 크기를 표현하는 사각형입니다.
view의 위치와 크기를 조정하기 위해서 설정할 수 있습니다.
view의 center값 조정 시 origin point가 변경되고 bounds값에 의해 size가 변경될 수 있습니다.

경고
transform이 identity가 아니라면 frame은 정의되지 않으며 그렇기때문에 해당 값은 무시해야합니다.

경고의 의미가 무엇인지 코드를 통해 확인해 보겠습니다.

let view1 = UIView(frame: CGRect(x: 80, y: 120, width: 100, height: 50))
view1.backgroundColor = .red
print("view1 frame: \(view1.frame)")
print("view1 bounds: \(view1.bounds)")
print("view1 center: \(view1.center)")
        
view1.transform = CGAffineTransform(scaleX: 2, y: 2)
print("--- 변경 후 ---")
print("view1 frame: \(view1.frame)")
print("view1 bounds: \(view1.bounds)")
print("view1 center: \(view1.center)")

// 출력 결과
// view1 frame: (80.0, 120.0, 100.0, 50.0)
// view1 bounds: (0.0, 0.0, 100.0, 50.0)
// view1 center: (130.0, 145.0)
// --- 변경 후 ---
// view1 frame: (30.0, 95.0, 200.0, 100.0)
// view1 bounds: (0.0, 0.0, 100.0, 50.0)
// view1 center: (130.0, 145.0)

위 코드의 결과를 보면 frame의 값은 변경되지만 bounds와 center의 값은 변화가 없습니다.
transform을 scale로 해서 center의 변경이 없는 걸까요? transform을 다른 값으로 변경 후 확인해 보겠습니다.

// view1.transform = CGAffineTransform(scaleX: 2, y: 2)
view1.transform = CGAffineTransform(translationX: 10, y: 0)

// 출력 결과
// view1 frame: (80.0, 120.0, 100.0, 50.0)
// view1 bounds: (0.0, 0.0, 100.0, 50.0)
// view1 center: (130.0, 145.0)
// --- 변경 후 ---
// view1 frame: (90.0, 120.0, 100.0, 50.0)
// view1 bounds: (0.0, 0.0, 100.0, 50.0)
// view1 center: (130.0, 145.0)

frame값을 보면 왼쪽으로 10이동했지만 center의 값은 여전히 변경되지 않았습니다.
경고의 의미처럼 transform 값이 변경됨에 따라 frame값이 영향을 받는 것을 알 수 있습니다.

transform이 수정되었을 때 자식view는 어떤 영향을 받을 가요?

let view2 = UIView(frame: CGRect(x: 10, y: 10, width: 50, height: 30))
view2.backgroundColor = .blue
view1.addSubview(view2)
print("view2 frame: \(view2.frame)")
print("view2 bounds: \(view2.bounds)")
print("view2 center: \(view2.center)")

// 출력 결과
// view2 frame: (10.0, 10.0, 50.0, 30.0)
// view2 bounds: (0.0, 0.0, 50.0, 30.0)
// view2 center: (35.0, 25.0)

출력결과를 보면 view2의 frame 및 bounds, center는 값의 변화가 없습니다. 하지만 실제 앱화면을 보면 view2도 view1의 transform영향을 받아 두배로 큰 화면을 보여줍니다.

  • view1의 scale이 1일 때
    enter image description here

  • view1의 scale이 2일 때
    enter image description here




bounds

bounds는 자신만의 위치체계에서 view의 크기와 위치를 표현하는 사각형입니다.
기본 bounds값은 origin은 (0, 0)이고 크기는 frame의 크기와 같습니다. 크기를 조정하면 center기준으로 증가하거나 축소됩니다. 또한 frame의 크기도 변경합니다.

코드를 통해 확인해보겠습니다.

let view1 = UIView(frame: CGRect(x: 80, y: 120, width: 100, height: 50))
view1.backgroundColor = .red
print("view1 frame: \(view1.frame)")
print("view1 bounds: \(view1.bounds)")
print("view1 center: \(view1.center)")
        
view1.bounds = CGRect(x: 0, y: 0, width: 50, height: 25)
print("--- 변경 후 ---")
print("view1 frame: \(view1.frame)")
print("view1 bounds: \(view1.bounds)")
print("view1 center: \(view1.center)")

// 출력 결과
// view1 frame: (80.0, 120.0, 100.0, 50.0)
// view1 bounds: (0.0, 0.0, 100.0, 50.0)
// view1 center: (130.0, 145.0)
// --- 변경 후 ---
// view1 frame: (105.0, 132.5, 50.0, 25.0)
// view1 bounds: (0.0, 0.0, 50.0, 25.0)
// view1 center: (130.0, 145.0)

bounds의 크기 변경 시 center값은 변경없고 크기만 변경된 것을 확인할 수 있습니다.

bounds의 origin을 변경할 때 어떤 변화가 있을가요?

view1.bounds = CGRect(x: -200, y: 0, width: 100, height: 50)

위 코드만 실행한 경우 view1에는 아무 변화가 없습니다.
여기에 view2를 자식으로 추가해보면 어떤 변화가 있는지 이해할 수 있습니다.

let view2 = UIView(frame: CGRect(x: 10, y: 10, width: 50, height: 30))
view2.backgroundColor = .blue
view1.addSubview(view2)
print("view2 frame: \(view2.frame)")
print("view2 bounds: \(view2.bounds)")
print("view2 center: \(view2.center)")

// 출력 결과
// view2 frame: (10.0, 10.0, 50.0, 30.0)
// view2 bounds: (0.0, 0.0, 50.0, 30.0)
// view2 center: (35.0, 25.0)

enter image description here

view2가 view1안에 그려지는 게 아니라 200pt떨어진 위치에 그려집니다. bounds의 origin을 수정하면 자신의 좌표 체계의 origin만을 변경시킨다는 것을 알 수 있습니다.





frame과 bounds에 대해서 알아보았습니다.
지금부터는 frame과 observer를 관측할 때 코드로 생성할 때와 storyboard의 제약 조건을 추가할 때에 어떤 차이가 있는지 알아보겠습니다.

  • 코드로 UI를 생성할 때

    class ViewController: UIViewController {
    
        var testView: UIView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            testView = UIView()
            
            testView.addObserver(self, forKeyPath: #keyPath(UIView.bounds), options: [.new, .initial], context: nil)
            testView.addObserver(self, forKeyPath: #keyPath(UIView.frame), options: [.new, .initial], context: nil)
            testView.addObserver(self, forKeyPath: #keyPath(UIView.center), options: [.new, .initial], context: nil)
        }
    
        
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            guard let newValue = change?[NSKeyValueChangeKey.newKey] as? NSValue else { return }
            
            if keyPath == #keyPath(UIView.bounds) {
                print("bounds: \(newValue)")
            } else if keyPath == #keyPath(UIView.frame) {
                print("frame: \(newValue)")
            } else if keyPath == #keyPath(UIView.center) {
                print("center: \(newValue)")
            }
        }
        
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            
            testView.frame = view.bounds.insetBy(dx: 20, dy: 20)
        }
    
    }
    
    // 실행 결과
    // bounds: NSRect: {{0, 0}, {0, 0}}
    // frame: NSRect: {{0, 0}, {0, 0}}
    // center: NSPoint: {0, 0}
    // frame: NSRect: {{20, 20}, {374, 856}}
    

    bounds와 frame, center의 초기값이 zero로 관측된 후에 testView의 frame이 변경되어 frame의 변화가 관측되었습니다. frame에 의해 center와 bounds의 크기가 변경되더라도 frame만 변경되었다고 관측됩니다.

  • storyboard에서 제약조건으로 UI를 생성할 때
    빨간색 View를 아래왁 같이 가운데 배치하고 제약조건을 추가합니다.
    enter image description here

    코드는 아래와 같이 작성합니다.

    class ViewController: UIViewController {
    
        @IBOutlet var testView: UIView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            testView.addObserver(self, forKeyPath: #keyPath(UIView.bounds), options: [.new, .initial], context: nil)
            testView.addObserver(self, forKeyPath: #keyPath(UIView.frame), options: [.new, .initial], context: nil)
            testView.addObserver(self, forKeyPath: #keyPath(UIView.center), options: [.new, .initial], context: nil)
        }
    
        
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            guard let newValue = change?[NSKeyValueChangeKey.newKey] as? NSValue else { return }
            
            if keyPath == #keyPath(UIView.bounds) {
                print("bounds: \(newValue)")
            } else if keyPath == #keyPath(UIView.frame) {
                print("frame: \(newValue)")
            } else if keyPath == #keyPath(UIView.center) {
                print("center: \(newValue)")
            }
        }
    
    }
    
    // 실행결과
    // bounds: NSRect: {{0, 0}, {215, 100}}
    // frame: NSRect: {{80, 80}, {215, 100}}
    // center: NSPoint: {187.5, 130}
    // 아래부터 제약조건에 의해 수정됨.
    // bounds: NSRect: {{0, 0}, {254, 100}}
    // center: NSPoint: {207, 174}
    

    bounds와 frame, center 초기값이 먼저 관측되고 제약조건에 의해 bounds와 center값이 변경되었을 때 관측되는 것을 볼 수 있습니다. 코드로 생성했을 때와 다르게 frame은 관측되지 않습니다.



제약조건을 추가했을 때는 frame으로 크기를 조정하지 않고 왜 bounds와 center를 이용하여 크기와 위치를 조정하는 걸가요?
그것은 frame 설명의 경고에서 다룬것처럼 transform이 identity가 아닐 경우 frame이 무시되기 때문이라고 생각됩니다.

sample code를 통해 확인해 보겠습니다.

enter image description here

스토리 보드를 위와 같이 구성합니다. 빨간 testView에 노란색 childView를 가운데 추가하고 파란색의 siblingView를 구성했습니다. 제약 조건은 화면처럼 보여지도록 구성했습니다.
앱을 실행하면 아래 화면처럼 보입니다.
enter image description here

class TestView: UIView {

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.transform = CGAffineTransform(translationX: 50, y: 20)
    }
}

testView의 transform을 위 코드처럼 수정하고 결과를 확인해보겠습니다.

enter image description here

빨간색의 testView가 왼쪽으로 50, 아래로 20 이동하고 그려지고 노란색 childView는 변경된 빨간색 testView에 맞춰 가운데에 그려집니다. 하지만 파란색의 siblingView는 변화없이 처음과 동일한 위치에 그려집니다.
노란색 childView는 빨간색의 testView의 bounds와 center가 변경되지 않았기 때문에 빨간색 view의 중앙에 여전히 위치합니다. 파란색 siblingView도 위치는 빨간색 testView의 center값을 기준으로 위치가 지정되기 때문에 처음과 동일한 곳에 위치합니다.



이번에는 코드로 storyboard와 동일하게 생성해보겠습니다.

class ViewController: UIViewController {

    var testView: UIView!
    var childView: UIView!
    var siblingView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        testView = TestView()
        testView.backgroundColor = .red
        view.addSubview(testView)
        childView = UIView()
        childView.backgroundColor = .yellow
        testView.addSubview(childView)
        siblingView = UIView()
        siblingView.backgroundColor = .blue
        view.addSubview(siblingView)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        testView.frame = CGRect(x: 80, y: 124, width: 254, height: 100)
        childView.frame = CGRect(x: testView.bounds.midX - 50, y: testView.bounds.midY - 25, width: 100, height: 50)
        siblingView.frame = CGRect(x: testView.frame.minX, y: testView.frame.maxY + 30, width: 100, height: 60)
    }

}

위 코드를 실행하면 처음 storyboard 버전과 동일한 결과를 보여줍니다.

이번에는 transform을 수정해보겠습니다.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    testView.frame = CGRect(x: 80, y: 124, width: 254, height: 100)
    // transform 수정
    testView.transform = CGAffineTransform(translationX: 50, y: 20)
    childView.frame = CGRect(x: testView.bounds.midX - 50, y: testView.bounds.midY - 25, width: 100, height: 50)
    siblingView.frame = CGRect(x: testView.frame.minX, y: testView.frame.maxY + 30, width: 100, height: 60)
}

enter image description here

실행결과는 위와 같습니다. storyboard버전 결과와 다르게 파란색 siblingView도 빨간색 testView와 같이 왼쪽으로 50, 아래로 20 이동해서 보여집니다. frame을 통해 위치과 크기를 설정하면 transform의 변화에 영향을 받기 때문에 일관된 동작을 보여줄 수 없습니다.

storyboard 버전과 동일한 결과를 만들기 위해 이번에는bounds와 center를 이용해서 아래와 같이 수정해 보겠습니다.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    testView.frame = CGRect(x: 80, y: 124, width: 254, height: 100)
    testView.transform = CGAffineTransform(translationX: 50, y: 20)
    childView.frame = CGRect(x: testView.bounds.midX - 50, y: testView.bounds.midY - 25, width: 100, height: 50)
    siblingView.bounds = CGRect(x: 0, y: 0, width: 100, height: 60)
    siblingView.center = CGPoint(x: testView.center.x - 77, y: testView.center.y + 110)
}

실행 결과는 storyboard버전에서 transform값을 변경한 화면과 동일하게 표시됩니다.




이번 test를 통해서 제약조건에서 동작할 때 frame이 아닌 bounds와 center를 이용해서 위치와 크기를 조정한다는 것을 알게 되었고 frame을 사용했을 때와 bounds와 center를 사용할 때 결과가 다르다는 것을 알게 되었습니다.
또한 transform을 수정하면 frame이 영향을 받지만 boudns와 center는 변경되지 않기 때문에 일관된 작업을 하기 위해서는 frame이 아닌 bounds와 center를 사용해야한다는 것을 알게되었습니다. 이 이유 때문에 제약조건에서 bounds와 center를 사용하는 것 같습니다.



참고

댓글

이 블로그의 인기 게시물

dismiss에 대해서 알아봅시다

1. Framework: framework란?

closure에서는 왜 self를 사용해야 할까?