본문 바로가기

학습 노트/iOS (2021)

166. Constraints with Code #4

Constraints with Code #4

Align Center

사용할 Scene은 위와 같다.
Label을 중앙에 배치하고, 파란색 View를 아래에 배치한다.
단, Label은 Intrinsic Size를 사용하도록 크기 제약은 추가하지 않는다.
Label과 파란색 View의 간격은 10pt, 파란색 View의 높이는 5pt, 파란색 View의 너비는 Label의 너비와 같도록 제약을 추가한다.

추가된 모습은 위와 같다.

NSLayoutConstraint로 제약 추가하기

func layoutWithInitializer() {
	label.translatesAutoresizingMaskIntoConstraints = false
	tealView.translatesAutoresizingMaskIntoConstraints = false
	
	var centerX = NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal, toItem: bottomContainer, attribute: .centerX, multiplier: 1.0, constant: 0)
	let centerY = NSLayoutConstraint(item: label, attribute: .centerY, relatedBy: .equal, toItem: bottomContainer, attribute: .centerY, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([centerX, centerY])
	
	let top = NSLayoutConstraint(item: tealView, attribute: .top, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1.0, constant: 10)
	let width = NSLayoutConstraint(item: tealView, attribute: .width, relatedBy: .equal, toItem: label, attribute: .width, multiplier: 1.0, constant: 0)
	let height = NSLayoutConstraint(item: tealView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 5)
	centerX = NSLayoutConstraint(item: tealView, attribute: .centerX, relatedBy: .equal, toItem: label, attribute: .centerX, multiplier: 1.0, constant: 0)
	
	NSLayoutConstraint.activate([top, width, height, centerX])
}

가운데 정렬 제약은 centerX, centerY를 사용한다.

결과는 위와 같다.

Visual Format Language로 제약 추가하기

Visual Format Language로는 가운데 정렬 제약을 추가하지 못한다.
이번에는 다른 방식과 조합해 제약을 설정한다.

func layoutWithVisualFormatLanguage() {
	label.translatesAutoresizingMaskIntoConstraints = false
	tealView.translatesAutoresizingMaskIntoConstraints = false
	
	label.centerXAnchor.constraint(equalTo: bottomContainer.centerXAnchor).isActive = true
	label.centerYAnchor.constraint(equalTo: bottomContainer.centerYAnchor).isActive = true
}

지금 상황에서 Equal Width의 제약도 추가할 수 없다.
만약 파란색 View가 Lable과 같은 선상의 왼쪽이나 오른쪽에 존재한다면 가능하지만,
지금처럼 위나 아래에 있는 경우 Equal Height는 추가할 수 있으나 Equal Width는 불가능하다.

func layoutWithVisualFormatLanguage() {
	label.translatesAutoresizingMaskIntoConstraints = false
	tealView.translatesAutoresizingMaskIntoConstraints = false
	
	label.centerXAnchor.constraint(equalTo: bottomContainer.centerXAnchor).isActive = true
	label.centerYAnchor.constraint(equalTo: bottomContainer.centerYAnchor).isActive = true
	
	tealView.widthAnchor.constraint(equalTo: label.widthAnchor).isActive = true
	
	let vertical = "V:[label]-10-[teal(==5)]"
	
	let views: [String: Any] = ["label": label, "teal": tealView]
	
	let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [.alignAllCenterX], metrics: nil, views: views)
	
	NSLayoutConstraint.activate(verticalConstraint)
}

Visual Format Language의 문법으로 가운데 정렬을 표현할 수는 없지만,
constraints 메소드의 두 번째 파라미터인 options로 이를 가능하게 할 수 있다.
지금은 alignAllCenterX만 사용했지만,

public struct FormatOptions : OptionSet {
	
	public init(rawValue: UInt)
	
	
	public static var alignAllLeft: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllRight: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllTop: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllBottom: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllLeading: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllTrailing: NSLayoutConstraint.FormatOptions { get }
		
