[번역] Stack Views

작성일 :

개요


해당 문서는 학습 목적으로 Apple 공식 문서인 🔗 Auto Layout Guide을 번역한 글입니다. 다소 오역이 있을 수 있어 잘못된 내용이 있을 수 있습니다. 문제가 되거나 오류가 있다면 댓글 부탁드립니다.

내용


지금부터 살펴 볼 예제는 StackView를 사용하여 점점 더 복잡해지는 레이아웃을 만드는 방법입니다. 스택 뷰는 사용자 인터페이스를 빠르고 쉽게 디자인할 수 있는 강력한 도구입니다. 속성(attributes)을 사용하면 정렬된 뷰를 배치하는 방법에 대해 높은 수준의 제어가 가능합니다. 추가적인 커스텀 제약 조건을 사용하여 이러한 설정을 보완할 수 있습니다. 그러나 이것은 레이아웃의 복잡성을 증가시킵니다.

이러한 레시피의 소스 코드를 보려면 🔗 Auto Layout Cookbook 프로젝트를 참조하십시오.

간단한 StackView


이 예시는 단일 수직 StackView를 사용하여 레이블, 이미지 보기 및 버튼을 배치합니다.

image01

View 와 Constraints

Interface Builder에서 수직 스택 뷰를 드래그하여 시작하고 꽃 레이블, 이미지 뷰 및 편집 버튼을 추가합니다. 그런 다음 그림과 같이 제약 조건을 설정합니다.

image02
  1. Stack View.Leading = Superview.LeadingMargin
  2. Stack View.Trailing = Superview.TrailingMargin
  3. Stack View.Top = Top Layout Guide.Bottom + Standard
  4. Bottom Layout Guide.Top = Stack View.Bottom + Standard

속성(Attributes)

Attributes inspector에서 다음 StackView의 속성을 설정합니다.

image03

다음으로 ImageView 에서 다음 속성(Attributes)을 설정합니다.

image04

마지막으로 크기 속성에서 ImageView의 content-hugging와 compression-resistance(CHCR)의 우선 순위를 설정합니다.

image05

Discussion

StackView를 부모 View에 고정해야 하지만 그렇지 않으면 StackView가 다른 명시적 제약 없이 전체 레이아웃을 관리합니다.

이 예제에서 StackView는 작은 표준 여백으로 부모View를 채웁니다. 배열된 뷰는 StackView의 경계를 채우기 위해 크기가 조정됩니다. 수평으로 각 뷰는 StackView의 너비와 일치하도록 늘어납니다. 세로로 보기는 CHCR 우선 순위에 따라 늘어납니다. ImageView는 사용 가능한 공간을 채우기 위해 항상 축소되고 커져야 합니다. 따라서 수직 콘텐츠 CHCR 우선 순위는 레이블 및 버튼의 기본 우선 순위보다 낮아야 합니다.

마지막으로 이미지 뷰의 모드를 Aspect Fit으로 설정합니다. 이 설정은 이미지의 종횡비를 유지하면서 ImageView의 경계 내에 맞도록 ImageView가 이미지 크기를 조정하도록 강제합니다. 이를 통해 StackView는 이미지를 왜곡하지 않고 ImageView의 크기를 임의로 조정할 수 있습니다.

View를 고정하여 상위 View를 채우는 방법에 대한 자세한 내용은 🔗 Attributes 및 🔗 Adaptive Single View 를 참조하세요.

Nested Stack Views


이 예제는 중첩된 스택 뷰의 여러 레이어에서 빌드된 복잡한 레이아웃을 보여줍니다. 그러나 이 예에서 스택 뷰는 원하는 동작을 단독으로 생성할 수 없습니다. 대신 레이아웃을 더 세분화하려면 추가 제약 조건이 필요합니다.

image06

Views and Constraints

중첩된 스택 뷰로 작업할 때 안쪽에서 바깥쪽으로 작업하는 것이 가장 쉽습니다. Interface Builder에서 이름 행을 배치하여 시작하십시오. 레이블과 텍스트 필드를 올바른 상대 위치에 배치하고 둘 다 선택한 다음 Editor > Embed In > Stack View 메뉴 항목을 클릭합니다. 이렇게 하면 행에 대한 수평 스택 뷰가 생성됩니다.

그런 다음 이 행을 가로로 배치하고 선택하고 Editor > Embed In > Stack View 메뉴 항목을 다시 클릭합니다. 이렇게 하면 행의 가로 스택이 생성됩니다. 표시된 대로 인터페이스를 계속 빌드합니다.

image07
  1. Root Stack View.Leading = Superview.LeadingMargin
  2. Root Stack View.Trailing = Superview.TrailingMargin
  3. Root Stack View.Top = Top Layout Guide.Bottom + 20.0
  4. Bottom Layout Guide.Top = Root Stack View.Bottom + 20.0
  5. Image View.Height = Image View.Width
  6. First Name Text Field.Width = Middle Name Text Field.Width
  7. First Name Text Field.Width = Last Name Text Field.Width

Attributes

