스토리보드를 사용하지 않고 Constraint를 코드로 부여하기

뷰를 구성하는 요소들의 개수나 위치가 모두 정해지지 않은 경우, 뷰를 구성하는 순간 그 안의 요소들을 생성하고 각각의 constraint를 지정해줘야 할 땐 스토리보드를 사용하기가 애매하다.

나는 받아오는 데이터의 개수에 따라 라벨을 만들고, 각각의 위치를 지정해주고 싶었기 때문에 코드로 constraint 지정하는 방식을 시도하기로 했다.

과정

스토리보드를 사용할 땐, 내가 원하는 UI 요소를 드래그 앤 드랍 한 뒤 해당 요소의 어트리뷰트를 설정해주고, Xcode 하단의 constraint 설정 버튼을 활용하거나, ctrl 드래그를 이용해 제약조건을 설정 해 주면 된다.

코드로 짤 때 역시 이 과정을 똑같이 해 주면 된다.

  1. UI 요소 생성
  2. 어트리뷰트 설정
  3. 제약조건 추가

코드

1. UI 요소 생성

class MyViewController: UIViewController {
    var myLabel: UILabel = {
        var label = UILabel()

        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()


    }
}

myLabel이라는, 현재로선 위치도, 너비와 높이도, 내용도 없는 빈 라벨을 생성했다.

이 라벨이 우리 화면에 제대로 보이기 위해선 너비와 높이를 갖고, 특정 위치를 갖고, 색이나 내용을 가져야 한다. 이를 위의 과정 2, 3번에서 해 주면 된다.

2. 어트리뷰트 설정

라벨을 생성할 때 기본적인 어트리뷰트를 설정해줘도 되고, 함수로 따로 만들어줘도 된다. 나는 함수로 만들어 주었다.

class MyViewController: UIViewController {
    ...
    
    private func setAttribute() {
        myLabel.text = "2unbini"
        myLabel.textAlignment = .center
        myLabel.layer.borderColor = UIColor.systemYellow.cgColor
        myLabel.layer.borderWidth = 5.0
        myLabel.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.2)
    }
}

생성해준 라벨에 텍스트, 배경색, 겉 라인을 추가해줬다. 이렇게 하고 viewDidLoad에 해당 함수를 넣으면..!! 아무 것도 안 뜬다.

안 뜨는 이유 1 - 라벨이 선언만 되어 있다.

스토리보드 상에서 뷰 컨트롤러에 어떤 UI 요소를 추가하면 자동으로 해당 뷰에 그 요소가 들어가게 된다. 하지만, 코드상에선 이 작업을 추가로 해 주어야 한다.

func addSubview(_ view: UIView)

인자로 받는 뷰를 리시버의 서브 뷰 리스트에 추가하는 함수다. 즉, 추가하고자 하는 요소를 인자로 넣어, 추가하고자 하는 뷰에 addSubview 해 주면 된다.

나는 myLabel을 루트 뷰의 자식 뷰로 넣어주고 싶기 때문에 다음과 같은 문장을 추가했다.

view.addSubview(myLabel)

안 뜨는 이유 2 - 라벨의 너비와 높이가 설정되어있지 않다.

그래도 안 뜬다. 왜냐면, 내가 추가한 라벨은 너비와 높이를 갖고 있지 않기 때문이다.

이를 해결하기 위한 방법은 세 가지가 있다. 뷰를 생성할 때 frame을 설정하거나, 어트리뷰트 설정에서 width와 height를 지정해주거나, 제약조건을 줄 때 widthAnchor, heightAnchor의 조건을 지정해주면 된다.

어떻게 하든 제약조건이 짱쎄기 때문에 일단 확인하기 위해 생성 부분을 수정 해 주었다.

class MyViewController: UIViewController {
    var myLabel: UILabel = {
        var label = UILabel(CGRect(x: 0, y: 0, width: 100, height: 100))

        return label
    }()
    ...
}

이렇게 하고 Run 해주면 이렇게 디바이스 왼쪽 위 끝에 딱 붙은 100 * 100짜리 라벨이 나온다.

옹졸한라벨

3. 제약조건 추가

이제 저 옹졸하게 왼쪽 위에 딱 붙은 녀석의 위치를 제약조건을 통해 조정해주자.

