[Swift] Frame VS Bounds (1)

o_jooon_·2024년 4월 2일
0

swift

목록 보기
6/8
post-thumbnail

이번 포스팅은 iOS 개발을 하면서 자주 보게되는 frame과 bounds에 대한 내용입니다.
둘 다 뷰의 크기, 좌표 등을 계산하거나 수정할 때 사용하는데요, 둘의 차이가 무엇인지 비교해보겠습니다!
분량이 많아서 최소 2개정도 나누어 포스팅을 할 예정입니다.

다음 포스팅: [Swift] Frame VS Bounds (2)

첫 번째 포스팅은 frame과 bounds의 기본 정의 및 생성과 회전 시의 차이입니다.

전체 코드


extension UIView {
    open var frame: CGRect
    open var bounds: CGRect

들어가기에 앞서, frame과 bounds는 UIView에 속해있는 속성입니다.
따라서, UIView를 상속 받는 모든 UIComponents에서 확인할 수 있습니다.

struct CGRect {
	init(origin: CGPoint, size: CGSize)

두 속성 모두 CGRect 타입인 것을 보면, origin(위치)size(크기)를 가지고 있죠.
origin은 x, y값을 size는 width, height값을 가지고 있습니다.
(x, y)는 뷰의 가장 왼쪽 위의 지점을 나타내고,
(width, height)는 뷰의 가로 세로 크기를 나타냅니다.


먼저 다음과 같이 오토레이아웃을 설정한 후 frame과 bounds를 출력해보겠습니다.

private func addUI(superView: UIView, subView: UIView) {
    superView.addSubview(subView)
    subView.translatesAutoresizingMaskIntoConstraints = false
}

private func configureUI() {
    addUI(superView: view, subView: safeArea)
    addUI(superView: view, subView: firstView)
    addUI(superView: firstView, subView: secondView)
}
    
private func configureLayout() {
    NSLayoutConstraint.activate([
    	safeArea.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        safeArea.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        safeArea.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
        safeArea.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            
        firstView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
        firstView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
        firstView.widthAnchor.constraint(equalToConstant: 300),
        firstView.heightAnchor.constraint(equalToConstant: 300),
            
        secondView.topAnchor.constraint(equalTo: firstView.topAnchor, constant: 20),
        secondView.leadingAnchor.constraint(equalTo: firstView.leadingAnchor, constant: 20),
        secondView.widthAnchor.constraint(equalToConstant: 200),
        secondView.heightAnchor.constraint(equalToConstant: 200)
    ])
}

흰색의 위 아래 부분을 포함한 화면 전체를 나타내는 뷰(ViewController의 view)가 rootView,
빨간색의 범위를 가진 사각형을 나타내는 뷰가 safeArea,
초록색의 범위를 가진 사각형을 나타내는 뷰가 firstView,
파란색의 범위를 가진 사각형을 나타내는 뷰가 secondView 입니다.

네 개 모두 frame과 bounds를 보면 비슷해보이지만 다른게 있죠?? 바로 x, y의 값입니다.
frame은 각각 다른 좌표값을 가진 반면, bounds는 모두 (0.0, 0.0)의 좌표를 가지고 있습니다.

이제 이런 차이가 발생하는 이유를 알아봅시다.


Frame

frame은 superview의 좌표계에서 뷰의 위치와 크기를 나타내는 사각형 이라고 합니다.
frame과 bounds 모두 origin(위치)와 size(크기)에 대한 정보를 가지고 있었죠?
frame은 Super view를 기준으로 현재 뷰의 위치와 크기 정보를 나타냅니다.

따라서, frame은 뷰의 좌상단 좌표가 superview의 원점으로부터 상대적으로 얼마만큼 떨어져 있는지 및 얼마나 큰지의 정보를 가진 사각형을 말합니다.

가장 중요한 것은 superview의 좌표계상대적 이라는 키워드 인데요!
뷰를 화면에 나타내기 위해서는 가장 최상단 뷰인 ViewController의 view에서부터 추가되기 때문에,
최상단 뷰를 제외한 모든 뷰는 superview가 존재하고 frame은 superview의 좌표 공간을 기준으로 이루어집니다.
또한, 상대적이기 때문에, superview의 frame이 변함과 동시에 해당 뷰의 subview들의 화면에서 보이는 위치(절대적 위치)가 달라져도 subview들의 frame 자체는 변하지 않습니다.

좌표 공간은 frame.origin에서 확인할 수 있는 (x, y)라고 생각하면 됩니다.
superview가 Affine 변환을 통해 origin이 변경되는 경우, subview는 superview에 대해 상대적인 좌표를 표시하기 때문에 superview.frame.origin이 변경된다고 해서 subview.frame.origin은 변경되지 않습니다.
superview의 좌표 공간의 모든 정보가 이동하면서 그 안에 속해있는 subview의 좌표 공간 정보도 함께 이동하는 것으로, subview는 superview에서 독립적으로 이동하는 것이 아니기 때문입니다.

이 내용은 아래에 회전 부분에서 다시 다루겠습니다.

Superview

그럼 대체 superview는 뭐냐? 바로 현재 뷰의 상위 뷰 입니다.
rootview와 superview를 헷갈리시면 안됩니다!
자기 자신을 하위뷰로 가지고 있는 바로 한 단계 위의 상위뷰가 superview에요!

Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview.

Document에서 설명하는 addSubView에 따르면, superview는 하나만 가질 수 있다고 합니다.
superview를 이미 가지고 있는 뷰를 다른 뷰에서 addSubView를 하는 경우 다음과 같은 과정이 일어납니다.