각 스택에는 자체 속성 세트가 있습니다. 스택이 내용을 배치하는 방법을 정의합니다. 속성 관리자에서 다음 속성을 설정합니다.

image08

또한 텍스트 뷰에 밝은 회색 배경색을 지정합니다. 이렇게 하면 방향이 변경될 때 텍스트 뷰의 크기가 어떻게 조정되는지 더 쉽게 확인할 수 있습니다.

image09

마지막으로 CHCR 우선 순위는 사용 가능한 공간을 채우기 위해 확장해야 하는 뷰를 정의합니다. 크기 속성에서 다음 CHCR 우선순위를 설정합니다.

image10

Discussion

이 예시에서 스택 뷰는 함께 작동하여 대부분의 레이아웃을 관리합니다. 그러나 그들은 스스로 원하는 행동을 모두 만들 수는 없습니다. 예를 들어, 이미지 뷰의 크기가 조정될 때 이미지는 종횡비를 유지해야 합니다. 안타깝게도 Simple Stack View에서 사용되는 기술은 여기서는 작동하지 않습니다. 레이아웃은 이미지의 후행 가장자리와 아래쪽 가장자리에 모두 맞아야 하며 Aspect Fit 모드를 사용하면 해당 치수 중 하나에 추가 공백이 추가됩니다. 다행스럽게도 이 예에서 이미지의 종횡비는 항상 정사각형이므로 이미지가 이미지 뷰의 경계를 완전히 채우고 이미지 뷰를 1:1 종횡비로 제한할 수 있습니다.

NOTE

Interface Builder에서 aspect ratio 제약 조건은 단순히 뷰의 높이와 너비 사이의 제약 조건입니다. Interface Builder는 여러 가지 방법으로 제약 조건의 승수를 표시할 수도 있습니다. 일반적으로 종횡비 제약 조건의 경우 비율로 표시됩니다. 따라서 View.Width = View.Height 제약 조건은 1:1 종횡비로 나타날 수 있습니다.

또한 모든 텍스트 필드의 너비는 동일해야 합니다. 안타깝게도 모두 별도의 스택 뷰에 있으므로 스택에서 이를 관리할 수 없습니다. 대신 동일한 너비 제약 조건을 명시적으로 추가해야 합니다.

단순 스택 뷰와 마찬가지로 일부 CHCR 우선순위도 수정해야 합니다. 이들은 부모클래스의 경계가 변경됨에 따라 뷰가 축소되고 증가하는 방법을 정의합니다.

수직으로 텍스트 뷰가 확장되어 위쪽 스택과 버튼 스택 사이의 공간을 채우려고 합니다. 따라서 텍스트 뷰의 수직 콘텐츠 허깅은 다른 수직 콘텐츠 허깅 우선 순위보다 낮아야 합니다.

수평으로 레이블은 고유 콘텐츠 크기로 나타나야 하며 텍스트 필드는 추가 공간을 채우기 위해 크기가 조정됩니다. 기본 CHCR 우선 순위는 레이블에 적합합니다. Interface Builder는 이미 콘텐츠 허깅을 251로 설정하여 텍스트 필드보다 높게 만듭니다. 그러나 텍스트 필드의 가로 콘텐츠 허깅과 가로 compression resistance을 모두 낮춰야 합니다.

이미지 뷰는 이름 행을 포함하는 스택과 높이가 같도록 축소되어야 합니다. 그러나 스택 뷰는 콘텐츠를 느슨하게 포함할 뿐입니다. 이것은 이미지 뷰의 수직 압축 저항이 매우 낮아야 함을 의미하므로 스택 뷰가 확장되는 대신 이미지 뷰가 축소됩니다. 또한 이미지 뷰의 Aspect ratio 제약 조건은 수직 및 수평 제약 조건이 상호 작용할 수 있기 때문에 레이아웃을 복잡하게 만듭니다. 즉, 텍스트 필드의 가로 콘텐츠 허깅도 매우 낮아야 합니다. 그렇지 않으면 이미지 뷰가 축소되는 것을 방지할 수 있습니다. 두 경우 모두 우선순위를 48 이하의 값으로 설정하십시오.

Dynamic Stack View


이 예시는 런타임 시 스택에서 항목을 동적으로 추가하고 제거하는 방법을 보여줍니다. 스택에 대한 모든 변경 사항은 애니메이션으로 표시됩니다. 또한 스택 뷰는 스크롤 뷰 안에 배치되어 목록이 너무 길어서 화면에 맞지 않는 경우 목록을 스크롤할 수 있습니다.

image11

NOTE

이 예시는 스택 뷰로 동적으로 작업하고 스크롤 뷰 내에서 스택 뷰로 작업하는 것을 보여주기 위한 것입니다. 실제 앱에서 이 레시피의 동작은 대신 UITableView 클래스를 사용하여 구현해야 합니다. 일반적으로 동적 스택 뷰를 사용하여 스크래치로 만든 테이블 뷰 복제본을 간단히 구현해서는 안 됩니다. 대신 다른 기술로는 쉽게 구축할 수 없는 동적 사용자 인터페이스를 만드는 데 사용하십시오.

