본문 바로가기

학습 노트/iOS (2021)

164. Constraints with code #2

Constraints with code #2

top, leading, trailing, bottom

//
//  FillParentViewController.swift
//  Constraints with Code Practice
//
//  Created by Martin.Q on 2021/12/17.
//

import UIKit

class FillParentViewController: UIViewController {
	@IBOutlet weak var bottomContainer: UIView!
	@IBOutlet weak var orangeView: UIView!
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
		layoutWithInitializer()
    }
}

extension FillParentViewController {
	func layoutWithInitializer() {
		
	}
	func layoutWithVisualFormatLanguage() {
		
	}
	func layoutWithAnchor() {
		
	}
}

사용할 Scene과 코드는 위와 같다.
Scene은 상, 하 두 개의 구역으로 나뉘어 있다.
위에서 Interface Builder로 추가하는 제약을 코드로 아래에 적용하도록 한다.

extension으로 추가된 세 메서드는 각각
NSLayoutConstraint, Visual Format Language, NSLayoutAnchor 방식을 사용해 동일한 제약을 구현한다.

Interface Builder로 추가되는 제약은 위와 같다.

NSLayoutConstraint로 제약 추가하기

func layoutWithInitializer() {
		let leading = NSLayoutConstraint(item: orangeView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leading, multiplier: 1.0, constant: 0)
	}

코드로 추가하는 leading 제약은 위와 같다.

파라미터의 순서대로,
item은 제약 대상의 이름,
attribute는 NSLayoutConstraint.Attribute에 선언돼있는 멤버 중 하나,
relatedBy는 NSLayoutConstraint.Relation에 선언돼있는 멤버 중 하나,
toItem은 연관 된 대상의 이름,
multiplier는 제약 공식의 배수,
constant는 실제 제약의 값이다.

func layoutWithInitializer() {
	let leading = NSLayoutConstraint(item: orangeView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leading, multiplier: 1.0, constant: 0)
	let top = NSLayoutConstraint(item: orangeView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .top, multiplier: 1.0, constant: 0)
	let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: 0)
	let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: 0)
}

다른 방향의 제약도 이름만 다르고 동일하다.

func layoutWithInitializer() {
	let leading = NSLayoutConstraint(item: orangeView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leading, multiplier: 1.0, constant: 0)
	let top = NSLayoutConstraint(item: orangeView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .top, multiplier: 1.0, constant: 0)
	let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: 0)
	let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: 0)
	
	NSLayoutConstraint.activate([top, leading, trailing, bottom])
}

이후 activate 메서드를 사용해 제약을 활성화해야 한다.
해당 메서드는 활성화할 제약을 배열로 전달받는다.
activate 메서드는 NSLayoutConstraint에 선언돼있다.

적용한 뒤 확인해 보면 예상과는 다르게 적용돼있는 것을 볼 수 있다.

로그에는 NSAutoresizingMaskLayoutConstraint에 관한 에러들이 몇 개 보인다.
OrangeView에는 아무런 명시적 제약이 존재하지 않고, 이에 따라 Autoresize Mask를 기반으로 제약이 추가되고,
이러한 제약을 AutoresizingMaskLayoutConstraint라고 부른다.
해당 제약이 코드에서 추가한 제약과 충돌하고 있음을 의미한다.

따라서 코드를 통해 제약을 추가할 때는 AutoresizeMaskConstraint가 추가되지 않도록 미리 조치하는 것이 중요하다.

func layoutWithInitializer() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let leading = NSLayoutConstraint(item: orangeView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leading, multiplier: 1.0, constant: 0)
	let top = NSLayoutConstraint(item: orangeView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .top, multiplier: 1.0, constant: 0)
	let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: 0)
	let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: 0)
	
	NSLayoutConstraint.activate([top, leading, trailing, bottom])
}

가장 첫 번째 줄에 translatesAutoresizingMaskIntoConstraints 속성을 false로 변경했다.
코드로 제약을 추가하는 경우 해당 과정을 반드시 진행해야 한다.

이제는 의도한 대로 표시된다.

Visual Format Language로 제약 추가하기

func layoutWithVisualFormatLanguage() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let horizontal = "|[o]|"
	let vertical = "V:|[o]|"
}

Visual Format Language는 수직과 수평방향의 제약을 동시에 표현하지 못한다.
따라서 위와 같이 수직과 수평방향의 제약을 따로 구분해 생성해야 한다.

'[ ]'안의 대상이 View이고, '| |'는 View가 포함돼있는 Superview를 나타낸다.
또한 '| |'와 '[ ]'사이의 간격은 이 둘 사이의 margin을 의미한다.
앞에 붙은 'V:'는 Virtical의 약자로 수직방향의 제약임을 나타내는 키워드이다.
수평방향 제약은 'H:'로 구분하기도 하지만 생략하는 경우가 많다.

따라서 위에 작성한 문장들은 각각
'o의 leading과 trailing을 Superview의 leading과 trailing에 margin 없이 표시하라.'
'o의 top과 bottom을 Superview의 top과 bottom에 margin 없이 표시하라.'
와 같은 의미가 된다.