  1. 기존의 superview는 해당 view를 subview에서 제거한다.
  2. 새로운 superview에 해당 view를 subview로 추가한다.

간략하게 보면 다음과 같아요.

let a = UIView()
let b = UIView()
let c = UIView()
let d = UIView()
let e = UIView()

a.addSubView(b) // a는 b의 superview, b는 a의 subview
a.addSubView(c) // a는 c의 superview, c는 a의 subview -> superview는 subview를 여러개 가질 수 있음
b.addSubView(d) // b는 d의 superview, d는 b의 subview

/*

	a    (rootview)
   | \
   b  c
   |
   d						

*/

e.addSubView(b) // a가 superview로 있던 b를 subview로 추가

/*

	a    (rootview)	   e		b의 superview가 e로 변경됨과 동시에
    |				   |		b의 subview였던 d도 함께 e에 속하게됨
    c				   b
    				   |
                       d

*/

여기서 잘 봐야 하는 점은 b의 superview가 e로 변경되면 b의 subview였던 d도 함께 이동한다는 점입니다.

아무튼, 처음에 보여드렸던 이미지의 계층 구조는 다음과 같습니다.

safeArea와 firstView는 rootView에 속해있으므로
rootView는 safeArea와 firstView의 superview 이며, safeArea와 first는 rootView의 subview 입니다.

secondView는 firstView에 속해있으므로
firstView는 secondView의 superview 이며, secondView는 firstView의 subview 입니다.


Bounds

bounds은 자기 자신의 좌표계에서 뷰의 위치와 크기를 나타내는 사각형 이라고 합니다.
frame과 가장 다른 점이 어떤 뷰를 기준으로 삼는가 입니다.
frame은 superview를 기준으로 했지만, bounds는 자기 자신을 기준으로 합니다.

즉, frame에 대한 속성값을 변경시키면, superview를 기준으로 한 속성이 바뀌고
bounds에 대한 속성값을 변경시키면, 자기 자신을 기준으로 한 속성이 바뀌는 것이죠.

그렇기 때문에, 뷰를 새로 정의하거나 superview를 기준으로 변경을 주고싶은 경우에 frame을,
자기 자신을 기준으로 변경을 주고싶은 경우에 bounds를 쓴다고 보면 되겠습니다.

그럼 이해하기 쉽게 사진과 함께 설명하도록 하겠습니다!


생성

frame과 bounds의 기본적인 설명이 끝났으니, 이제 가장 위에 올린 이미지를 파헤쳐봅시다.

그 전에, frame에 대해 헷갈릴 수 있는 부분이 많아 bounds보다 frame에 대한 설명이 훨씬 길어진다는 점...! 양해바랍니다..

rootView

_______rootView_______
frame -> (x: 0.0, y: 0.0, width: 393.0, height: 852.0)
bounds -> (x: 0.0, y: 0.0, width: 393.0, height: 852.0)

먼저 rootView부터 봅시다.
ViewController의 view는 보통 화면 전체를 나타내기 때문에, origin(x,y)은 기본값으로 0.0에서 화면의 크기만큼 지정되어 있습니다.

safeArea

_______safeArea_______
frame -> (x: 0.0, y: 59.0, width: 393.0, height: 759.0)
bounds -> (x: 0.0, y: 0.0, width: 393.0, height: 759.0)

safeArea는 rootView의 subview로 정의했었죠?

frame:

rootView를 기준으로 y가 59만큼(아래로 59만큼) 떨어져 있다는 뜻입니다.
오토레이아웃을 view.safeAreaLayoutGuide에 모든 방향을 0으로 걸어주었는데, y값이 저렇게 설정되어 있네요.
해당 영역의 크기와 좌표는 기종, 설정에 따라 달라집니다.

bounds:

bounds.origin 값을 변경하지 않았기 때문에 (0.0, 0.0)으로 되어있습니다.

firstView

_______firstView_______
frame -> (x: 20.0, y: 79.0, width: 300.0, height: 300.0)
bounds -> (x: 0.0, y: 0.0, width: 300.0, height: 300.0)

firstView 또한 rootView의 subView로 되어있었습니다.

frame:

rootView를 기준으로 x는 20.0, y는 79.0만큼 떨어져 있습니다.

safeArea와 20씩 차이나는게 보이죠?
오토레이아웃을 view.safeAreaLayoutGuide.topAnchor와 bottomAnchor에 20만큼 주었기 때문입니다.

frame과 bounds 모두 width와 height를 300으로 고정했기 때문에 300.0으로 나왔네요.

secondView

_______secondView_______
frame -> (x: 20.0, y: 20.0, width: 200.0, height: 200.0)
bounds -> (x: 0.0, y: 0.0, width: 200.0, height: 200.0)

secondView는 firstView의 subView로 되어있었습니다.

frame:

firstView를 기준으로 x와 y 모두 20.0씩 떨어져 있습니다.

여기서 아시겠나요?? x는 firstView와 같고, y는 safeArea보다 작은데도 가장 안쪽에 위치하고 있죠.
Superview의 좌표를 기준으로 정해지기 때문입니다.

firstView는 rootView를 기준으로 (20.0, 79.0)만큼 떨어져 있고, secondView는 firstView를 기준으로 (20.0, 20.0)만큼 떨어져 있기 때문에, 결과적으로 secondView는 rootView를 기준으로 (40.0, 99.0)만큼 떨어져 있는 것과 같습니다.

회전

frame과 bounds에 대해 찾아보다 잘 설명되어있는 블로그의 포스트를 발견하여 회전 후 frame과 bounds를 비교해주는 상황도 추가해봤습니다.

[참조: 개발자 소들이님]

다음과 같은 네 가지 상황에 대해서 비교를 해 볼 예정입니다.
1. firstView만 90도 회전
2. secondView만 90도 회전
3. 1번의 경우 + secondView를 90도 회전
4. 3번의 경우 + firstView를 90도 회전

각 좌표는 소수점 1자리까지 출력하였습니다.

1. firstView만 90도 회전

firstView가 회전하면서 secondView도 함께 회전합니다.
superview의 변화는 당연하게 subview에도 영향이 끼칩니다.

사진은 frame을 기준으로 했습니다. 초록색과 파란색의 점은 두 뷰의 frame.origin에 대한 점입니다.
bounds.origin은 두 뷰의 가장 왼쪽 모서리가 됩니다.

frame:

갈색의 테두리가 rootView.frame, 갈색의 점이 rootView.frame.origin의 위치입니다.
빨간색의 테두리가 실질적인 firstView.frame, 빨간색의 점이 실질적인 firstView.frame.origin의 위치입니다.

frame은 화면에 보이는 정보(초록 사각형)가 아닌, 화면에 사각형을 그려주기 위한 사각형(빨간색을 기준으로 한 사각형)의 정보를 나타내는 것이었습니다.

회전한 후, 해당 뷰를 그려주는 빨간 테두리의 사각형이 firstView의 frame이 되면서 frame.origin의 값은 (-42.1, 16.9)로, frame.size의 값은 (424.3, 424.3)으로 바뀌었습니다.
x좌표는 rootView의 범위를 벗어나면서 음수로, y좌표는 rootView의 safeAreaLayoutGuide를 벗어나면서 16.9가 되었습니다.
width와 height는 각각 대각선으로 늘어난 뷰를 위해 434.3으로 늘어났네요.

secondView의 경우, 분홍색 테두리를 따라서 새로 그려지고 같이 늘어날 줄 알았는데 그대로였어요.
아까 말했듯이 frame은 superview와 상대적이기 때문에, secondView의 기준에서는 초록색 점이 원점(0.0, 0.0)인 것입니다.

bounds:

bounds는 두 개의 뷰 모두 그대로의 값을 가지고 있습니다.
자기 자신을 나타내는 속성값이니, 회전하면 큰 관점에서 바라보는 저희 입장에서는 변한거지만 자기 자신은 변하지 않은거죠.

2. secondView만 90도 회전

secondView의 회전은 rootview의 입장에서 firstView가 회전한 1번의 경우와 같습니다.

frame:

secondView가 기준으로 보고 있는 firstView의 원점은 초록색의 점입니다.
firstView가 볼 때, secondView의 frame은 회전하면서 해당 뷰를 그려주는 분홍 테두리의 사각형이 됩니다.

3. 1번의 경우 + secondView를 90도 회전

firstView를 90도 회전 시킨 후, secondView를 90도 회전시킨 경우입니다.
결과 자체는 1번의 경우와 2번의 경우를 합친 것과 같습니다.

frame:

firstView.frame의 origin(x, y)은 빨간 점으로, firstView가 기준으로 잡고있는 rootView의 원점인 화면 가장 왼쪽 위를 기준으로 한 값을 나타냅니다.
secondView.frame의 origin(x, y)은 분홍 점으로, secondView가 기준으로 잡고있는 firstView의 원점인 초록색 점을 기준으로 한 값을 나타냅니다.

firstView와 secondView 모두 독립적으로 90도씩 회전했기 때문에, superview의 기준에서 각각 바뀐 좌표들이 출력되는 것이죠.

4. 3번의 경우 + firstView를 90도 회전

firstView를 90도 회전 시킨 후, secondView를 90도 회전시키고 firstView를 한 번 더 90도 회전시킨 경우입니다.
firstView는 기존 좌표로 돌아왔으나 secondView의 위치 및 모양이 많이 달라졌습니다.

frame:

그럼에도 불구하고 secondView.frame의 값은 3번과 동일하죠.
이유는 위의 경우들에서 설명한 것과 동일합니다.


결론

frame:

frame은 superview의 원점(좌상단 지점)을 기준으로 한 origin(x, y)값을 가지며,
크기는 해당 뷰를 화면에 그릴 수 있는 네 점 (x, y), (x + width, y), (x, y + height), (x + width, y + height) 을 포함하는 가장 작은 직사각형을 뜻한다.

superview가 회전한다고 해서 subview의 frame.origin의 값은 바뀌지 않는다.
-> superview의 회전은 해당 superview의 superview를 기준으로 독립적으로 회전하는 것일 뿐, subview는 superview를 따라서 회전하기 때문이다.

bounds:

bounds는 자기 자신을 기준으로 하기 때문에 값을 변경시켜주지 않는 한 항상 동일한 origin(0.0, 0.0)값을 가지며, 크기는 생성될 시점에 정해진 크기의 값을 가진다.

superview의 변화와는 상관없이 독립적이다.


이번 포스팅에서는 frame과 bounds의 정의 및 뷰를 생성하거나 회전했을 때 각각 어떻게 변화하는지 살펴봤습니다.
iOS에서 가장 기본이 되는 것 중 하나라고 생각해서 정리하려고 해봤는데, 생각보다 복잡하고 아주 조금만 보여주었는데도 분량이 엄청나네요..

이 두 속성을 언제 무엇을 어떤 식으로 변경시켜서 사용하는 지는 다음 포스팅에서 설명하겠습니다.
하나에 다 쓰기엔 너무 길어질 것 같아서 ㅎㅎ

profile
iOS개발 공부 중입니다.

0개의 댓글