Views and Constraints

초기 사용자 인터페이스는 매우 간단합니다. 장면에 스크롤 보기를 배치하고 장면을 채우도록 크기를 조정합니다. 그런 다음 스크롤 뷰 내부에 스택 뷰를 배치하고 스택 뷰 내부에 항목 추가 버튼을 배치합니다. 모든 것이 제자리에 있는 즉시 다음 제약 조건을 설정합니다.

image12
  1. Scroll View.Leading = Superview.LeadingMargin
  2. Scroll View.Trailing = Superview.TrailingMargin
  3. Scroll View.Top = Superview.TopMargin
  4. Bottom Layout Guide.Top = Scroll View.Bottom + 20.0
  5. Stack View.Leading = Scroll View.Leading
  6. Stack View.Trailing = Scroll View.Trailing
  7. Stack View.Top = Scroll View.Top
  8. Stack View.Bottom = Scroll View.Bottom
  9. Stack View.Width = Scroll View.Width

Attributes

Attributes inspector에서 다음 스택 뷰 속성을 설정합니다.

image13

Code

이 예시에는 스택 뷰에 항목을 추가하고 제거하기 위한 약간의 코드가 필요합니다. 스크롤 뷰와 스택 뷰 모두에 대한 아웃렛이 있는 장면에 대한 사용자 지정 뷰 컨트롤러를 만듭니다.

swift
class DynamicStackViewController: UIViewController {

    @IBOutlet weak private var scrollView: UIScrollView!
    @IBOutlet weak private var stackView: UIStackView!

    // Method implementations will go here...

}

다음으로 viewDidLoad 메서드를 재정의하여 스크롤 뷰의 초기 위치를 설정합니다. 스크롤 뷰의 콘텐츠가 상태 표시줄 아래에서 시작되기를 원합니다.

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

    // setup scrollview
    let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
    scrollView.contentInset = insets
    scrollView.scrollIndicatorInsets = insets

}

이제 항목 추가 버튼에 대한 작업 메서드를 추가합니다.

swift
// 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 메소드와 달리 이 메소드는 Interface Builder의 어떤 컨트롤에도 연결되지 않습니다. 대신 앱은 뷰가 생성될 때 프로그래밍 방식으로 각 항목 뷰를 이 메서드에 연결합니다.

swift
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()
        })
    }
}

이 방법은 애니메이션 블록에서 뷰를 숨깁니다. 애니메이션이 완료된 후 뷰 계층 구조에서 뷰를 제거합니다. 이렇게 하면 스택의 정렬된 보기 목록에서 보기가 자동으로 제거됩니다.

entry view는 모든 뷰가 될 수 있지만 이 예제에서는 날짜 레이블, 임의의 16진수 문자열이 포함된 레이블 및 삭제 버튼이 포함된 스택 뷰를 사용합니다.

swift
// 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
}

Discussion

이 레시피에서 설명하는 것처럼 런타임 중에 스택 뷰에서 보기를 추가하거나 제거할 수 있습니다. 스택의 레이아웃은 정렬된 뷰 배열의 변경 사항을 보상하기 위해 자동으로 조정됩니다. 그러나 기억할 가치가 있는 몇 가지 중요한 사항이 있습니다.

  • hidden 뷰는 여전히 스택의 정렬된 뷰 배열 안에 있습니다. 그러나 표시되지 않으며 다른 정렬된 보기의 레이아웃에 영향을 주지 않습니다.
  • 스택의 정렬된 뷰 배열에 뷰를 추가하면 뷰 계층 구조에 자동으로 추가됩니다.
  • 스택의 정렬된 뷰 배열에서 뷰를 제거해도 뷰 계층에서 자동으로 제거되지 않습니다. 그러나 뷰 계층 구조에서 뷰를 제거하면 정렬된 보기 뷰에서 제거됩니다.
  • iOS에서 보기의 hidden 속성은 일반적으로 애니메이션할 수 없습니다. 그러나 이 속성은 뷰가 스택의 정렬된 뷰 배열에 배치되는 즉시 뷰에 대해 애니메이션 가능해집니다. 실제 애니메이션은 뷰가 아닌 스택에서 관리합니다. hidden 속성을 사용하여 뷰를 스택에 추가하거나 스택에서 제거하는 애니메이션을 만듭니다.

이 예시는 또한 스크롤 뷰와 함께 자동 레이아웃을 사용하는 아이디어를 소개합니다. 여기서 스택과 스크롤 뷰 사이의 제약 조건은 스크롤 뷰의 콘텐츠 영역 크기를 설정합니다. 동일한 너비 제약 조건은 명시적으로 스택(및 콘텐츠 크기)을 설정하여 스크롤 뷰를 가로로 채웁니다. 수직으로 콘텐츠 크기는 스택의 피팅 크기를 기반으로 합니다. 사용자가 더 많은 항목을 추가하면 스택 뷰가 길어집니다. 콘텐츠가 너무 많아 화면에 표시되지 않는 즉시 스크롤이 자동으로 활성화됩니다.

자세한 내용은 🔗 Working with Scroll Views을 참조하세요.