코드로 constraints를 주는 방법은 여러 가지(갓택오버플로우 발)가 있다.

NSLayoutConstraint

근-본 애플 문서를 보자.

두 UI 객체 간 만족해야 하는 관계를 뜻한다. 이게 뭔 말이냐? 함수의 용례를 보면 이해가 쉽다.

NSLayoutConstraint(item: myLabel,
                   attribute: .top,
                   relatedBy: .equal,
                   toItem: view,
                   attribute: .top,
                   multiplier: 1,
                   constant: 100).isActive = true

위 NSLayoutConstraint 객체는 myLabel의 상단이 view의 상단에 1을 곱하고 100만큼 더해진 상태와 동일하다는 것을 뜻한다.

즉, mylabel.top = 1.0 * view.top + 100.0 의 식으로 표현할 수 있는 상태다.

내가 제약조건을 설정하고자 하는 뷰를 item에, 그리고 그 제약조건의 관계를 함께 할 뷰를 toItem에, 그리고 구체적으로 각 뷰의 어떤 부분인지를 attribute에 넣는다. 그리고 위의 식처럼 같은지, 크거나 같은지, 작거나 같은지를 relatedBy에 설정 해 주고, 비율과 상수를 각각 multiplierconstant에 넣어 주면 된다.

이렇게 만들어진 제약 조건을 active하게 만들어주면 내가 정해 준 뷰에 제약조건이 설정된다.

NSLayoutAnchor

근데 위와 같이 해 주면 근본있게 제약조건을 설정해줄 수 있지만, 코드가 길어지는 단점이 있다. 단순히 myLabel의 top에 view의 top + 100 만큼의 제약조건을 주는 것이기 때문에 NSLayoutAnchor 클래스를 사용해 다음과 같이 표현할 수 있다.

myLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true

이렇게 해 주면, 위에서 구구절절 써내려갔던 것과 동일한 제약조건이 실행된다.

translatesAutoresizingMaskIntoConstraints

설정한 제약조건을 모두 active하게 해주고 빌드 해 보면, 아무 일도 일어나지 않는다.

이는 인터페이스 빌더로 생성하지 않은, 코드로 생성한 뷰에 자동으로 설정되는 불리언 값이다. 이 값이 true로 설정된 뷰는 루트 뷰의 제약조건 복사본을 시스템에 의해 자동으로 부여받는다.

뷰의 크기와 위치를 그대로 따르기 때문에 내가 코드로 생성한 뷰의 크기나 위치에 대한 제약조건을 걸 수 없다. 이 크기나 위치에 대해 동적으로 설정하고 싶다면, 이 값을 false로 설정해준 뒤 제약조건을 추가해주면 된다.

라는 내용이 문서에 있다.

다 적용하면

class MyViewController: UIViewController {
    ...
    private func setConstraints() {
        myLabel.translatesAutoresizingMaskIntoConstraints = false
        myLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true
        myLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1).isActive = true
        myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        myLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

다음과 같이 제약조건을 설정해주는 함수를 만들었다.

런 해 보면, 이렇게 짜잔 하고 설정된다.

오예

결론

코드로(Programmatically) UI 요소를 만들고, 제약조건을 걸기 위한 과정은 인터페이스 빌더에서 하는 것과 동일하다.

그대신 코드로 하기 때문에 신경 써 줘야 하는 부분들(addSubview, translatesAutoresizingMaskIntoConstraints)이 있다.

소스코드

📍 전체 소스코드
import UIKit

class ViewController: UIViewController {
    
    var myLabel: UILabel = {
       var label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        configureLabel()
    }
    
    private func configureLabel() {
        view.addSubview(myLabel)
        setAttribute()
        setConstraints()
    }

    private func setAttribute() {
        myLabel.text = "2unbini"
        myLabel.textAlignment = .center
        myLabel.layer.borderColor = UIColor.systemYellow.cgColor
        myLabel.layer.borderWidth = 5.0
        myLabel.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.2)
    }
    
    private func setConstraints() {
        myLabel.translatesAutoresizingMaskIntoConstraints = false
        myLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true
        myLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1).isActive = true
        myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        myLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

잘못된 정보나 더 나은 방향이 있다면 언제든 알려 주세요!

댓글남기기