[UIView] View에서 CALayer에 추가해준 속성이 적용되지 않았던 이유 (bounds, frame, viewDidLayoutSubviews, layoutSubViews)

2023. 12. 1. 11:12UIKit, H.I.G

728x90

1️⃣ 어떤 이슈가 있었는데?


오랜만에 예전에 쓴 글을 참고해서 gradinet가 적용되는 View를 만들어주려고 했다.

뷰(gradinetView)가 가지는 CAGradientLayer를 잡아서 색상, 위치, 그라디언트의 시작점과 종료 지점을 지정해 주고,
gradient의 frame을 뷰의 bounds와 동일하게 선언해 주고,
이렇게 만들어진 CAGradientLayer 객체를 view의 layer에 추가해 주는 방식으로 만들어주면 될 것이라 생각했다.

하지만, CAGradientLayer 객체가 view에 추가되지 않아 Gradient가 적용된 View를 확인할 수 없었다.

이번 글에서는 해당 View에서 어떤 이유로 gradient가 적용되지 않았는지, 이 문제를 해결하기 위해서 사용한 방식은 어떤 것이었는지를 정리해 보겠다.
처음 코드는 아래에 첨부했다.

private let gradientView: UIView = {
        let view = UIView()
        let gradient = CAGradientLayer()
        
        gradient.colors = [UIColor.black.withAlphaComponent(0.0).cgColor,
                           UIColor.black.withAlphaComponent(0.5).cgColor]
        gradient.locations = [0.0, 1.0]
        gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
        
        gradient.frame = view.bounds
        view.layer.addSublayer(gradient)
        
        return view
    }()

 

2️⃣ frame과 bounds를 완벽하게 먼저 이해하고 넘어가보자!


항상 머리로는 알고 있어도, 헷갈리면서 정확하게 구분 안 되는 개념이 frame과 bounds였다.
이번 개념에서 알고 있어야 하는 부분이므로 한번 이 참에 정리하고 넘어가보도록 하자! 일단 공식문서상에는 이렇게 쓰여있다.

☑️ frame : The frame rectangle, which describes the view’s location and size in its superview’s coordinate system.
                  뷰의 위치와 사이즈를 superview의 좌표계를 기반으로 설명하는 사각형

☑️ bounds : The bounds rectangle, which describes the view’s location and size in its own coordinate system.
                    뷰의 위치와 사이즈를 자체 좌표계를 기반으로 설명하는 사각형

*두 개념 모두 CGRect(origin: CGPoint(x, y), size: CGSize(width, height))를 이용해서 레이아웃을 표현한다.

즉, 객체가 어디에 위치하는지 (왼쪽 상단 점인 x, y좌표로 표현하는 origin), 어떤 크기인지 (가로, 세로로 표현하는 size)에 대한 정보를 담고 있다는 점에서 공통적이지만, 어떤 것을 기준으로 잡는지가 객체의 위치와 크기를 잡는지가 다르다는 뜻이다.

아래 사진에서 UIView의 origin을 예시로 들어보겠다.

frame은 자신보다 상위 계층의 뷰(superview)를 기준으로 위치가 표현되기 때문에 superView인 ImageView의 origin 위치(0, 0)를 기준으로 잡히는 것을 확인할 수 있다. -> 아래 UIView의 origin은 imageView의 origin x로부터 16만큼, origin y만큼 195만큼 떨어져 있음
반면, bounds의 origin은 자기 자신의 위치를 표현하는 것이므로 항상 (0, 0)을 나타낼 것이다.

왼쪽이 frame의 origin, 오른족이 bounds의 origin을 나타낸다.

size 또한 변화가 있다.
단순 사각형 모양일 때는 width와 height 값을 잡는 CGSize값이 동일하다.

하지만, 만약 View를 rotateAngle 시킨다고 했을 때, frame은 양 끝 외곽 점(= 뷰를 감싸는 전체 공간)을 기준으로 size가 잡히고, bounds는 기존 view의 size값과 동일하게 잡히게 된다.

첫 번째는 frame과 bounds의 공통된 size, 두 번째는 frame에서 뷰를 감싸는 size, 세 번재는 bounds에서의 size

그럼 다시 위로 돌아와서 gradint의 frame을 view의 bounds와 동일하게 지정해 줬다는 의미를 이해할 수 있겠다.

 

3️⃣ view의 크기(bounds)가 정해지지 않은 상태에서 gradient의 frame을 지정하는 것은 불가능했다.