이때 '[ ]'안의 View의 이름은 실제 View의 이름과 연결된 키워드여야 한다.

func layoutWithVisualFormatLanguage() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let horizontal = "|[o]|"
	let vertical = "V:|[o]|"
	
	let views: [String: Any] = ["o": orangeView]
}

 이렇게 문자열과 지어지는 Dictionary를 생성하고, 이후의 메서드에게 전달해 줘야 한다.

func layoutWithVisualFormatLanguage() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let horizontal = "|[o]|"
	let vertical = "V:|[o]|"
	
	let views: [String: Any] = ["o": orangeView]
	
	let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [], metrics: nil, views: views)
}

constraints 메서드를 사용해 VisualFormatLanguage를 사용한 제약을 생성한다.
두 번째, 세 번째 파라미터는 지금은 사용하지 않고,
첫 번째 파라미터는 방금 작성한 Visual Format Language 제약 문을,
네 번째 파라미터에는 제약의 해석에 사용될 Dictionary를 전달한다.

func layoutWithVisualFormatLanguage() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let horizontal = "|[o]|"
	let vertical = "V:|[o]|"
	
	let views: [String: Any] = ["o": orangeView]
	
	let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [], metrics: nil, views: views)
	
	NSLayoutConstraint.activate(horizontalConstraint + verticalConstraint)
}

NSLayoutConstraint로 생성한 제약은 한 번에 하나의 제약을 반환하지만,
VisualFormatLanguage로 생성한 제약은 제약 문에 따라 두 개 이상의 제약을 반환할 수 있다.
그렇기에 constraints가 반환하는 값은 항상 배열의 형식이다.

따라서 activate 메서드에 위와 같이 전달하면 제약 적용이 완료된다.

viewDidLoad에서 호출 메서드를 변경하고 결과를 확인해 보면 의도한 대로 표시되는 것을 확인할 수 있다.

func layoutWithInitializer() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let leading = NSLayoutConstraint(item: orangeView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leading, multiplier: 1.0, constant: 0)
	let top = NSLayoutConstraint(item: orangeView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .top, multiplier: 1.0, constant: 0)
	let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: 0)
	let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: 0)
	
	NSLayoutConstraint.activate([top, leading, trailing, bottom])
}
func layoutWithVisualFormatLanguage() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let horizontal = "|[o]|"
	let vertical = "V:|[o]|"
	
	let views: [String: Any] = ["o": orangeView]
	
	let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [], metrics: nil, views: views)
	
	NSLayoutConstraint.activate(horizontalConstraint + verticalConstraint)
}

NSLayoutConstraint와 비교하면 작성할 코드의 양이 줄어들고,
조금 더 직관적이다.

NSLayoutAnchor를 사용해서 제약 추가하기

View는 Anchor 접미어가 붙은 다양한 속성을 제공한다.
이를 사용하면 더 간단하고 직관적으로 제약을 추가할 수 있다.

func layoutWithAnchor() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	orangeView.leadingAnchor.constraint(equalTo: bottomContainer.leadingAnchor).isActive = true
	
}

View의 leadingAnchor에 접근한 뒤, constraint(equalTo:) 메서드에 기준이 될 Anchor을 전달하고,
isActive 속성을 이어서 true로 설정하면 완료된다.

func layoutWithAnchor() {
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	orangeView.leadingAnchor.constraint(equalTo: bottomContainer.leadingAnchor).isActive = true
	orangeView.topAnchor.constraint(equalTo: bottomContainer.topAnchor).isActive = true
	orangeView.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor).isActive = true
	orangeView.bottomAnchor.constraint(equalTo: bottomContainer.bottomAnchor).isActive = true
	
}

다른 제약들도 마찬가지의 방식으로 생성한다.

viewDidLaod에서 호출 메서드를 변경하고 실행해 보면 똑같은 결과를 얻을 수 있다.

width, height, margin

//
//  FixedFrameViewController.swift
//  Constraints with Code Practice
//
//  Created by Martin.Q on 2021/12/17.
//

import UIKit

