아래 레시피는 복잡성을 증가시키는 레이아웃의 생성을 위한 스택뷰를 어떻게 사용할 수 있는지를 보여줍니다. 스택뷰는 빠르고 쉽게 UI를 디자인하는 강력한 도구입니다. 스택뷰의 특성은 어떻게 뷰를 정렬할 것인지에 대해 높은 수준의 컨트롤을 가능하게 합니다. 추가적인 커스텀 제약들을 통해 설정을 확장할 수 있습니다. 그러나 이는 레이아웃의 복잡성을 증가시킵니다.
이런 레이아웃에 대한 소스코드를 확인하려면 Auto Layout Cookbook 프로젝트를 보시기 바랍니다.
Auto Layout Cookbook 프로젝트
https://developer.apple.com/sample-code/xcode/downloads/Auto-Layout-Cookbook.zip
이 레시피는 하나의 수직 스택뷰를 사용하며, 레이블, 이미지뷰, 버튼이 있습니다.
인터페이스 빌더에서 수직 스택뷰를 드래그해서 가져오는 것을 시작으로, flowers 레이블, 이미지뷰, 편집 버튼을 추가하면 됩니다. 그리고 아래 보이는 것처럼 제약을 설정합니다.
Stack View.Leading = Superview.LeadingMargin
Stack View.Trailing = Superview.TrailingMargin
Stack View.Top = Top Layout Guide.Bottom + Standard
Bottom Layout Guide.Top = Stack View.Bottom + Standard
특성 인스펙터에서 아래 스택뷰 특성을 설정하시기 바랍니다.
Stack | Axis | Alignment | Distribution | Spacing |
---|---|---|---|---|
StackView | Vertical | Fill | Fill | 8 |
다음은 이미지뷰에 아래 특성을 설정해야 합니다.
View | Attribute | Value |
---|---|---|
Image View | Image | (an image of flowers) |
Image View | Mode | Aspect Fit |
마지막으로 크기 인스펙터에서 이미지뷰의 content-hugging과 compression-resistance 속성을 설정해야 합니다.
Name | Horizontal hugging | Vertical hugging | Horizontal resistance | Vertical resistence |
---|---|---|---|---|
Image View | 250 | 249 | 750 | 749 |
슈퍼뷰에 스택뷰를 고정시켜야 합니다. 그러면 스택뷰는 다른 제약이 없어도 전체 레이아웃을 관리합니다.
이 레시피에서 스택뷰는 스택뷰의 슈퍼뷰를 작은, 표준 마진으로 채웁니다. 정렬된 뷰들은 스택뷰의 bounds를 채울 수 있도록 크기가 바뀝니다. 수평에서 각 뷰는 스택뷰의 넓이에 일치하도록 확장됩니다. 수직에서 뷰들은 content-hugging과 compression-resistance 속성에 기반해 확장됩니다. 이미지뷰는 항상 사용 가능한 여백을 채울 수 있도록 축소 및 확장되어야 합니다. 그러므로 수직의 content hugging과 compression resistance 속성은 레이블과 버튼의 기본값 우선순위보다 낮아야만 합니다. 이를 통해 스택뷰는 이미지를 왜곡하지 않고 이미지뷰의 크기를 임의로 바꿉니다.
슈퍼뷰에 하나의 뷰를 채우면서 고정시키는 것을 더 알아보려면 Attributes와 Adaptive Single View를 살펴보시기 바랍니다.
두 가지에 대한 내용은 아래에 나옵니다.
이 레시피는 중첩 스택뷰의 다중 레이어로부터 만들어진 복잡한 레이아웃을 보여줍니다. 그러나 이 예제에서 스택뷰는 홀로 원하는 움직임을 생성할 수 없습니다. 대신 추가적인 제약들이 레이아웃을 정제할 수 있도록 요구됩니다.
뷰 계층구조가 만들어진 후, 이 문장 바로 아래 섹션에서 보여지는 제약들을 추가하기 바랍니다.
중첩 스택뷰를 사용할 때, 안쪽에서 바깥쪽으로 사용하는 것이 가장 쉽습니다. 인터페이스 빌더에서 이름 행들을 놓는 것으로 시작합니다. 정확한 기준이 되는 위치에 레이블과 텍스트 필드를 위치시키고, 모두를 선택한 뒤 Edtitor, Embed In, Stack View 메뉴 아이템 순서로 클릭합니다. 이를 통해 행에 대한 수평 스택뷰를 생성합니다.
다음으로 이 행들을 수평으로 위치시키고, 이들을 선택한 뒤, Editor, Embed In, Stack View 메뉴 아이템을 다시 클릭합니다. 이는 행들의 수평 스택을 생성합니다. 계속해서 인터페이스에 보이는 것처럼 빌드합니다.
Root Stack View.Leading = Superview.LeadingMargin
Root Stack View.Trailing = Superview.TrailingMargin
Root Stack View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Root Stack View.Bottom + 20.0
Image View.Height = Image View.Width
First Name Text Field.Width = Middle Name Text Field.Width
First Name Text Field.Width = Last Name Text Field.Width
각각의 스택은 고유한 특성 집합을 갖고 있습니다. 이들은 스택이 컨텐츠를 어떻게 놓을지 정의합니다. 특성 인스펙터에서 아래에 해당하는 특성을 설정합니다.
Stack | Axis | Alignment | Distribution | Spacing |
---|---|---|---|---|
First Name | Horizontal | First Baseline | Fill | 8 |
Middle Name | Horizontal | First Baseline | Fill | 8 |
Last Name | Horizontal | First Baseline | Fill | 8 |
Name Rows | Vertical | Fill | Fill | 8 |
Upper | Horizontal | First Baseline | Fill | 8 |
Button | Horizontal | First Baseline | Fill Equally | 8 |
Root | Vertical | Fill | Fill | 8 |
추가적으로 텍스트뷰에 light gray 배경색 값을 줍니다. 이는 오리엔테이션이 바뀔 때 어떻게 텍스트뷰의 크기가 다시 바뀌는지를 더 쉽게 볼 수 있도록 합니다.
View | Attribute | Value |
---|---|---|
Text View | Background | Light Gray Color |
마지막으로 content-hugging과 compression-resistance 속성은 사용 가능한 공간을 채우기 위해 어떤 뷰들이 확정되어야 하는지를 정의합니다. 사이즈 인스펙터에서 아래 속성들을 설정합니다.
Name | Horizontal hugging | Vertical hugging | Horizontal resistance | Vertical resistance |
---|---|---|---|---|
Image View | 250 | 250 | 48 | 48 |
Text View | 250 | 249 | 250 | 250 |
First, Middle, and Last Name Labels | 251 | 251 | 750 | 750 |
First, Middle, and Last Name Text Fields | 48 | 250 | 749 | 750 |
이 레시피에서 스택뷰들은 대부분의 레이아웃을 관리하기 위해 함께 움직입니다. 그러나 그들 스스로 원하는 모든 움직임들을 생성할 수는 없습니다. 예를 들어 이미지는 이미지뷰의 크기가 바뀔 수 있도록 aspect ratio를 유지해야합니다. 안타깝게도 Simple Stack View에서 사용된 테크닉이 여기에서는 작동하지 않습니다. 레이아웃은 이미지의 trailing과 bottom edge가 가까운 형태로 채워지기 위해 필요하고, 이와 같은 차원에서 Aspect Fit 모드를 사용하는 것은 흰 공간의 여백을 추가할 것입니다. 다행히 이 예제에서 이미지의 aspect ratio는 항상 정사각형입니다. 그렇기 때문에 이미지가 완전히 이미지뷰의 bounds에 채워질 수 있고, 이미지뷰의 aspect ratio가 1:1인 제약을 둡니다.
NOTE
인터페이스 빌더에서 aspect ratio 제약은 뷰의 높이와 넓이 사이의 간단한 제약입니다. 인터페이스 빌더는 몇 가지 방법으로 이 제약에 대해 멀티플라이어를 보여줄 수 있습니다. 보통 aspect ratio 제약에서 비율로 보여줍니다. 그렇기 때문에 View.Width = View.Height 제약이 1:1 aspect ratio로 나타날 것입니다.
추가적으로 모든 텍스트 필드들은 같은 넓이를 가져야 합니다. 안타깝게도 그들은 분리된 스택뷰에 있기 때문에 스택들은 이를 다룰 수 없습니다. 대신 equal width 제약을 추가해야 합니다.
Simple Stack View처럼 몇 가지 content-hugging과 compression-resistance 속성을 수정해야 합니다. 이를 통해 슈퍼클래스의 bounds가 변화할 때, 어떻게 뷰들이 축소 및 확장될 수 있는지를 정의할 수 있습니다.
수직에서 상위 스택과 버튼 스택 사이 공간을 채울 수 있도록 텍스트뷰가 확장되어야 합니다. 그러므로 텍스트뷰의 수직 content hugging이 다른 수직 content hugging 속성에 비해 더 낮은 값이어야 합니다.
수평에서 레이블들은 그들이 갖는 내재된 컨텐트 크기를 가진 상태로 나타나야 합니다. 반면에 텍스트 필드들은 여백을 채우기 위해 크기가 바뀌어야 합니다. 기본값 content-hugging, compression-resistance 속성은 레이블들에서 잘 작동합니다. 인터페이스 빌더는 이미 content-hugging을 251로 설정하고 있고, 이를 통해 텍스트 필드보다 더 높은 값이도록 합니다. 그러나 아직 텍스트 필드의 수평 content-hugging과 수평 compression-resistance 모두가 더 낮아야 합니다.
이미지뷰는 스택이 포함하고 있는 이름 행들과 같은 높이이도록 축소되어야 합니다. 그러나 스택뷰들은 그들의 컨텐트를 약하게 끌어안고 있습니다. 이는 이미지뷰의 수직 compression-resistance가 매우 낮아야 함을 의미하고, 이를 통해 이미지뷰가 스택뷰의 확장을 하는 것이 아니라 축소됩니다. 추가적으로 이미지뷰의 aspect ratio 제약은 레이아웃을 복잡하게 만듭니다. 왜냐하면 이는 수직과 수평 제약들이 상호작용할 수 있도록 해주기 때문입니다. 이는 텍스트 필드들의 수평 content hugging 또한 매우 낮아야 함을 의미합니다. 혹은 텍스트 필드들은 이미지뷰가 축소되는 것을 방지합니다. 우선순위 값을 48 혹은 더 낮게 설정해야 합니다.
이 레시피는 런타임에 스택으로부터 아이템들을 동적으로 추가하고 제거하는 것을 설명합니다. 모든 스택에 대한 모든 변화는 애니메이션 동작을 갖습니다. 추가적으로 스택뷰는 스크롤뷰 내에 위치합니다. 이를 통해 만약 스크린에 채워지기에 너무 길다면, 스크롤이 가능하도록 해줍니다.
NOTE
이 레시피는 스택뷰들을 동적으로 작동하는 것을 설명하기 위한 의도만을 갖고 있고, 스크롤뷰 내부에서 스택뷰가 작동하는 것만을 설명하기 위해 의도된 것입니다. 실제 앱에서 이 레시피의 움직임들은 UITableView 클래스를 사용해 구현되어야 합니다. 일반적으로 간단하게 구현할 수 있는 scratch-built 테이블뷰의 복제인 동적 스택뷰를 사용하지 말아야 합니다. 대신 다른 기술을 사용할 수 없는 동적 UI를 생성하려면 이 방식을 사용하시기 바랍니다.
초기 UI는 매우 간단합니다. 스크롤뷰를 씬에 놓고, 씬을 채울 수 있도록 크기를 조절합니다. 그리고 스택뷰를 스크롤뷰 안에 넣은 뒤, 추가 아이템 버튼을 스택뷰에 놓습니다. 그후 아래 제약들을 설정합니다.
Scroll View.Leading = Superview.LeadingMargin
Scroll View.Trailing = Superview.TrailingMargin
Scroll View.Top = Superview.TopMargin
Bottom Layout Guide.Top = Scroll View.Bottom + 20.0
Stack View.Leading = Scroll View.Leading
Stack View.Trailing = Scroll View.Trailing
Stack View.Top = Scroll View.Top
Stack View.Bottom = Scroll View.Bottom
Stack View.Width = Scroll View.Width
특성 인스펙터에서 아래와 같은 스택뷰 특성들을 설정합니다.
Stack | Axis | Alignment | Distribution | Spacing |
---|---|---|---|---|
Stack View | Vertical | Fill | Equal Spacing | 0 |
이 레시피는 스택뷰로부터 아이템들을 추가하거나 제거하기 위한 약간의 코드가 필요합니다. 스크롤뷰와 스택뷰 모두에서 아웃렛을 갖는 씬에 대한 커스텀 뷰 컨트롤러를 생성합니다.
class DynamicStackViewController: UIViewController {
@IBOutlet weak private var scrollView: UIScrollView!
@IBOutlet weak private var stackView: UIStackView!
// Method implementations will go here...
}
다음으로 viewDidLoad
메소드를 오버라이드해서 스크롤뷰의 초기 위치를 설정할 수 있도록 합니다. 스크롤뷰의 컨텐트가 status bar 아래에서 시작되어야 합니다.
override func viewDidLoad() {
super.viewDidLoad()
// setup scrollview
let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
추가 아이템 버튼을 위한 액션 메소드를 추가합니다.
// MARK: Action Methods
@IBAction func addEntry(sender: AnyObject) {
let stack = stackView
let index = stack.arrangedSubviews.count - 1
let addView = stack.arrangedSubviews[index]
let scroll = scrollView
let offset = CGPoint(x: scroll.contentOffset.x,
y: scroll.contentOffset.y + addView.frame.size.height)
let newView = createEntry()
newView.hidden = true
stack.insertArrangedSubview(newView, atIndex: index)
UIView.animateWithDuration(0.25) { () -> Void in
newView.hidden = false
scroll.contentOffset = offset
}
}
이 메소드는 스크롤뷰에 대한 새로운 오프셋 계산하고, 새로운 엔트리뷰를 생성합니다. 엔트리뷰는 숨겨져 있고, 스택에 추가됩니다. 숨겨진 뷰들은 나타나는 모양이나 스택의 레이아웃에 영향을 미치지 못합니다. 그래서 스택의 나타나는 것은 변화가 없는 상태로 나타납니다. 그리고 애니메이션 블록에서 뷰가 나타나고, 뷰의 모양이 애니메이션되면서 스크롤 오프셋 업데이트됩니다.
엔트리들을 제거하기 위한 유사한 메소드를 추가해야 합니다. 그러나 addEntry
메소드와 다르게 이 메소드는 인터페이스 빌더에 있는 컨트롤과 연결되지 않습니다. 대신 뷰가 생성될 때 앱은 각각의 엔트리뷰를 코드로 연결시킵니다.
func deleteStackView(sender: UIButton) {
if let view = sender.superview {
UIView.animateWithDuration(0.25, animations: { () -> Void in
view.hidden = true
}, completion: { (success) -> Void in
view.removeFromSuperview()
})
}
}
이 메소드는 애니메이션 블록에서 뷰를 숨깁니다. 애니메이션이 완료된 후 뷰 계층구조로부터 뷰를 제거합니다. 정렬된 스택의 리스트로부터 뷰를 자동으로 제거합니다.
엔트리뷰는 모든 뷰가 가능할지라도 이 예제는 date 레이블을 포함하는 스택뷰를 사용하고 있고, 무작위 헥스 스트링을 포함하는 레이블과 삭제 버튼을 갖고 있습니다.
// MARK: - Private Methods
private func createEntry() -> UIView {
let date = NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .ShortStyle, timeStyle: .NoStyle)
let number = "\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())"
let stack = UIStackView()
stack.axis = .Horizontal
stack.alignment = .FirstBaseline
stack.distribution = .Fill
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
let deleteButton = UIButton(type: .RoundedRect)
deleteButton.setTitle("Delete", forState: .Normal)
deleteButton.addTarget(self, action: "deleteStackView:", forControlEvents: .TouchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
private func randomHexQuad() -> String {
return NSString(format: "%X%X%X%X",
arc4random() % 16,
arc4random() % 16,
arc4random() % 16,
arc4random() % 16
) as String
}
}
뷰들은 런타임 동안 스택뷰로부터 추가 혹은 제거될 수 있습니다. 스택의 레이아웃은 정렬된 뷰들의 어레이에서 변화에 상응하도록 자동으로 적응합니다. 그러나 몇 가지 기억해야 할 중요한 점들이 있습니다.
숨겨진 뷰들은 정렬된 뷰들의 스택 어레이 내부에 여전히 존재합니다. 그러나 화면에 보여지지 않고 다른 정렬된 뷰들의 레이아웃에 영향을 미치지 못합니다.
정렬된 뷰의 스택 어레이에 뷰를 추가하는 것은 자동으로 뷰 계층구조에 추가됩니다.
정렬된 뷰의 스택 어레이로부터 하나의 뷰를 제거하는 것은 뷰 계층구조로부터 자동으로 제거되지 않습니다. 그러나 뷰 계층구조로부터 뷰를 제거하는 것은 정렬된 뷰의 어레이로부터 뷰를 제거합니다.
iOS에서 뷰의 숨겨진 속성은 애니메이션할 수 없습니다. 그러나 이 속성은 스택의 정렬된 뷰 어레이에 놓여지는 순간 애니며이션할 수 있게 됩니다. 실제 애니메이션은 스택에 의해 다뤄질 수 있고, 뷰에 의해서 다뤄지지 않습니다. hidden 속성을 사용해 뷰의 추가나 제거를 애니메이션할 수 있습니다.
이 레시피는 스크롤뷰에 대한 오토 레이아웃 사용의 아이디어를 소개하고 있습니다. 스택뷰와 스크롤뷰 사이의 제약들은 스크롤뷰의 컨텐트 영역 크기를 설정합니다. 같은 넓이 제약은 명시적으로 스크롤뷰에 수평으로 채워질 수 있도록 스택(그러므로 컨텐트 크기)을 설정합니다. 수직에서 컨텐트 크기는 스택의 fitting size에 기반합니다. 스택뷰는 더 많은 엔트리를 추가하면 더 커집니다. 스크롤링은 화면을 채우기에 너무 많은 컨텐트가 존재하는 순간부터 가능해집니다.
더 많은 정보가 필요하다면 Working with Scroll Views를 보시기 바랍니다.
Working with Scroll Views는 다음 글에 나옵니다.
아래 레시피들은 상대적으로 간단한 제약들을 사용하는 것을 설명합니다. 이 예제를 기본 구성요소로 사용하면서 더 크고 복잡한 레이아웃을 생성할 수 있습니다.
레시피의 소스코드를 보려면 Auto Layout Cookbook 프로젝트를 보시기 바랍니다.
Auto Layout Cookbook 프로젝트
https://developer.apple.com/sample-code/xcode/downloads/Auto-Layout-Cookbook.zip
이 레시피는 네 개의 edge에 고정값 마진을 두면서 슈퍼뷰를 채우는 하나의 빨간색 뷰를 놓습니다.
인터페이스 빌더에서 씬에 뷰를 드래그한 후 씬을 채울 수 있도록 크기를 바꿉니다. 슈퍼뷰의 edge가 기준이 되도록 정확한 위치를 선택하기 위해 인터페이스 빌더의 가이드라인을 사용합니다.
NOTE
완벽한 픽셀 위치에 뷰를 놓는 것에 대해 걱정할 필요는 없습니다. 제약를 설정한 후 시스템은 정확한 크기와 위치를 계산할 것입니다.
뷰를 위치시킨 뒤 아래 제약들을 설정합니다.
Red View.Leading = Superview.LeadingMargin
Red View.Trailing = Superview.TrailingMargin
Red View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Red View.Bottom + 20.0
뷰가 빨간색 배경이 될 수 잇도록 하고 아래 특성들을 특성 인스펙터에서 설정합니다.
View | Attribute | Value |
---|---|---|
Red View | Background | Red |
이 레시피의 제약들은 빨간색 뷰에 대해 슈퍼뷰의 edge로부터 고정된 거리를 유지하도록 합니다. leading과 trailing edge는 슈퍼뷰의 마진에 뷰를 고정합니다. top과 bottom은 top과 bottom 레이아웃 가이드에 뷰를 고정시킵니다.
NOTE
시스템은 적합한 leading과 trailing 마진을 가질 수 있도록 루트 뷰의 마진(16 혹은 20포인트이며 기기에 따라 달라짐)을 자동으로 설정합니다. 그리고 top과 bottom 마진은 0포인트입니다. 이를 통해서 모든 컨트롤 바(status bar, navigation bar, tab bar, tool bar 등)에 대해 쉽게 이동시키도록 합니다.
그러나 이 레시피는 바 아래에 컨텐트를 위치시킬 필요가 있습니다. 간단하게 빨간색 뷰의 leading과 trailing edge를 슈퍼뷰의 leading과 trailing 마진에 고정시킬 수 있습니다. 그러나 레이아웃 가이드 기준으로 top과 bottom은 직접 설정해줘야만 합니다.
기본값으로 뷰와 슈퍼뷰의 edge 사이에 대해 인터페이스 빌더의 표준 여백은 20.0 포인트입니다. 그리고 다른 뷰 사이 여백은 8.0 포인트입니다. 이는 빨간색 뷰의 top과 status bar의 bottom 사이를 8.0 포인트 여백으로 해야 함을 암시합니다. 그러나 status bar는 아이폰이 landscape 오리엔테이션일 때 사라집니다. 그리고 status bar 없이 8.0 포인트 여백은 너무 좁습니다.
항상 앱에서 최적화되어 작동할 수 있도록 레이아웃을 선택해야 합니다. 이 레시피는 top과 bottom에 고정값인 20.0 포인트 마진을 사용합니다. 이는 가능한 간단한 제약 로직을 유지하고, 모든 오리엔테이션에서 합리적인 것으로 보입니다. 다른 레이아웃은 고정값 8.0 포인트 마진에서 더 잘 작동할 것입니다.
만약 바가 있거나 혹은 없는 경우에 자동으로 적응하는 레이아웃을 원한다면 Adaptive Single View를 보시기 바랍니다.
Adaptive Single View는 바로 아래에 나옵니다.
이 레시피는 슈퍼뷰의 네 가지 edge에 마진을 두는 슈퍼뷰를 채우는 파란색 하나의 뷰를 위치시킵니다. 그러나 Simple Single View 레시피와 다르게 이 레시피의 top 마진은 뷰의 컨텍스트에 기반해 적응합니다. 만약 status bar가 있다면, 뷰는 status bar 아래에 표준 여백(8.0 포인트)을 갖고 위치합니다. 만약 status bar가 없다면 뷰는 슈퍼뷰의 edge 아래에 20.0 포인트 간격을 두고 놓여집니다.
Simple 뷰와 Adaptive 뷰를 아래처럼 볼 수 있습니다.
인터페이스 빌더에서 씬에 뷰를 드래그합니다. 그리고 씬을 채울 수 있도록 크기를 조절합니다. 가이드라인에 edge들을 정렬하는 것을 통해 크기를 조절합니다. 그리고 아래와 같은 제약을 설정합니다.
Blue View.Leading = Superview.LeadingMargin
Blue View.Trailing = Superview.TrailingMargin
Blue View.Top = Top Layout Guide.Bottom + Standard (Priority 750)
Blue View.Top >= Superview.Top + 20.0
Bottom Layout Guide.Top = Blue View.Bottom + Standard (Priority 750)
Superview.Bottom >= Blue View.Bottom + 20.0
뷰의 배경 색을 파란색으로 하기 위해 특성 인스펙터에서 아래처럼 설정합니다.
View | Attribute | Value |
---|---|---|
Blue View | Background | Blue |
이 레시피는 파란색 뷰의 top과 bottom 모두에 적응형 마진을 생성합니다. 바가 있는 경우 뷰의 edge는 바로부터 8.0 포인트 떨어진 곳에 있습니다. 만약 bar가 없다면, edge는 슈퍼뷰의 edge로부터 20.0 포인트 떨어진 곳에 있습니다.
이 레시피는 컨텐츠를 정확하게 위치시킬 수 있도록 레이아웃 가이드를 사용합니다. 시스템은 바의 존재 여부와 바의 크기에 기반해 이 가이드들의 위치를 설정합니다. top 레이아웃 가이드는 top 바(에를 들어 status bar, navigation bar)의 bottom edge에 따라 위치합니다. bottom 레이아웃 가이드는 bottom 바(예를 들어 tab bar)의 top edge에 따라 위치합니다. 바가 없다면 시스템은 레이아웃 가이드를 슈퍼뷰의 edge에 따라 위치시킵니다.
레시피는 적응형 동작을 빌드하기 위해 한 쌍의 제약을 사용합니다. 첫 번째 제약은 greater-than-or-equal 제약입니다. 이 제약은 파란색 뷰의 edge가 슈퍼뷰의 edge로부터 적어도 20.0 포인트 떨어질 수 있도록 보장합니다. 20.0 포인트 마진을 최소로 정의하는 것입니다.
다음으로 선택적 제약은 레이아웃 가이드에 상응하는 것으로부터 뷰가 8.0 포인트 떨어져 있기를 시도합니다. 선택적인 제약이기 때문에 시스템이 제약을 충족시킬 수 없다면, 가능한 가깝게 놓으려고 할 것이고, 제약은 파란색 뷰의 edge를 레이아웃 가이드에 끌어당기면서 스프링 같은 역할을 할 것입니다.
만약 시스템이 바를 보여주지 않고 있다면 레이아웃 가이드는 슈퍼뷰의 edge와 동일합니다. 파란색 뷰의 edge는 슈퍼뷰의 edge로부터 8.0 포인트, 20.0 포인트(혹은 그 이상)가 될 수 없습니다. 그러므로 시스템은 선택적 제약을 만족시키지 못합니다. 아직까지 20.0 포인트를 최소로하는 마진을 설정하면서, 가능한 가깝게 만드려고 할 것입니다.
만약 바가 존재한다면, 모든 제약들이 충족될 수 있습니다. 모든 바는 최소 20.0 포인트보다 넓습니다. 그렇기 때문에 만약 시스템이 바의 edge로부터 파란색 뷰의 edge를 8.0 포인트 떨어뜨려 놓는다면, 슈퍼뷰의 edge로부터 20.0 포인트가 넘을 수 있다는 것을 보장합니다.
다른 방향으로 밀어내는 역할을 하는 한 쌍의 제약을 사용하는 이 테크닉은 적응형 레이아웃을 생성하기 위해 흔히 사용됩니다. Views with Intrinsic Content Size부분에서 content-hugging, compression-resistance 속성을 살펴볼 때 이 테크닉을 다시 보게 될 것입니다.
Views with Instrinsic Content Size는 이 글의 아래에 나옵니다.
이 레피시는 두 뷰를 양 옆에 놓습니다. 두 뷰는 항상 같은 넓이를 갖고 있으며, 슈퍼뷰의 bounds가 변화하는 것과 상관없이 동작합니다. 두 뷰는 슈퍼뷰를 채우고 있습니다. 모든 측면에 고정된 마진을 갖고 있고 표준 여백 마진도 갖고 있습니다.
인터페이스 빌더에서 두 뷰를 드래그하고 씬을 채울 수 있도록 위치시킵니다. 씬에 있는 객체들 사이에 정확한 여백을 갖도록 설정하기 위해 가이드라인을 사용해서 그렇게 할 수 있습니다.
두 뷰의 넓이를 같도록 만드는 것에 대해서 걱정할 필요는 없습니다. 대략적으로 위치를 잡아주고 제약들이 이를 이뤄주도록 해야 합니다.
뷰를 넣은 후 아래 제약들을 설정합니다.
Yellow View.Leading = Superview.LeadingMargin
Green View.Leading = Yellow View.Trailing + Standard
Green View.Trailing = Superview.TrailingMargin
Yellow View.Top = Top Layout Guide.Bottom + 20.0
Green View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Yellow View.Bottom + 20.0
Bottom Layout Guide.Top = Green View.Bottom + 20.0
Yellow View.Width = Green View.Width
특성 인스펙터에서 뷰의 배경색을 설정합니다.
View | Attribute | Value |
---|---|---|
Yellow View | Background | Yellow |
Green View | Background | Green |
이 레이아웃은 두 뷰에 대해 top과 bottom 마진을 명시적으로 정의합니다. 마진이 같은 한 두 뷰는 암묵적으로 같은 높이를 갖습니다. 그러나 이 레이아웃이 가능한 유일한 솔루션은 아닙니다. 초록색 뷰의 top과 bottom을 슈퍼뷰에 고정시키는 것 대신 노란색 뷰의 top과 bottom을 같도록 설정할 수 있습니다. top과 bottom edge를 명시적으로 정렬시키는 것은 수직 레이아웃에서 두 뷰가 같도록 해줍니다.
이처럼 상대적으로 간단한 레이아웃은 몇 가지 다른 제약들을 사용하면서 생성될 수 있습니다. 몇 가지는 다른 방법들보다 명백히 더 나은 방법일 것입니다. 하지만 대부분은 대략적으로 같을 것입니다. 각각의 접근법은 장단점을 갖고 있습니다. 이 레시피의 접근법은 두 가지 장점을 갖습니다. 첫 번째, 이해하기가 쉽습니다. 두 번째, 레이아웃은 두 뷰 중 하나를 제거한다고 하더라도 대부분 손대지 않은 것처럼 남아있을 것입니다.
뷰 계층구조로부터 하나의 뷰를 제거하는 것은 그 뷰에 있었던 모든 제약을 제거합니다. 만약 노란색 뷰를 제거한다면 1, 2, 4, 6, 8번 제약들이 모두 제거된다는 것을 의미합니다. 그러나 초록색 뷰를 잡고 있는 세 가지 제약은 여전히 남겨집니다. 그러면 초록색 뷰의 leading edge 위치를 정의하는 하나의 제약만 추가하면 레이아웃이 고정됩니다.
주요 단점은 직접 모든 top, bottom 제약들이 같도록 설정해줄 필요가 있다는 것입니다. 하나의 상수를 변화시켜보면, 두 뷰는 명백하게 고르지 않은 모습일 것입니다. 제약들을 생성하기 위해 인터페이스 빌더의 핀 도구를 사용할 때 일관적인 상수들을 설정하는 것이 상대적으로 쉽습니다. 제약들을 생성하기 위해 드래그 드랍을 사용하면 다소 어려워질 것입니다.
동일하게 유효한 제약의 집합인 여럿을 다룰 때, 집합 하나를 선택하면 이해하기가 가장 쉽고, 레이아웃의 컨텍스트를 유지하기에도 가장 쉬울 것입니다. 예를 들어 다른 크기를 갖는 몇 가지 뷰들을 중앙에 정렬한다면, 뷰들의 Center X 특성에 제약을 두는 것이 가장 쉬울 것입니다. 다른 레이아웃은 뷰의 edge들, 혹은 그들의 높이와 넓이를 추론하는 것이 더 쉬울 수 있습니다.
최적의 제약 집합을 선택하는 것은 Creating Nonambiguous, Satisfiable Layouts를 살펴보시기 바랍니다.
Creating Nonambiguous, Satisfiable Layouts는 앞의 글인 Getting Started에서 확인할 수 있습니다. 아래에 링크를 남기겠습니다.
https://velog.io/@panther222128/Getting-Started-1by05tb6
이 레시피는 Two Equal-Width Views 레시피와 매우 유사하지만 한 가지 차이를 갖습니다. 이 레시피에서 오렌지 뷰는 항상 보라색 뷰보다 넓이가 두 배입니다.
대략적인 위치에 두 뷰를 드래그해서 위치시킵니다. 그리고 아래 보이는 제약들을 설정합니다.
Purple View.Leading = Superview.LeadingMargin
Orange View.Leading = Purple View.Trailing + Standard
Orange View.Trailing = Superview.TrailingMargin
Purple View.Top = Top Layout Guide.Bottom + 20.0
Orange View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Purple View.Bottom + 20.0
Bottom Layout Guide.Top = Orange View.Bottom + 20.0
Orange View.Width = 2.0 x Purple View.Width
뷰의 배경색상을 특성 인스펙터에서 설정합니다.
View | Attribute | Value |
---|---|---|
Purple View | Background | Purple |
Orange View | Background | Orange |
이 레시피는 넓이 제약에 멀티플라이어를 사용합니다. 멀티플라이어는 뷰의 높이 혹은 넓이에만 사용될 수 있습니다. 이를 통해 두 가지 다른 뷰의 상대적인 크기를 설정할 수 있습니다. 다른 방법으로 뷰가 갖는 높이와 넓이 사이에서 뷰의 aspect ratio를 구체화해 제약을 설정할 수 있습니다.
인터페이스 빌더는 몇 가지 다른 형식으로 멀티플라이어를 구체화할 수 있도록 해줍니다. decimal number (2.0), percentage (200%), fraction (2/1), ratio (2:1)으로 멀티플라이어를 표현할 수 있습니다.
이 레시피는 대부분 Two Different-Width Views와 일치합니다. 그러나 이 레시피에서는 뷰 넓이들의 움직임을 더 복잡하게 정의하기 위한 한 쌍의 제약을 사용합니다. 이 레시피에서 시스템은 빨간색 뷰를 파란색 뷰만큼 두 배 넓도록 만들 것이고, 파란색 뷰는 최소 150.0 포인트의 최소 넓이를 갖습니다. 이렇게 함으로써 portrait에서 아이폰에 대해 뷰들은 거의 같은 넓이를 갖고, landscape에서 뷰들은 더 커지며 빨간색 뷰는 파란색보다 두 배 넓어집니다.
캔버스에 두 뷰를 위치시키고 아래에 보이는 제약들을 설정합니다.
Blue View.Leading = Superview.LeadingMargin
Red View.Leading = Blue View.Trailing + Standard
Red View.Trailing = Superview.TrailingMargin
Blue View.Top = Top Layout Guide.Bottom + 20.0
Red View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Blue View.Bottom + 20.0
Bottom Layout Guide.Top = Red View.Bottom + 20.0
Red View.Width = 2.0 x Blue View.Width (Priority 750)
Blue View.Width >= 150.0
특성 인스펙터에서 뷰들의 배경색을 설정합니다.
View | Attribute | Value |
---|---|---|
Blue View | Background | Blue |
Red View | Background | Red |
이 레시피는 두 뷰의 넓이들을 제어하기 위해 한 쌍의 제약을 사용합니다. 선택적인 proportional 넓이 제약은 빨간색 뷰가 파란색 뷰에 비해 두 배 넓게 할 수 있도록 뷰들을 당깁니다. 그러나 greater-than-or-equal 제약은 파란색 뷰의 넓이에 대해 최소 넓이 값을 상수로 둡니다.
슈퍼뷰의 leading과 trailing 마진 사이 거리가 458.0 포인트(150.0 + 300.0 + 8.0)이거나 더 클 때, 빨간색 뷰는 파란색 뷰보다 두 배가 큽니다. 만약 마진 사이 거리가 줄어들면, 파란색 뷰는 150,0 포인트 넓이로 설정되고, 빨간색 뷰는 남은 공간을 채웁니다(뷰 사이 마진이 8.0 포인트).
아직 Adaptive Single View 레시피에서 소개될 패턴에 대한 다양한 변형이 있다는 것을 인식하고 있어야 합니다.
이 디자인을 제약을 추가해 확장할 수 있습니다. 예를 들어 세 가지 제약을 사용하는 것입니다. 반드시 요구되는 제약인 빨간색 뷰의 최소 넓이, 높은 우선순위를 갖는 파란색 뷰의 최소 넓이의 선택적 제약, 뷰 사이에서 크기 비율을 설정하기 위한 낮은 우선순위의 선택적 제약이 세 가지입니다.
아래 레시피는 내재된 컨텐트 크기를 갖는 뷰들을 다루는 것에 대해 설명합니다. 일반적으로 내재된 컨텐트 크기는 필요한 제약의 수를 감소시켜 레이아웃을 간단하게 설정할 수 있도록 합니다. 그러나 내재된 컨텐트 크기는 보통 복잡성이 추가되는 뷰의 content-hugging과 compression-resistance 속성을 설정하기를 요구합니다.
이 레시피에 대한 소스코드를 보려면 Auto Layout Cookbook 프로젝트를 살펴보시기 바랍니다.
Auto Layout Cookbook 프로젝트
https://developer.apple.com/sample-code/xcode/downloads/Auto-Layout-Cookbook.zip
이 레시피는 간단한 레이블과 텍스트 필드를 다루는 것을 설명합니다. 이 예제에서 레이블의 넓이는 텍스트 속성의 크기에 기반하고, 텍스트 필드는 남은 여백을 채우기 위해 확장되거나 축소됩니다.
이 레시피는 뷰의 내재된 컨텐트 크기를 사용하기 때문에 레이아웃을 구체화하기 위한 다섯 가지 제약만 필요합니다. 그러나 정확하게 크기가 조절될 수 있도록 정확한 content-hugging과 compression-resistance 속성을 설정해줘야 합니다.
내재된 컨텐트 크기와 content-hugging과 compression-resistance 속성에 대해 더 알아보려면 Instrinsic Content Size를 살펴보시기 바랍니다.
Instrinsic Content Size는 앞 글에 나옵니다.
인터페이스 빌더에서 레이블과 텍스트 필드를 드래그합니다. 레이블의 텍스트와 텍스트 필드의 플레이스홀더를 설정하고, 아래 보이는 제약들을 설정합니다.
Name Label.Leading = Superview.LeadingMargin
Name Text Field.Trailing = Superview.TrailingMargin
Name Text Field.Leading = Name Label.Trailing + Standard
Name Text Field.Top = Top Layout Guide.Bottom + 20.0
Name label.Baseline = Name Text Field.Baseline
활용 가능한 공간을 채우기 위해 텍스트 필드가 뻗어나가려면, 레이블의 content-hugging보다 텍스트 필드의 content-hugging 우선순위가 더 낮아야 합니다. 기본값으로 인터페이스 빌더는 레이블의 content-hugging을 251으로 설정해야 하고, 텍스트 필드는 250으로 설정해야 합니다. 사이즈 인스펙터에서 이를 확인할 수 있습니다.
Name | Horizontal hugging | Vertical hugging | Horizontal resistance | Vertical resistance |
---|---|---|---|---|
Name Label | 251 | 251 | 750 | 750 |
Name Text Field | 250 | 250 | 750 | 750 |
이 레이아웃은 수직 레이아웃을 정의하기 위해 두 개의 제약(4와 5)만을 사용한다는 것을 알아야 합니다. 그리고 수평 레이아웃을 정의하기 위해 제약(1, 2, 3)은 세 가지를 사용합니다. Creating Nonambiguous, Satisfiable Layouts의 rule of thumb은 하나의 뷰에 두 개의 수평 제약과 두 개의 수직 제약이 필요하다고 했었습니다. 그러나 레이블과 텍스트 필드의 내재된 컨텐트 크기는 높이와 레이블의 넓이를 제공해 필요한 제약의 수가 줄어듭니다.
이 레이아웃은 레이블 텍스트보다 텍스트 필드가 더 크다는 가정을 간단하게 만들어주고, top 레이아웃 가이드로부터 떨어진 거리를 정의하기 위해 텍스트 필드의 높이를 사용합니다. 레이블과 텍스트 필드 모두 텍스트를 표시하기 위해 사용되기 때문에 레시피는 텍스트의 베이스라인을 사용하면서 이들을 정렬시킵니다.
수평에서 이용 가능한 크기를 채우기 위해 확장되어야 하는 뷰가 어떤 뷰인지를 정의해줄 필요가 있습니다. 뷰의 content-hugging, compression-resistance 속성을 수정해 이를 구현할 수 있습니다. 이 예제에서 인터페이스 빌더는 이름 레이블의 수평, 수직 hugging 우선순위를 251로 미리 설정해줘야 합니다. 텍스트 필드의 기본값인 250보다 더 크기 때문에 텍스트 필드는 남은 공간을 채우기 위해 확장됩니다.
NOTE
레이아웃은 컨트롤 하기에 매우 작은 곳에 보여진다면, compression-resistance 값들을 수정할 필요가 있습니다. compression-resistance는 충분한 공간이 없을 때 어떤 뷰가 잘려야 하는지를 정의합니다.
이 예제에서 compression-resistance를 수정하는 것은 이 글을 읽고 계신 분들의 연습으로 남겨두겠습니다. 만약 이름 레이블의 텍스트 혹은 폰트가 충분히 크다면, 모호한 레이아웃이 생성되도록 충분한 여백이 있지 않을 것입니다. 시스템은 이를 깨기 위해 하나의 제약을 선택하고, 텍스트 필드 혹은 레이블이 잘리도록 합니다.
남은 여백에 대해 지나치게 큰 레이아웃을 만들고 싶지는 않을 것입니다. 필요한 경우 대안이 되는 레이아웃인 컴팩트 사이즈 클래스를 사용할 수 있습니다. 그러나 여러 언어와 동적 타입을 지원하는 뷰를 디자인할 때, 행의 길이가 얼마나 될지를 정확하게 예측하는 것은 어렵습니다. compression resistance를 수정하는 것이 안전하고 좋은 방법입니다.
Simple Label and Text Field 레시피는 텍스트 필드가 이름 레이블보다 더 크다는 것을 가정하는 것으로 레이아웃 로직을 간단하게 만들었습니다. 그러나 이것이 항상 맞는 것은 아닙니다. 만약 레이블의 폰트 크기를 충분히 증가시키면, 텍스트 필드를 넘어 확장할 것입니다.
이 레시피는 런타임에서 가장 큰 컨트롤을 기반으로 컨트롤들의 수직 여백을 동적으로 설정합니다. 레귤러 시스템 폰트로 이 레시피는 Simple Label and Text Field 레시피(스크린샷을 보시기 바랍니다)와 동일하게 나타납니다. 그러나 레이블의 폰트 크기를 36.0 포인트로 증가시키면 레이아웃의 수직 여백이 레이블의 top으로부터 계산됩니다.
이는 다소 인위적인 예제입니다. 결국 레이블의 폰트 크기를 증가시키면, 텍스트 필드의 폰트 크기도 증가시킬 것입니다. 그러나 아이폰의 접근성 설정에서 큰 폰트를 사용할 수 있는 상태에서 이 테크닉은 동적 타입과 고정 크기의 컨트롤(이미지와 같은)을 사용할 때 유용합니다.
Simple Label and Text Field에서 했었던 것처럼 뷰 계층구조를 설정하고, 다소 복잡한 제약들을 사용합니다.
Name Label.Leading = Superview.LeadingMargin
Name Text Field.Trailing = Superview.TrailingMargin
Name Text Field.Leading = Name Label.Trailing + Standard
Name Label.Top >= Top Layout Guide.Bottom + 20.0
Name Label.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
Name Text Field.Top >= Top Layout Guide.Bottom + 20.0
Name Text Field.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
Name label.Baseline = Name Text Field.Baseline
활용 가능한 여백을 채우기 위해 텍스트 필드가 확장하려면, 텍스트 필드의 content-hugging이 레이블의 content-hugging보다 더 낮게 설정되어야 합니다. 기본값으로 인터페이스 빌더는 레이블의 content-hugging을 251로 설정해야 하고, 텍스트 필든는 250으로 설정해야 합니다. 이를 사이즈 인스펙터에서 확인할 수 있습니다.
Name | Horizontal hugging | Vertical hugging | Horizontal resistance | Vertical resistance |
---|---|---|---|---|
Name Label | 251 | 251 | 750 | 750 |
Name Text Field | 250 | 250 | 750 | 750 |
이 레시피는 각 컨트롤에 대해 한 쌍의 제약을 사용합니다. greater-than-or-equal 제약은 컨트롤과 레이아웃 사이의 최소 거리를 정의합니다. 반면에 선택적 제약은 레이아웃 가이드로부터 정확히 20.0 포인트 만큼 컨트롤을 당기는 것을 시도합니다.
두 제약은 더 큰 제약에 대해 조건을 충족시키고, 그렇기 때문에 시스템은 레이아웃으로부터 정확히 20.0 포인트 떨어진 곳에 위치시킵니다. 그러나 더 짧은 컨트롤은 오직 최소 거리만 조건을 충족합니다. 다른 제약은 무시됩니다. 이는 오토 레이아웃 시스템이 런타임에 컨트롤의 변화에 대해 레이아웃을 동적으로 다시 계산할 수 있도록 합니다.
Note
선택적 제약의 우선순위 설정에서 content-hugging 제약 (250)보다 더 낮은 값이 될 수 있도록 해야 합니다. 그렇지 않으면 시스템은 content-hugging 제약을 무시하고, 뷰를 재배치하는 것이 아니라 확장하게 합니다.
이는 베이스라인 정렬을 사용하는 레이아웃을 작업할 때 특히나 혼란스러운 부분입니다. 왜냐하면 베이스라인 정렬은 텍스트 뷰들이 내재된 컨텐트 높이로 표현될 때에 유효하기 때문입니다. 만약 시스템이 하나의 뷰 크기를 다시 조절하면, 베이스라인 제약을 필수로 하고 있음에도 텍스트는 제대로 정렬되지 않을 수 있습니다.
이 레시피는 Simple Label and Text Field recipe을 레이블과 텍스트 필드를 열처럼 표현할 수 있도록 확장시킵니다. 여기서 모든 레이블의 trailing edge는 정렬되어 있습니다. 텍스트 필드의 leading, trailing edge가 정렬됩니다. 그리고 수평으로 위치시키는 것은 가장 긴 레이블에 기반합니다. 그러나 Simple Label and Text Field처럼 이 레시피는 텍스트 필드가 항상 레이블보다 크다는 것을 가정함으로써 레이아웃 로직을 단순화 합니다.
레이블, 텍스트 필드들을 놓고 아래 제약을 설정합니다.
First Name Label.Leading = Superview.LeadingMargin
Middle Name Label.Leading = Superview.LeadingMargin
Last Name Label.Leading = Superview.LeadingMargin
First Name Text Field.Leading = First Name Label.Trailing + Standard
Middle Name Text Field.Leading = Middle Name Label.Trailing + Standard
Last Name Text Field.Leading = Last Name Label.Trailing + Standard
First Name Text Field.Trailing = Superview.TrailingMargin
Middle Name Text Field.Trailing = Superview.TrailingMargin
Last Name Text Field.Trailing = Superview.TrailingMargin
First Name Label.Baseline = First Name Text Field.Baseline
Middle Name Label.Baseline = Middle Name Text Field.Baseline
Last Name Label.Baseline = Last Name Text Field.Baseline
First Name Text Field.Width = Middle Name Text Field.Width
First Name Text Field.Width = Last Name Text Field.Width
First Name Text Field.Top = Top Layout Guide.Bottom + 20.0
Middle Name Text Field.Top = First Name Text Field.Bottom + Standard
Last Name Text Field.Top = Middle Name Text Field.Bottom + Standard
특성 인스펙터에서 아래에 보이는 특성을 설정합니다. 특히 모든 레이블에서 텍스트를 오른쪽 정렬합니다. 이는 텍스트보다 더 긴 레이블을 사용할 수 있도록 해주고, 텍스트 필드 옆에 정렬시키도록 합니다.
View | Attribute | Value |
---|---|---|
First Name Label | Text | First Name |
First Name Label | Alignment | Right |
First Name Text Field | Placeholder | Enter first name |
Middle Name Label | Text | Middle Name |
Middle Name Label | Alignment | Right |
Middle Name Text Field | Placeholder | Enter middle name |
Last Name Label | Text | Last Name |
Last Name Label | Alignment | Right |
Last Name Text Field | Placeholder | Enter last name |
각각의 쌍에서 레이블의 content-hugging은 텍스트 필드의 content-hugging보다 높아야 합니다. 인터페이스 빌더는 자동으로 이를 수행합니다. 사이즈 인스펙터에서 확인할 수 있습니다.
Name | Horizontal hugging | Vertical hugging |
---|---|---|
First Name Label | 251 | 251 |
First Name Text Field | 250 | 250 |
Middle Name Label | 251 | 251 |
Middle Name Text Field | 250 | 250 |
Last Name Label | 251 | 251 |
Last Name Text Field | 250 | 250 |
이 레시피는 기본적으로 Simple Label and Text Field의 세 가지 복사본을 통해 시작합니다. 하나씩 쌓는 방법입니다. 그러나 행이 적절하게 정렬될 수 있도록 몇 가지 추가적인 것이 필요합니다.
첫 번째로 각각의 레이블을 오른쪽 정렬하는 것을 통해 문제를 간단하게 만듭니다. 그리고 레이블의 넓이를 같게 만듭니다. 텍스트의 길이와 상관없이 trailing edge를 쉽게 정렬시킬 수 있습니다. 추가적으로 레이블의 compression-resistance가 content-hugging보다 크기 때문에 모든 레이블은 축소되는 것이 아니라 확장되는 것이 우선으로 적용됩니다. leading과 trailing edge를 정렬하고, 모든 레이블은 가장 긴 레이블의 내재된 컨텐트 크기로 자연스럽게 뻗어나갑니다.
그러므로 모든 레이블의 leading과 trailing edge만 정렬시키면 됩니다. 모든 텍스트 필드에 대해서도 leading과 trailing edge를 정렬시킬 필요가 있습니다. 다행히 레이블들의 leading edge는 이미 슈퍼뷰의 leading 마진에 정렬되어 있습니다. 유사하게 텍스트 필드의 trailing edge 역시 슈퍼뷰의 trailing 마진에 정렬되어 있습니다. 다른 두 edge만 정렬시키면 되고, 모든 행이 같은 넓이를 갖기 때문에 모든 것이 정렬될 것입니다.
이렇게 하는 데 몇 가지 방법이 있습니다. 이 레시피에서 각각의 텍스트 필드에 대해 같은 넓이를 적용했습니다.
이 레시피는 Dynamic Height Label and Text Field 레시피와 Fixed Height Columns 레시피에서 배운 모든 것을 결합합니다. 이 레시피의 목표는 아래 내용을 포함합니다.
레이블들의 trailing edge가 정렬되며, 가장 긴 레이블을 기준으로 합니다.
텍스트 필드는 같은 넓이를 갖고, leading과 trailing edge가 정렬됩니다.
텍스트 필드는 슈퍼뷰에서 남은 모든 여백을 채우기 위해 확장됩니다.
행들의 높이는 행 중 가장 큰 요소를 기준으로 결정됩니다.
모든 것이 동적이기 때문에 만약 폰트 크기 혹은 레이블 텍스트가 바뀐다면, 레이아웃은 자동으로 업데이트됩니다.
Fixed Height Columns에서 했었던 것처럼 레이블과 텍스트 필드 레이아웃을 만들지만 몇 가지 추가적인 제약들이 필요합니다.
First Name Label.Leading = Superview.LeadingMargin
Middle Name Label.Leading = Superview.LeadingMargin
Last Name Label.Leading = Superview.LeadingMargin
First Name Text Field.Leading = First Name Label.Trailing + Standard
Middle Name Text Field.Leading = Middle Name Label.Trailing + Standard
Last Name Text Field.Leading = Last Name Label.Trailing + Standard
First Name Text Field.Trailing = Superview.TrailingMargin
Middle Name Text Field.Trailing = Superview.TrailingMargin
Last Name Text Field.Trailing = Superview.TrailingMargin
First Name Label.Baseline = First Name Text Field.Baseline
Middle Name Label.Baseline = Middle Name Text Field.Baseline
Last Name Label.Baseline = Last Name Text Field.Baseline
First Name Text Field.Width = Middle Name Text Field.Width
First Name Text Field.Width = Last Name Text Field.Width
First Name Label.Top >= Top Layout Guide.Bottom + 20.0
First Name Label.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
First Name Text Field.Top >= Top Layout Guide.Bottom + 20.0
First Name Text Field.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
Middle Name Label.Top >= First Name Label.Bottom + Standard
Middle Name Label.Top = First Name Label.Bottom + Standard (Priority 249)
Middle Name Text Field.Top >= First Name Text Field.Bottom + Standard
Middle Name Text Field.Top = First Name Text Field.Bottom + Standard (Priority 249)
Last Name Label.Top >= Middle Name Label.Bottom + Standard
Last Name Label.Top = Middle Name Label.Bottom + Standard (Priority 249)
Last Name Text Field.Top >= Middle Name Text Field.Bottom + Standard
Last Name Text Field.Top = Middle Name Text Field.Bottom + Standard (Priority 249)
특성 인스펙터에서 아래 특성들을 설정합니다. 특히 모든 레이블에서 텍스트를 오른쪽 정렬 합니다. 레이블을 오른쪽 정렬하는 것은 레이블들의 텍스트보다 더 긴 레이블들을 사용할 수 있도록 해주고, 텍스트의 edge는 여전히 텍스트 필드 옆에 정렬됩니다.
View | Attribute | Value |
---|---|---|
First Name Label | Text | First Name |
First Name Label | Alignment | Right |
First Name Text Field | Placeholder | Enter first name |
Middle Name Label | Text | Middle Name |
Middle Name Label | Alignment | Right |
Middle Name Text Field | Placeholder | Enter middle name |
Last Name Label | Text | Last Name |
Last Name Label | Alignment | Right |
Last Name Text Field | Placeholder | Enter last name |
각각의 쌍에 대해 레이블의 content-hugging은 텍스트 필드보다 더 높아야 합니다. 인터페이스 필더는 자동으로 이를 수행합니다. 사이즈 인스펙터에서 이 속성을 확인할 수 있습니다.
Name | Horizontal hugging | Vertical hugging |
---|---|---|
First Name Label | 251 | 251 |
First Name Text Field | 250 | 250 |
Middle Name Label | 251 | 251 |
Middle Name Text Field | 250 | 250 |
Last Name Label | 251 | 251 |
Last Name Text Field | 250 | 250 |
이 레시피는 간단하게 Dynamic Height Label 레시피와 Text Field and Fixed Height Columns 레시피를 결합합니다. Dynamic Height Label, Text Field and Fixed Height Columns 레시피처럼 이 레시피는 행들의 수직 간격을 동적으로 설정하기 위해 여러 쌍의 제약들을 사용합니다. Fixed Height Columns 레시피처럼 이 레시피는 레이블들에서 오른쪽으로 정렬된 텍스트를 사용합니다. 그리고 같은 넓이를 갖도록 제약을 명시적으로 두고 열들을 정렬합니다.
NOTE
이 예제는 뷰와 top 레이아웃 가이드 사이 거리를 20.0 포인트 간격으로 사용합니다. 그리고 뷰들 사이의 거리는 8.0 포인트입니다. 이는 20.0 포인트 top 마진을 설정한 결과입니다. 바의 존재 여부에 따라 마진이 자동으로 적응할 수 있기를 원한다면, 제약을 추가해야 합니다. 일반적인 테크닉은 Adaptive Single View 레시피에서 볼 수 있습니다. 그러나 정확한 구현은 직접 해야 합니다.
지금까지 내용을 보면 레이아웃의 로직이 다소 복잡해지고 있습니다. 그러나 이와 같은 것들을 간단하게 할 수 있는 몇 가지 방법이 있습니다. 첫 번째는 이전에 언급한 것처럼 가능하면 스택뷰를 사용하는 것입니다. 스택뷰 사용은 복잡한 레이아웃을 간편하게 만들어줄 것입니다.
이 레시피는 같은 크기의 두 버튼을 배치하는 것을 설명합니다. 수직으로 버튼은 스크린의 bottom에 정렬됩니다. 수평으로 두 버튼은 남은 여백을 채울 수 있도록 확장됩니다.
인터페이스 빌더에서 두 버튼을 씬에 드래그합니다. 씬의 bottom에 따라 가이드라인을 사용하는 것을 통해 이들을 정렬합니다. 버튼이 같은 넓이를 갖도록 하는 것은 걱정할 필요가 없습니다. 남은 수평 공간을 채우기 위해 하나가 확장될 것입니다. 대략적으로 배치한 다음 아래 제약들을 설정합니다. 오토 레이아웃은 정확하게 계산할 것입니다.
Short Button.Leading = Superview.LeadingMargin
Long Button.Leading = Short Button.Trailing + Standard
Long Button.Trailing = Superview.TrailingMargin
Bottom Layout Guide.Top = Short Button.Bottom + 20.0
Bottom Layout Guide.Top = Long Button.Botton + 20.0
6 . Short Button.Width = Long Button.Width
버튼들을 시각화할 수 있는 배경색을 갖도록 합니다. 이를 통해 기기가 회전될 때 프레임이 어떻게 변화하는지 더 쉽게 볼 수 있도록 합니다. 추가적으로 버튼에서 다른 길이의 타이틀을 사용합니다. 버튼의 타이틀이 버튼의 넓이에 영향을 미치지 않는다는 것을 보여줄 것입니다.
View | Attribute | Value |
---|---|---|
Short Button | Background | Light Gray Color |
Short Button | Title | short |
Long Button | Background | Light Gray Color |
Long Button | Title | Much Longer Button Title |
이 레시피는 버튼의 내재된 높이를 사용합니다. 그러나 레이아웃을 계산할 때 버튼의 넓이를 사용하지는 않습니다. 수평에서 버튼들은 그들이 같은 넓이를 가질 수 있도록, 남은 공간을 채울 수 잇도록 명시적으로 크기를 갖게 됩니다. 버튼의 내재된 높이가 레이아웃에 영향을 미친다는 것을 보기 위해서 Two Equal-Width Views 레시피와 비교해보시기 바랍니다. 이 레시피에서 네 가지가 아니라 오직 두 가지 제약만 필요합니다.
버튼들은 어떻게 버튼의 텍스트가 레이아웃에 영향(혹은 이 경우에 영향을 미치지 않는다는 것)을 미치는지를 보여줄 수 있는 다른 길이의 타이틀이 주어집니다.
Note
이 레시피에서 버튼들은 light gray 배경색이 주어지기 때문에 프레임을 볼 수 있도록 합니다. 보통 버튼과 레이블은 투명한 배경을 갖고 있으며, 이를 통해 프레임의 변화(불가능하지 않다면)를 확인하기가 어렵습니다.
이 레시피는 Two Equal-Width Buttons 레시피를 확장해 세 개의 같은 넓이를 갖는 버튼을 사용합니다.
버튼들을 배치시키고 아래와 같은 제약들을 설정합니다.
Short Button.Leading = Superview.LeadingMargin
Medium Button.Leading = Short Button.Trailing + Standard
Long Button.Leading = Medium Button.Trailing + Standard
Long Button.Trailing = Superview.TrailingMargin
Bottom Layout Guide.Top = Short Button.Bottom + 20.0
Bottom Layout Guide.Top = Medium Button.Bottom + 20.0
Bottom Layout Guide.Top = Long Button.Bottom + 20.0
Short Button.Width = Medium Button.Width
Short Button.Width = Long Button.Width
기기가 회전될 때 프레임이 어떻게 변하는지 쉽게 확인할 수 있도록 배경색을 지정합니다. 추가적으로 버튼에 다른 길이의 타이틀을 사용합니다. 이를 통해 버튼 타이틀이 버튼의 넓이에 영향을 미치지 못한다는 것을 볼 수 있습니다.
Para | View | Attribute | Value |
---|---|---|---|
Short Button | Background | Ligt Gray Color | |
Short Button | Title | short | |
Medium Button | Background | Light Gray Color | |
Medium Button | Title | Medium | |
Long Button | Background | Light Gray Color | |
Long Button | Title | Long Button Title |
남은 버튼을 추가하는 것은 세 가지 제약(두 개의 수평 제약과 하나의 수직 제약)을 요구합니다. 버튼의 내재된 넓이를 사용하지 않는 사실을 기억해야 합니다. 그렇기 때문에 위치와 크기를 구체화할 수 있는 최소 두 개의 수평 제약이 필요합니다. 그러나 버튼의 내재된 높이는 사용합니다. 그렇기 때문에 수직 위치를 구체화하려면 하나의 추가적인 제약만 필요합니다.
NOTE
같은 넓이를 설정하는 제약을 빠르게 설정하려면 세 개의 버튼을 선택하고 인터페이스 빌더의 pin 도구에서 equal width 제약을 생성하면 됩니다. 인터페이스 빌더는 자동으로 필요한 제약 모두를 생성할 것입니다.
표면적으로 이 레시피는 Two Equal-Width Buttons(스크린샷을 보시기 바랍니다) 레시피와 닮아있습니다. 그러나 이 레시피에서 버튼들의 넓이는 가장 긴 타이틀에 기반을 두고 있습니다. 만약 충분한 공간이 있다면, 버튼들은 더 긴 버튼의 내재된 크기와 일치할 때까지 확장될 것입니다. 모든 추가적인 공간은 버튼을 중심으로 같게 나눠집니다.
아이폰은 Two Equal-Width Buttons와 Two Buttons with Equal Spacing 레이아웃들이 portrait 오리엔테이션에서 거의 동일한 것처럼 나타날 것입니다. 기기를 landscape(혹은 아이패드처럼 더 큰 기기를 사용할 때) 오리엔테이션으로 회전시킬 때 차이가 분명하게 보입니다.
인터페이스 빌더에서 두 버튼과 세 개의 뷰 객체를 드래그합니다. 버튼을 뷰들 사이에 위치시키고, 아래에 보이는 제약을 설정합니다.
Leading Dummy View.Leading = Superview.LeadingMargin
Short Button.Leading = Leading Dummy View.Trailing
Center Dummy View.Leading = Short Button.Trailing
Long Button.Leading = Center Dummy View.Trailing
Trailing Dummy View.Leading = Long Button.Trailing
Trailing Dummy View.Trailing = Superview.TrailingMargin
Bottom Layout Guide.Top = Leading Dummy View.Bottom + 20.0
Bottom Layout Guide.Top = Short Button.Bottom + 20.0
Bottom Layout Guide.Top = Center Dummy View.Bottom + 20.0
Bottom Layout Guide.Top = Long Button.Bottom + 20.0
Bottom Layout Guide.Top = Trailing Dummy View.Bottom + 20.0
Short Button.Leading >= Superview.LeadingMargin
Long Button.Leading >= Short Button.Trailing + Standard
Superview.TrailingMargin >= Long Button.Trailing
Leading Dummy View.Width = Center Dummy View.Width
Leading Dummy View.Width = Trailing Dummy View.Width
Short Button.Width = Long Button.Width
Leading Dummy View.Height = 0.0
Center Dummy View.Height = 0.0
Trailing Dummy View.Height = 0.0
기기 회전 시 프레임이 어떻게 변하는지 보기 쉽도록 배경색을 지정합니다. 추가적으로 버튼에 다른 길이의 타이틀을 사용합니다. 버튼틀의 크기는 가장 긴 타이틀에 따라 결정됩니다.
View | Attribute | Value |
---|---|---|
Short Button | Background | Light Gray Color |
Short Button | Title | Short |
Long Button | Background | Light Gray Color |
Long Button | Title | Much Longer Button Title |
보이는 것처럼 제약이 복잡해졌습니다. 이 예제는 구체적인 테크닉을 설명하기 위해 디자인되어있지만, 실제 앱에서는 스택뷰 사용을 고려해보는 것이 좋습니다.
이 예제에서 흰색 여백의 크기가 뷰의 프레임 변할 수 있도록 해야 합니다. 흰색 여백의 넓이를 제어하기 위해 같은 넓이를 갖도록 하는 제약을 설정해야 합니다. 그러나 빈 공간에 제약을 둘 수 없습니다. 제약을 둘 수 있는 몇 가지 객체가 필요합니다.
이 레시피에서 더미뷰들을 사용해 빈 공간을 표현합니다. 이 뷰들은 UIView 클래스의 비어있는 인스턴스들입니다. 이 레시피에서 0 포인트 높이가 주어지고, 이를 통해 뷰 계층구조에 미치는 영향을 최소화합니다.
NOTE
더미뷰들은 레이아웃에 상당한 비용을 추가하기 때문에 신중하게 사용해야 합니다. 이 뷰들이 크다면 그래픽 컨텍스트는 엄청난 양의 메모리를 사용할 수 있습니다. 심지어 의미있는 정보를 담고 있지 않고 있음에도 많은 양의 메모리를 사용할 수 있습니다.
추가적으로 이 뷰들은 뷰 계층구조의 리스폰더 체인에 참여합니다. 이는 hit 테스팅처럼 리스폰더 체인에 따라 전달되는 메시지에 반응한다는 것을 의미합니다. 신중하게 다루지 않는다면 이 뷰들이 찾기 어려운 버그를 생성하면서 메시지를 낚아채고 반응합니다.
안타깝게도 인터페이스 빌더에서 씬에 레이아웃 가이드를 추가할 수 없습니다. 그리고 코드로 작성된 객체들을 스토리보드 기반의 씬과 혼합하는 것은 매우 복잡해집니다. 일반적으로 커스텀 레이아웃 가이드를 사용하는 것보다 스토리보드와 인터페이스 빌더를 사용하는 것이 더 낫습니다.
이 레시피는 버튼 주변의 최소 공간을 설정하기 위해 greater-than-or-equal 제약들을 사용합니다. 필수로 적용되어야 하는 제약들은 버튼이 항상 같은 넓이를 갖는 것을 보장하고, 더미 뷰들 또한 항상 같은 넓이를 가질 수 있도록 합니다(더미뷰들은 버튼과 다른 넓이일 수 있습니다). 레이아웃의 남은 부분은 대부분 버튼의 content-hugging, compression-resistance 속성에 의해 다뤄집니다. 만약 충분한 공간이 없다면 더비뷰들은 0 포인트 넓이로 무너지고, 버튼은 이용 가능한 공간을 스스로 나눠가집니다(버튼 사이의 표준 여백으로). 사용할 수 있는 공간이 늘어나면, 더미뷰들은 확장하기 시작합니다. 더미뷰들은 남은 모든 공간을 채울 때까지 확장됩니다.
이 레시피는 제약들로 이뤄진 두 개의 다른 집합을 사용합니다. 하나는 Any-Any 레이아웃 용도로 인스톨됩니다. 이 제약들은 같은 넓이 버튼의 한 쌍을 정의합니다. Two Equal-Width Buttons 레시피와 동일합니다.
다른 하나의 제약 집합은 Compact-Regular 레이아웃 용도로 인스톨됩니다. 이 제약들은 아래에 보이는 것처럼 버튼들이 쌓이는 것을 정의합니다.
수직으로 쌓인 버튼들은 아이폰의 portrait 오리엔테이션에서 사용됩니다. 버튼들의 수평 행은 다른 모든 곳에서 사용됩니다.
Two Equal-Width Buttons 레시피에서 했던 것과 정확히 동일하게 버튼을 배치합니다. Any-Any 사이즈 클래스에서 제약을 1부터 6까지 설정합니다.
다음으로 인터페이스 빌더의 사이즈 클래스를 Compact-Regular 레이아웃으로 전환합니다.
2, 5 제약을 언인스톨하고 7, 8, 9 제약을 아래에 보이는 것처럼 추가합니다.
Short Button.Leading = Superview.LeadingMargin
Long Button.Leading = Short Button.Trailing + Standard
Long Button.Trailing = Superview.TrailingMargin
Bottom Layout Guide.Top = Short Button.Bottom + 20.0
Bottom Layout Guide.Top = Long Button.Botton + 20.0
Short Button.Width = Long Button.Width
Long Button.Leading = Superview.LeadingMargin
Short Button.Trailing = Superview.TrailingMargin
Long Button.Top = Short Button.Bottom + Standard
기기 회전 시 프레임 변화를 쉽게 볼 수 있도록 버튼에 배경색을 설정합니다. 추가적으로 버튼은 다른 길이의 타이틀을 사용해 버튼 타이틀이 버튼의 넓이에 영향을 미치지 않는다는 것을 보여줄 수 있도록 합니다.
View | Attribute | Value |
---|---|---|
Short Button | Background | Light Gray Color |
Short Button | Title | Short |
Long Button | Background | Light Gray Color |
Long Button | Title | Much Longer Button Title |
인터페이스 빌더는 사이즈 클래스인 구체적인 뷰들, 뷰 특성들, 뷰 제약들을 설정할 수 있도록 해줍니다. 이는 총 아홉 개의 다른 사이즈 클래스가 주어지면서 넓이, 높이를 위한 세 가지 다른 사이즈 클래스(Compact, Any 혹은 Reqular)에 다른 옵션을 구체화할 수 있도록 해줍니다. 네 가지는 기기에서 사용되는 Final 사이즈 클래스에 상응합니다(Compact-Compact, Compact-Regular, Regular-Compact, Regular-Regular). 나머지는 Base 크기 클래스 혹은 둘이나 둘 이상의 사이즈 클래스의 표현을 추상화합니다(Compact-Any, Regular-Any, Any-Compact, Any-Regular, and Any-Any).
주어진 사이즈 클래스를 위한 레이아웃을 로딩할 때 시스템은 해당 사이즈 클래스에 대해 가장 구체적인 설정들을 로드합니다. 이는 Any-Any 사이즈 클래스가 모든 뷰에서 사용되는 기본값을 정의하고 있음을 의미합니다. Compact-Any 세팅들은 모든 뷰의 compact 넓이에 영향을 미치고, Compact-Regular 세팅들은 compact 넓이와 regular 높이를 갖는 뷰들에서만 사용됩니다. 뷰의 사이즈 클래스가 변화하면 시스템은 자동으로 레이아웃을 서로 바꾸고 변화를 애니메이션으로 표현합니다. 뷰의 사이즈 클래스 변화는 예를 들어 아이폰이 portrait에서 landscape으로 전환되는 경우를 생각해볼 수 있습니다.
아이폰 오리엔테이션의 변화에 따라 대응할 수 있도록 다른 레이아웃을 생성하기 위해 이 기능을 사용할 수 있습니다. 또한, 아이패드와 아이폰의 레이아웃이 다른 식으로 생성하기 위해 이 기능을 사용할 수도 있습니다. size-class-specific 커스텀은 원하는 수준에 따라 폭이 넓어질 수도 있고 간단할 수도 있습니다. 당연히 많은 변화를 줄 수록 스토리보드는 더 복잡해지고, 디자인과 유지보수를 어렵게 합니다.
모든 베이스 사이즈 클래스들을 포함해, 각각의 가능한 사이즈 클래스에 대해 유효한 레이아웃을 만들어줘야 합니다. 보통 기본값 레이아웃이 되기 위해 한 가지 레이아웃을 선택하는 것이 가장 쉽습니다. Any-Any 사이즈 클래스에서 레이아웃을 디자인하는 것이 좋습니다. 그후 필요하다면 Final 사이즈 클래스를 수정합니다. 더 구체저인 사이즈 클래스에서 아이템들을 추가할 수도 있고 지울 수도 있습니다.
시작 전에 사이즈 클래스의 9 X 9 그리드를 그리기 원할 수도 있습니다. 이와 같은 사이즈 클래스에서 레이아웃과 함께 네 가지 코너를 채워야 합니다. 그러면 그리드는 여러 가지 사이즈 클래스에 걸쳐 어떤 제약이 공유되어야 하는지 볼 수 있도록 해줄 것입니다. 또한, 레이아웃과 사이즈 클래스 사이의 최적 조합을 찾을 수 있도록 도와줄 것입니다.
사이즈 클래스로 작업하는 것에 대한 정보는 Debugging Auto Layout을 살펴보시기 바랍니다.
Debugging Auto Layout은 다음 글입니다.