Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #17] 2023 / 04 / 19

오토 레이아웃을 잡고 객체의 frame을 사용하려 하는데, 한번 애를 먹었던 기억이 있어,

오늘은 이렇게 Update Clycle의 viewDidLayoutSubviews를 한번 간단히 정리하고 넘어가려고 합니다.

 

일단 Life Cycle + Update Cycle를 그림으로 한번 보고 가시죠.

 

각 Cycle 마다, 역할이 조금씩 다릅니다.

각 시점마다 무슨일이 일어나는지 알고나면 프로젝트를 만들 때, 도움이 많이 되더라고요.

 

 

viewDidLayoutSubviews

말 그대로 "UIVIew" 객체가 모든 서브뷰들의 레이아웃을 변경한 후에 호출되는 메서드입니다.

다른 메서드와 달리, 뷰와 서브뷰들의 레이아웃이 모두 정해진 후 호출되는 메서드입니다.

 

다른식으로 말해보면,

viewDidLayoutSubviews는 뷰 + 서브뷰의 크기와 위치가 최종적으로 확인된 후에 호출이 되므로,

즉, 이 시점에서는 모든 레이아웃이 변경 된 후에 호출이 되므로, 뷰와 서브뷰들의 마지막 실제 크기와 위치를 얻을 수 있습니다. 

 

 

분명히 머리로는 이해가 되지만, 언제 써먹는지 궁금할 것 같아서,

간단한 예제를 들고 왔습니다.

 

일단 버튼의 레이아웃을 잡아줬고, cornerRadius를 통해 버튼의 모양을 원으로 만들어 주는 작업을 했습니다.

그리고 viewDidLoad에서 실행을 시켜 줬습니다.

class ViewController: UIViewController {
    
    let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configUI()
    }
    
    func configUI() {
        view.addSubview(button)
        button.backgroundColor = .darkGray
        button.translatesAutoresizingMaskIntoConstraints = false
        
        button.layer.cornerRadius = button.frame.width / 2
        button.clipsToBounds = true
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 50),
            button.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

 

그런데 분명히 버튼 너비 크기만큼 cornerRadius를 줬는데도,

버튼 모양이 변하지 않네요? 이상하죠?

 

원으로 변하지 않는 이유를 확인을 위해,

각 Life Cycle 시점마다 버튼의 Frame을 알아 보기위해 프린트를 찍어봤습니다.

class ViewController: UIViewController {
    
    let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configUI()

        print(#function + " --- \(button.frame)")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print(#function + " --- \(button.frame)")

    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        print(#function + " --- \(button.frame)")
    }
    
    
    func configUI() {
        view.addSubview(button)
        button.backgroundColor = .darkGray
        button.translatesAutoresizingMaskIntoConstraints = false
        
        button.layer.cornerRadius = button.frame.width / 2
        button.clipsToBounds = true
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 50),
            button.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

 

빌드 시, 결과 값으로 아래와 같이 나옵니다.

viewDidLoad() --- (0.0, 0.0, 0.0, 0.0)
viewWillAppear(_:) --- (0.0, 0.0, 0.0, 0.0)
viewDidLayoutSubviews() --- (170.0, 397.0, 50.0, 50.0)

 

viewDidLoadviewWillAppear에서는 (0, 0, 0, 0)이 나오고,

viewDidLayoutSubviews의 시점에서만, Frame의 위치 및 크기들이 정상적으로 나왔네요.

 

이유는 위에서도 간단히 말했지만,

viewDidLayoutSubviews는 뷰가 생성되고 화면에 보이기 직전입니다.

 

즉, 뷰가 생성된 후, 최초로 뷰들의 크기나 위치를 알 수 있는 시점이므로

이와 같이 서브뷰의 크기를 사용이 가능하게 됩니다.

class ViewController: UIViewController {
    
    let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configUI()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        button.layer.cornerRadius = button.frame.width / 2
    }
    
    
    func configUI() {
        view.addSubview(button)
        button.backgroundColor = .darkGray
        button.translatesAutoresizingMaskIntoConstraints = false
        
        button.clipsToBounds = true

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 50),
            button.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
}

 

viewDidLayoutSubviews를 간단히 정리하면

 

일반적으로 서브뷰를 다시 배치해야 하거나, 크기 or 위치를 조정하는 작업에서 사용할 수 있습니다.

 

왜냐하면, viewDidLayoutSubviews는 모든 레이아웃이 모두 정해진 후, 호출이 되므로

서브뷰들의 크기 or 위치 값을 사용하여 UI를 업데이트할 수 있는 거죠!

 

 

참고. layoutSubviews

Life Cycle의 순서를 나타낸 그림을 다시 확인해보면,

실행 순서는 layoutSubviews() ->  viewDidLayoutSubviews() 순으로 호출되게 됩니다.

 

layoutSubviews()는

 뷰의 크기나 위치, 뷰 계층 구조가 변경될 때마다, 호출하게 되는데,

이 작업은 UIView의 하위 모든 서브뷰들의 크기 및 위치를 정해주는 작업이므로 비용이 큰 작업이라고 합네요.

(ex. 뷰가 처음 생성 될 때, 서브뷰가 추가 및 제거될 때, setNeedsLayout, layoutIfNeeded 메서드 등)

 

그 후, viewDidLayoutSubviews가 자동으로 호출이 되게 됩니다.

 

다시 말하지만, layoutSubviews()는 비용이 큰 작업이기 때문에 자주 호출되면, 성능이 떨어질 수 있습니다.

그래도 꼭 사용해야 된다면, 효율을 높히기 위해 최적화하는 방법들을 소개하겠습니다.

 

1. setNeedsLayout를 호출한 후, layoutIfNeeded를 통해 즉시 레이아웃을 업데이트시켜,

layoutSubviews를 여러 번 호출되는 것을 방지할 수 있습니다.

 

2. 뷰 계층 구조를 단순화하여 비용을 줄일 수 있습니다 (하위 뷰의 수를 줄인다)

 

3. 되도록이면 layoutSubviews에서 복잡한 계산을 피하고 정말 필요할 때만 실행()해줘야 합니다.

 

 

참고 사이트

한번 이분 참고해 보세요. Update Cycle을 잘 정리해 놓으셨네요.

https://jeonyeohun.tistory.com/336

https://stackoverflow.com/

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!