class FixedFrameViewController: UIViewController {
	@IBOutlet weak var brownView: UIView!
	@IBOutlet weak var orangeView: UIView!
	@IBOutlet weak var bottomContainer: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

extension FixedFrameViewController {
	func layoutWithInitializer() {
		
	}
	func layoutWithVisualFormatLanguage() {
		
	}
	func layoutWithAnchor() {
		
	}
}

사용할 Scene과 코드는 위와 같다.

갈색 View는 margin에 맞게 top 0, leading 0, 높이 100, 너비 100
주황색 View는 margin에 상관없이 trailing 0, bottom 0, 높이 100, 너비 100의 제약을 코드로도 추가하도록 한다.

NSLayoutConstraint로 제약 추가하기

func layoutWithInitializer() {
	brownView.translatesAutoresizingMaskIntoConstraints = false
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let top = NSLayoutConstraint(item: brownView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .topMargin, multiplier: 1.0, constant: 0.0)
	let leading = NSLayoutConstraint(item: brownView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0)
}

top, leading 제약은 attribute2로 .top이 아닌 .topmargin을 전달한 다는 것을 빼고는 이전과 동일하다.

func layoutWithInitializer() {
	brownView.translatesAutoresizingMaskIntoConstraints = false
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let top = NSLayoutConstraint(item: brownView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .topMargin, multiplier: 1.0, constant: 0.0)
	let leading = NSLayoutConstraint(item: brownView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0)
	let width = NSLayoutConstraint(item: brownView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
	let height = NSLayoutConstraint(item: brownView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
}

하지만 대상 다른 View와의 관계없이 설정해야 하는 높이와 너비는 조금 방식이 다르다.
네 번째 파라미터에 nil을 전달하고, 다섯 번째 파라미터에 .notAnAttribute를 전달한다.

func layoutWithInitializer() {
	brownView.translatesAutoresizingMaskIntoConstraints = false
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	let top = NSLayoutConstraint(item: brownView, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .topMargin, multiplier: 1.0, constant: 0.0)
	let leading = NSLayoutConstraint(item: brownView, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0)
	var width = NSLayoutConstraint(item: brownView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
	var height = NSLayoutConstraint(item: brownView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
	
	NSLayoutConstraint.activate([top, leading, width, height])
	
	let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: 20)
	let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: 20)
	width = NSLayoutConstraint(item: orangeView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
	height = NSLayoutConstraint(item: orangeView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
	
	NSLayoutConstraint.activate([trailing, bottom, width, height])
}

같은 방식으로 주황색 View에도 제약을 추가한다.
width와 height는 대상만 다른 같은 제약이므로 수정해 사용할 수 있도록 var로 변경한 뒤 재사용했다.

하지만 의도와는 조금 다른 결과가 시뮬레이터에 표시된다.
표현 범위를 벗어났다.

let trailing = NSLayoutConstraint(item: orangeView, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailing, multiplier: 1.0, constant: -20)
let bottom = NSLayoutConstraint(item: orangeView, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottom, multiplier: 1.0, constant: -20)

단순히 위와 같이 constant 값을 변경해 주거나, 대상의 위치를 반전해 주면 해결된다.

이제는 동일한 방식으로 View들을 표시하게 된다.

Visual Format Language로 제약 추가하기

func layoutWithVisualFormatLanguage() {
	brownView.translatesAutoresizingMaskIntoConstraints = false
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	var horizontal = "|-[b(100)]"
	var vertical = "V:|-[b(100)]"
	
	let views: [String: Any] = ["b": brownView, "o": orangeView]
	
	var horizontalConstant = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	var verticalConstant = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [], metrics: nil, views: views)
	
	NSLayoutConstraint.activate(horizontalConstant + verticalConstant)
	
	horizontal = "[o(100)]-20-|"
	vertical = "V:[o(100)]-20-|"
	
	horizontalConstant = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	verticalConstant = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [], metrics: nil, views: views)
	
	NSLayoutConstraint.activate(horizontalConstant + verticalConstant)
}

코드는 위와 같다.
갈색 View의 top과 leading에만 margin 만큼의 제약이 추가되므로 앞에만 '|'가 붙고, '-'로 기본 margin을 적용함을 나타낸다.
View의 이름 뒤에 '( )'로 너비와 높이의 constant 값이 오며, '=='가 붙어도 되지만 Equal인 경우 생략하기도 한다.
주황색 View도 한쪽에만 margin을 만든다. 기본 값이 아닌 20의 margin이기 때문에 '-20-'의 형태로 작성한다.

마찬가지로 동일하게 표시된다.

NSLayoutAnchor로 제약 추가하기

func layoutWithAnchor() {
	brownView.translatesAutoresizingMaskIntoConstraints = false
	orangeView.translatesAutoresizingMaskIntoConstraints = false
	
	brownView.topAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.topAnchor).isActive = true
	brownView.leadingAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.leadingAnchor).isActive = true
	brownView.widthAnchor.constraint(equalToConstant: 100).isActive = true
	brownView.heightAnchor.constraint(equalToConstant: 100).isActive = true
	
	orangeView.trailingAnchor.constraint(equalTo: bottomContainer.trailingAnchor, constant: -20).isActive = true
	orangeView.bottomAnchor.constraint(equalTo: bottomContainer.bottomAnchor, constant: -20).isActive = true
	orangeView.widthAnchor.constraint(equalToConstant: 100).isActive = true
	orangeView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}

코드는 위와 같다.

View의 높이나 너비는 widthAcnchor의 constraint(equalToConstant:) 메서드로 설정한다.
갈색 View는 상위 View의 margin에 정렬시킨다. 따라서 view의 layoutMarginGuide의 Anchor와 연관 지어야 한다.
주황색 View와 같이 기본적인 margin이 아닌 별도의 margin을 설정하는 경우
constraint(equalTo:constant:) 메서드를 사용해 대상과 값을 전달해 설정한다.

결과는 위와 같다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

166. Constraints with Code #4  (0) 2021.12.20
165. Constraints with Code #3  (0) 2021.12.19
163. Constraints with Code #1  (0) 2021.12.16
162. Auto Layout Practice #2  (0) 2021.12.16
161. Adaptive Layout  (0) 2021.12.16