	public static var alignAllCenterX: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllCenterY: NSLayoutConstraint.FormatOptions { get }
	
	public static var alignAllLastBaseline: NSLayoutConstraint.FormatOptions { get }
	
	@available(iOS 8.0, *)
	public static var alignAllFirstBaseline: NSLayoutConstraint.FormatOptions { get }
	
	
	public static var alignmentMask: NSLayoutConstraint.FormatOptions { get }
	
	
	/* choose only one of these three
	*/
	public static var directionLeadingToTrailing: NSLayoutConstraint.FormatOptions { get } // default
	
	public static var directionLeftToRight: NSLayoutConstraint.FormatOptions { get }
	
	public static var directionRightToLeft: NSLayoutConstraint.FormatOptions { get }
	
	
	public static var directionMask: NSLayoutConstraint.FormatOptions { get }
	
	
	/* Valid only for vertical layouts. Between views with text content the value
	will be used to determine the distance from the last baseline of the view above
	to the first baseline of the view below. For views without text content the top
	or bottom edge will be used in lieu of the baseline position.
	The default spacing "]-[" will be determined from the line heights of the fonts
	involved in views with text content, when present.
	*/
	@available(iOS 11.0, *)
	public static var spacingBaselineToBaseline: NSLayoutConstraint.FormatOptions { get }
	
	
	@available(iOS 11.0, *)
	public static var spacingMask: NSLayoutConstraint.FormatOptions { get }
}

위와 같이 많은 옵션이 존재한다.
사용할 때 주의해야 할 점이 몇 가지 있다.

  • 문자열에 하나의 View만 포함되는 경우엔 의미가 없다.
  • 여러 View가 포함되어있더라도 적어도 하나는 위치가 지정돼 있어야 한다.

결과는 이전과 같다.

NSLayoutAnchor로 제약 추가하기

func layoutWithAnchor() {
	label.translatesAutoresizingMaskIntoConstraints = false
	tealView.translatesAutoresizingMaskIntoConstraints = false
	
	label.centerXAnchor.constraint(equalTo: bottomContainer.centerXAnchor).isActive = true
	label.centerYAnchor.constraint(equalTo: bottomContainer.centerYAnchor).isActive = true
	
	tealView.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true
	tealView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10).isActive = true
	tealView.widthAnchor.constraint(equalTo: label.widthAnchor).isActive = true
	tealView.heightAnchor.constraint(equalToConstant: 5).isActive = true
}

코드는 위와 같다.
결과는 이전과 동일하다.

Align Edge

사용할 Scene은 위와 같다.

nameLabel에는 top, leading, trailing에 10pt의 제약을 추가했다.
nameInput에는 top에 10pt 제약을 추가하고, nameLabel의 leading, trailing을 공유한다.
emailLabel에는 top에 10pt 제약을 추가하고, nameLabel의 leading, trailing을 공유한다.
emailInput에는 top에 10pt 제약을 추가하고, nameLabel의 leading, trailing을 공유한다.
button은 bottom에 0pt 제약을 추가하고, nameLabel의 leading, trailing을 공유하며,
top에 10pt Greater than or Equal 제약을 추가한다.

이렇게 UI를 굴비식으로 만들면 이후 nameLabel의 제약만 수정하는 것으로 모든 View들이 변경돼 유지보수가 편해진다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	if #available(iOS 11.0, *) {
		var current = topContainer.directionalLayoutMargins
		current.leading = 30
		topContainer.directionalLayoutMargins = current
		bottomContainer.directionalLayoutMargins = current
	} else {
		var current = topContainer.layoutMargins
		current.left = 30
		topContainer.layoutMargins = current
		bottomContainer.layoutMargins = current
	}
}

View의 Margin을 통해 정렬을 변경하는 것은 iOS의 버전에 따라 구현 방식이 다르다.
iOS 11 이상의 경우 direcionalLayoutMargins를 사용하고,
이하의 경우 layoutMargins를 사용한다.