오케이. 아무튼 일단 이렇게 frame이랑 bounds는 이해했는데, gradinet layer가 적용되지 않는다는 이슈가 생겼다.

여러 원인을 찾아보다가 gradient의 frame과 view의 bounds를 print로 찍어보니,
(0.0, 0.0, 0.0, 0.0)이 반환되는 충격적인(?) 결과를 마주치게 된다.


(0.0, 0.0, 0.0, 0.0)이 print 되는 것일까 곰곰이 생각을 해보니,
나는 지금 컴포넌트의 속성을 지정해 주는 부분에서 위치를 지정하고 있는 중대한 오류를 저지르고 있었다는 것을 알 수 있었다.

"즉, 뷰의 크기가 정확하게 정해지지 않은 시점에서 정해지지 않은 그 크기만큼 gradient를 그려달라고 요청하고 있으니,
컴퓨터는 "응? 어디에다가 그리라고?" 하면서 그려야 하는 위치를 못 찾고 있었다는 것."
뷰가 확실하게 그려지고 난 다음에 frame이 됐든, bounds가 됬든 지정을 해주면 이 문제를 해결할 수 있다는 의미였다.

 

4️⃣ View와 ViewController 사이의 Layout Cycle (viewWillAppear~viewDidAppear)


예전에 ViewController의 생명주기를 다룬 글에서
뷰의 모양과 동작을 관리하는 ViewController는 loadView -> ViewDidLoad -> viewWillAppear -> viewDidAppear.. 등의 순으로 호출되면서 뷰를 불러온다고 말한 적이 있었다.

 

[UIViewController] ViewController의 생명주기에 대하여 알아보자

0️⃣ ViewController의 생명주기는 무엇이고, 왜 공부를 해야하는거지?ViewController의 생명주기에 대해 들어본 적이 있는가?혹여나 들어본 적이 없다 하더라도, iOS 개발을 하고자 Xcode를 켜본 적이 있

mini-min-dev.tistory.com

 

여기서 오늘 주목해 볼 것은 "뷰가 나타나기 직전에 호출되는 viewWillAppear와 뷰가 나타난 직후에 호출되는 viewDidAppear 사이에 일어나는 일"에 대해 깊게 들어가 볼 것이다


View가 나타나기 직전과, 나타난 직후 사이에는 View와 ViewController 사이의 Layout을 잡기 위한 Cycle의 과정이 포함되어 있다.
즉, 화면에 표시될 뷰들의 크기는 어떻고, 위치는 어디인지를 이 사이에서 잡는다는 의미!

"아. 그러니까 항상 하던 것처럼 viewDidLoad에서 호출했던 위의 내 코드가 위치를 불러오지 못했던 것이었군!"

문제를 확실하게 파악하게 된 순간이었다.
이 Cycle 과정은 다시 3가지로 나뉘게 된다. 아래 글을 확인해 보자.

☑️ Update : AutoLayout의 Constraints를 갱신하는 과정 (자식 뷰부터 부모 뷰까지 가는 순서대로 호출된다.)
☑️ Layout : Update에서 갱신된 Constraints를 바탕으로 레이아웃을 실행시키는 과정
                   이 부분에서 view의 center, bounds, frame이 결정된다. (여기서는 부모부터 자식 방향으로 호출된다.)
☑️ Draw : 위에서 잡힌 레이아웃을 바탕으로 본격적으로 뷰를 그리는 과정

View와 ViewController 사이의 Layout Cycle (회색이 ViewController 호출, 녹색이 View 호출 부분이다.)

 

5️⃣ 그래서 찾은 방법이 viewDidLayoutSubviews()와 layoutSubViews()였다.


결론적으로, 레이아웃이 결정되는 부분에서 frame을 지정해주는 식으로 코드를 수정해 gradient를 수정시킬 수 있었다.

 private let gradientView: UIView = {
        let view = UIView()
        let gradient = CAGradientLayer()
        gradient.colors = [UIColor.black.withAlphaComponent(0.0).cgColor,
                           UIColor.black.withAlphaComponent(0.5).cgColor]
        gradient.locations = [0.0, 1.0]
        gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
        view.layer.addSublayer(gradient)
        return view
    }()
 
 override func layoutSubviews() {
        super.layoutSubviews()
        
        gradientView.frame = storeImage.bounds
        gradientView.layer.sublayers?.first?.frame = gradientView.bounds
    }
728x90