호댕의 iOS 개발

[Library] SnapKit에 대해 알아보자 (remakeConstraints와 updateConstraints의 차이) 본문

Software Engineering/Swift

[Library] SnapKit에 대해 알아보자 (remakeConstraints와 updateConstraints의 차이)

호르댕댕댕 2022. 8. 29. 16:53

SnapKit 톺아보기

SnapKit은 기존 anchor를 사용해 Constraints를 잡는 것보다 짧고 쉽게 오토레이아웃을 잡을 수 있도록 도와주는 라이브러리이다. 

회사에서 프로젝트를 하며 처음 사용해봤는데 내가 생각했을 때 가장 좋은 장점은 translatesAutoresizingMaskIntoConstraints를 따로 안써도 된다는 것이다. 그리고 코드 자체도 꽤 간단해진다.

 

아래 코드처럼 직접 잡던 것들을 좀 더 간단하게 잡을 수 있다!

NSLayoutConstraint.activate([
	{ 레이아웃 관련 코드 }            
])

 

스토리보드에선 자동으로 translatesAutoresizingMaskIntoConstraints를 false로 할당해주지만 코드로 레이아웃을 잡게 되면 이를 직접 작성해줘야 하기 때문이다. 

은근 잊어버리고 짜기 쉬운 코드였는데 SnapKit은 내부에서 해당 코드를 넣어놨다. 그래서 따로 써주지 않아도 된다. 

위 코드는 ConstraintMaker를 초기화해줄 때 호출된다. 

 

button.snp.makeConstraints {
    $0.size.equalTo(40)
    $0.top.bottom.equalToSuperview().inset(4)
    $0.leading.equalToSuperview().offset(8)
}

기본적으로 UIView에 snp라는 프로퍼티로 접근해 Constraints를 잡게 된다. 

 

그럼 snp라는 프로퍼티는 뭘까?

일단 SnapKit에서 UIView를 typealias로 ConstraintView라고 부르고 있다. 

따라서 snp는 UIView의 Extension에 정의해놓은 프로퍼티이다. 이는 CostraintViewDSL을 반환한다. 여기서는 constraints를 추가, 제거, 업데이트 등을 할 수 있는 메서드들과 Hugging, Compression Resistance를 정할 수 있다. 

snp를 호출하게 되면 UIView를 통해 초기화해서 해당 View의 Constraints와 CRCH를 수정할 수 있게 된다. 

 

Constraints를 잡는 메서드들의 경우 클로저를 파라미터로 받는다. 클로저의 경우 아래 코드처럼 되어 있다. 

(_ make: ConstraintMaker) -> Void

 

ConstraintsMaker에는 다양한 방향으로 Constraints를 잡을 수 있도록 방향별로 프로퍼티를 만들어놓고 있다. 

사용하는 것 자체는 코드로 레이아웃을 잡는 것이 익숙한 사람이라면 금방 익숙해질 수 있다. 

 

offset VS inset

사용하다보면 offset과 inset을 자주 접하게 된다. 

두 개념은 각각 어떻게 다른 것일까?

 

offset의 경우 SuperView에서 offset 값을 단순히 더해주게 된다. 

Anchor를 잡을 때 bottom과 trailing은 음수로 Constant를 잡아주는 것과 동일하다고 생각하면 된다. 기준은 frame의 origin이 좌측 상단에서 시작하는 만큼 오른쪽과 아래로 가면 더해지는 것이다. 

 

말로 설명하면 좀 헷갈리지만 코드를 통해 직접 구현해보면 확실하게 감이 온다. 

import UIKit

import SnapKit
import Then

final class PracticeViewController: UIViewController {
    private let containerView = UIView().then {
        $0.backgroundColor = .red
    }

    private let innerView = UIView().then {
        $0.backgroundColor = .blue
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        render()
    }

    private func render() {
        view.backgroundColor = .yellow

        view.addSubview(containerView)
        view.addSubview(innerView)

        containerView.snp.makeConstraints {
            $0.edges.equalToSuperview().inset(100)
        }

        innerView.snp.makeConstraints {
            $0.edges.equalTo(containerView.snp.edges).offset(80)
        }
    }
}

간단하게 이렇게 코드를 짜봤다. 

배경의 경우 노란색, 그 안에 들어가는 첫번째 컨테이너 뷰는 빨간색, 컨테이너 뷰 안에 들어가는 뷰는 파랑색으로 구성했다. 

빨간 뷰의 경우 전체 edge들로부터 100만큼 떨어지도록 구현했다. 이 때 inset을 사용했다. 

UIEdgeInset처럼 각 가장자리로 부터 안쪽으로 Constraints가 잡히게 된다. 

 

파란색 View의 경우 offset으로 containerView로 부터 80만큼 떨어지게 구현해놓았다. offset의 경우 superView로부터 단순히 얼마나 떨어져 있는지를 보여주기 때문에 위 그림처럼 나오게 된 것이다. 

 

그렇다면 파란색 View(innerView)를 inset으로 레이아웃을 잡게 되면 어떻게 될까?

innerView.snp.makeConstraints {
    $0.edges.equalTo(containerView.snp.edges).inset(80) // offset을 inset으로만 수정
}

파란색 View가 빨간색 가장자리들을 기준으로 안쪽으로 Layout이 잡힌 것을 볼 수 있다. 

이 그림 2개를 비교해보면 inset과 offset의 차이점을 충분히 알 수 있을 것이다. 

 

remakeConstraints VS updateConstraints

updateConstraints가 있는데 왜 굳이 remakeConstraints가 있는 것일까?

이를 알기위해 레이아웃을 잡고 화면을 회전하는 경우를 생각해보자. 

import UIKit

import SnapKit
import Then

final class PracticeViewController: UIViewController {
    private let containerView = UIView().then {
        $0.backgroundColor = .red
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        render()
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

//        changeLayout(size: size)
    }

    private func render() {
        let screenHeight = UIScreen.main.bounds.height
        view.backgroundColor = .yellow

        view.addSubview(containerView)

        containerView.snp.makeConstraints {
            $0.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing)
            $0.top.equalToSuperview().inset(screenHeight * 0.6)
            $0.size.equalTo(50)
        }
    }

    private func changeLayout(size: CGSize) {
        let screenHeight = size.height

        containerView.snp.remakeConstraints {
            $0.trailing.equalToSuperview()
            $0.top.equalToSuperview().inset(screenHeight * 0.6)
            $0.size.equalTo(50)
        }
    }
}

일단 전체적인 코드는 다음과 같다. 

바로 실행하게 되면 이렇게 나온다. 

 

만약 render 함수를 통해 Constraints를 잡아놓고 회전을 시키게 되면 top에서 기존 Screen의 긴 세로를 기준으로 60%만큼 떨어지게 되면서 빨간색 네모가 아예 보이지 않게 된다. 

 

그래서 viewWillTransitionTo에서 레이아웃을 변경해줘야 한다.

그런데 이때 updateConstraints를 사용하게 되면 문제가 발생한다. 

 

기존 top이 leading이나 trailing으로 회전을 하면서 변경하게 되면서 activateIfNeeded(updatingExisting: Bool) 메서드에서 fatalError를 발생시키게 된다. 

 

기존과 기준 자체가 변경되었기 때문이다. 

 

따라서 이 때 사용해야 하는 것이 바로 remakeConstraints이다.

이렇게 되면 회전을 하더라도 새롭게 바뀐 screen의 높이를 기준으로 레이아웃을 다시 잡게 된다.

 

정리를 해보자면 remake는 top, leading, bottom, trailing의 기준 자체가 변경되었을 때 사용하면 된다. 

Comments