위와 같이 방식이 다르기때문에 View의 Margin을 변경하는 경우 Interface Builder에서 변경하면 예상과는 다른 결과가 나올 수 있다.
따라서 코드를 사용해 버전별로 분기해 구현하는 것이 좋다.
하지만 코드를 사용하면 실시간으로 결과를 확인할 수 없다는 단점이 있다.
따라서 특정 View를 기준으로 Trailing과 Leading을 추가하고, Constant 값을 변경하는 방식이 더 유연하다.

NSLayoutConstraint로 제약 추가하기

func layoutWithInitializer() {
	nameLabel.translatesAutoresizingMaskIntoConstraints = false
	nameInput.translatesAutoresizingMaskIntoConstraints = false
	mailLabel.translatesAutoresizingMaskIntoConstraints = false
	mailInput.translatesAutoresizingMaskIntoConstraints = false
	confirmBtn.translatesAutoresizingMaskIntoConstraints = false
	
	var top = NSLayoutConstraint(item: nameLabel, attribute: .top, relatedBy: .equal, toItem: bottomContainer, attribute: .topMargin, multiplier: 1.0, constant: 0)
	var leading = NSLayoutConstraint(item: nameLabel, attribute: .leading, relatedBy: .equal, toItem: bottomContainer, attribute: .leadingMargin, multiplier: 1.0, constant: 0)
	var trailing = NSLayoutConstraint(item: nameLabel, attribute: .trailing, relatedBy: .equal, toItem: bottomContainer, attribute: .trailingMargin, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([top, leading, trailing])
	
	top = NSLayoutConstraint(item: nameInput, attribute: .top, relatedBy: .equal, toItem: nameLabel, attribute: .bottom, multiplier: 1.0, constant: 10)
	leading = NSLayoutConstraint(item: nameInput, attribute: .leading, relatedBy: .equal, toItem: nameLabel, attribute: .leading, multiplier: 1.0, constant: 0)
	trailing = NSLayoutConstraint(item: nameInput, attribute: .trailing, relatedBy: .equal, toItem: nameLabel, attribute: .trailing, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([top, leading, trailing])
	
	top = NSLayoutConstraint(item: mailLabel, attribute: .top, relatedBy: .equal, toItem: nameInput, attribute: .bottom, multiplier: 1.0, constant: 10)
	leading = NSLayoutConstraint(item: mailLabel, attribute: .leading, relatedBy: .equal, toItem: nameLabel, attribute: .leading, multiplier: 1.0, constant: 0)
 	trailing = NSLayoutConstraint(item: mailLabel, attribute: .trailing, relatedBy: .equal, toItem: nameLabel, attribute: .trailing, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([top, leading, trailing])
	
	top = NSLayoutConstraint(item: mailInput, attribute: .top, relatedBy: .equal, toItem: mailLabel, attribute: .bottom, multiplier: 1.0, constant: 10)
	leading = NSLayoutConstraint(item: mailInput, attribute: .leading, relatedBy: .equal, toItem: nameLabel, attribute: .leading, multiplier: 1.0, constant: 0)
	trailing = NSLayoutConstraint(item: mailInput, attribute: .trailing, relatedBy: .equal, toItem: nameLabel, attribute: .trailing, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([top, leading, trailing])
	
	top = NSLayoutConstraint(item: confirmBtn, attribute: .top, relatedBy: .greaterThanOrEqual, toItem: mailInput, attribute: .bottom, multiplier: 1.0, constant: 10)
	leading = NSLayoutConstraint(item: confirmBtn, attribute: .leading, relatedBy: .equal, toItem: nameLabel, attribute: .leading, multiplier: 1.0, constant: 0)
	trailing = NSLayoutConstraint(item: confirmBtn, attribute: .trailing, relatedBy: .equal, toItem: nameLabel, attribute: .trailing, multiplier: 1.0, constant: 0)
	let bottom = NSLayoutConstraint(item: confirmBtn, attribute: .bottom, relatedBy: .equal, toItem: bottomContainer, attribute: .bottomMargin, multiplier: 1.0, constant: 0)
	NSLayoutConstraint.activate([top, leading, trailing, bottom])
}

코드는 위와 같다.
마지막 Button에는 equal이 아닌 greaterThanOrEqual을 사용해야 한다.

결과는 사진과 같다.

Visual Format Language로 제약 추가하기

func layoutWithVisualFormatLanguage() {
	nameLabel.translatesAutoresizingMaskIntoConstraints = false
	nameInput.translatesAutoresizingMaskIntoConstraints = false
	mailLabel.translatesAutoresizingMaskIntoConstraints = false
	mailInput.translatesAutoresizingMaskIntoConstraints = false
	confirmBtn.translatesAutoresizingMaskIntoConstraints = false
	
	let views: [String: Any] = ["nl": nameLabel, "ni": nameInput, "ml": mailLabel, "mi": mailInput, "btn": confirmBtn]
	
	let horizontal = "|-[nl]-|"
	let horizontalConstraint = NSLayoutConstraint.constraints(withVisualFormat: horizontal, options: [], metrics: nil, views: views)
	let vertical = "V:|-[nl]-10-[ni]-10-[ml]-10-[mi]-(>=10)-[btn]-|"
	let verticalConstraint = NSLayoutConstraint.constraints(withVisualFormat: vertical, options: [.alignAllLeading, .alignAllTrailing], metrics: nil, views: views)
	NSLayoutConstraint.activate(horizontalConstraint + verticalConstraint)
}

코드는 위와 같다.
greaterThanOrEqual을 표현하기 위해 '-(>=10)-'이 사용되고,
leading과 trailing을 공유하기 위해 options에 alignAllLeading, alignAllTrailing을 전달한다.

결과는 이전과 같다.

NSLayoutAnchor로 제약 추가하기

func layoutWithAnchor() {
	nameLabel.translatesAutoresizingMaskIntoConstraints = false
	nameInput.translatesAutoresizingMaskIntoConstraints = false
	mailLabel.translatesAutoresizingMaskIntoConstraints = false
	mailInput.translatesAutoresizingMaskIntoConstraints = false
	confirmBtn.translatesAutoresizingMaskIntoConstraints = false
	
	nameLabel.topAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.topAnchor, constant: 0).isActive = true
	nameLabel.leadingAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.leadingAnchor, constant: 0).isActive = true
	nameLabel.trailingAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.trailingAnchor, constant: 0).isActive = true
	
	nameInput.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 10).isActive = true
	nameInput.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
	nameInput.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
	
	mailLabel.topAnchor.constraint(equalTo: nameInput.bottomAnchor, constant: 10).isActive = true
	mailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
	mailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
	
	mailInput.topAnchor.constraint(equalTo: mailLabel.bottomAnchor, constant: 10).isActive = true
	mailInput.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
	mailInput.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
	
	confirmBtn.topAnchor.constraint(greaterThanOrEqualTo: mailInput.bottomAnchor, constant: 10).isActive = true
	confirmBtn.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor).isActive = true
	confirmBtn.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor).isActive = true
	confirmBtn.bottomAnchor.constraint(equalTo: bottomContainer.layoutMarginsGuide.bottomAnchor, constant: 0).isActive = true
}

코드는 위와 같고, 결과는 이전과 같다.
confirmBtn의 top에는 constraint(greateThanOrEqual:) 메서드를 사용한다.

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

168. Debugging Auto Layout  (0) 2021.12.21
167. Constraints with Code #5  (0) 2021.12.21
165. Constraints with Code #3  (0) 2021.12.19
164. Constraints with code #2  (0) 2021.12.17
163. Constraints with Code #1  (0) 2021.12.16