iOS/개념

StackView 톺아보기

누알라리 2020. 10. 4. 19:32

 

개요

개인 프로젝트 개발이 아닌 회사 업무를 시작하면서 스택뷰에 대해 더 잘 알아놔야겠다는 생각이 들어 정리한 글 입니다.

애플 개발자 문서에 나와있는 내용을 한 줄 한 줄 읽어보고, 많이 쓰이는 형태를 학습해봅니다.

시작하기에 앞서, 인용(회색 막대기) 으로 쓰인 글은 애플 개발자 문서를 번역한 내용입니다.

 


1.  애플 개발자 문서의 UIStackView

developer.apple.com/documentation/uikit/uistackview

 

Apple Developer Documentation

 

developer.apple.com

1-1. UIStackView란?

애플 개발자 문서를 차근차근 읽어봅시다. 

일단 StackView란..

StackView란 Auto Layout을 이용해 열 또는 행에 View들의 묶음을 배치할 수 있는 간소화된 인터페이스입니다.

여기서 오토 레이아웃이란

디바이스의 방향, 스크린 사이즈, 혹은 일어날 어떠한 변화에 맞춰서 dynamic 하게 가능한 공간에 UI를 생성해주는 기능

을 말합니다. 

잠시 예시를 들어볼까요?

카카오톡의 대화창 TableView의 Cell 중 한 부분입니다.

그냥 View들을 배치할 수도 있지만, 만약 StackView를 사용한다면,

StackView 안에 가로축으로  [ (이미지 View), (발신자 + 톡 내용 첫번째 줄로 구성된 View), (시간, 안읽은 메세지 수로 구성된 View) ] 를 배치할 수 있겠네요.

 

그냥 뷰를 배치하는 것과 스택뷰에 넣어서 배치하는 것을 구분짓는 기준은

스택뷰에 들어가는 뷰가 런타임 도중 동적으로 제거되거나 추가될 때 에 기존 뷰들의 레이아웃이 바뀌냐 안바뀌냐에 따라 나눌 수 있습니다.

예를 들어

"위 카톡 대화창에서 이미지뷰가 사라질 때 나머지 뷰들이 이미지뷰가 사라진 만큼 왼쪽으로 땡겨져야 한다!"

라는 기획안이 나온다면 이미지뷰의 유무에 따라 오토레이아웃이 적용되어야 하므로 스택뷰에 넣어야겠죠.

 

만약 이런 변화 없이 그냥 이미지뷰만 띠용 하고 날아가게 해주세요. 라고한다면 오토 레이아웃을 적용시킬 필요가 없으니

굳이 스택뷰를 쓰지 않아도 되겠죠. 오히려 개발 복잡도만 증가시킬 수도 있겠네요.

 

다시 본론으로 돌아가서..

오토 레이아웃은 어떤식으로 적용시키는 걸까요?

StackView는 "Arranged Subviews" 프로퍼티에 들어있는 모든 뷰의 레이아웃을 관리합니다.
이 뷰들은 StackView의 arrangedSubviews 배열의 순서에 따라 StackView의 축(axis)을 따라 배치됩니다.
더 정확히 하면, 레이아웃은 StackView의 축(axis), distribution(분배), alignment(정렬방식), spacing(여백)에 따라 배치됩니다.

1. axis(축) 2. distribution(분배) 3. alignment(정렬) 4. spacing(여백)

이 프로퍼티들을 이용해 스택뷰의 arrangeSubviews 배열에 들어있는 View들의 Layout을 정해주는 것이었습니다.

 

더 읽어보면,

StackView를 사용하기 위해, 스토리보드를 엽니다. (....) 인터페이스 빌더는 이것의 content에 맞춰 StackView의 사이즈를 재조정합니다. 또한 사용자는 StackView의 Attribute를 조정해서 StackView의 컨텐츠들을 조정할 수 있다.

스택뷰의 내부 Content로 StackView를 resize할 수 있고, 반대로 StackView의 프로퍼티를 조정함으로써 내부 Content들의 크기를 조절할 수 있다고 합니다.

 

더 주의해야할 사항으로는...

사용자는 StackView의 position과 size(옵션)를 정의해줘야합니다. 그래야 스택뷰가 contents들의 레이아웃과 사이즈(옵션)을 조정할 수 있습니다.

스택뷰의 position은 무조건 지정해주어야 하고, 그 안의 컨텐츠들의 사이즈까지 조정하려면 사이즈도 지정해줘야 하는군요!

 

여기까지가 개발자문서의 개요 부분이었습니다.

중요한 내용은 거의 학습한 것 같아요. 다시 한번 정리해보자면....

https://developer.apple.com/documentation/uikit/uistackview

StackView란 Auto layout을 적용해 내부에 배치된 View들을 열 또는 행에 배치해주는 인터페이스 입니다.

이 때, Auto Layout이 적용되는 방법은 StackView의 Axis, Distribution, Alignment, Spacing 플래그에 따라 결정됩니다.

StackView 내부의 View들은 arrangeSubviews 배열에 의해 관리됩니다.

StackView를 사용하려면 Position을 정확히 지정해줘야하며, 상황에 따라 Size를 지정해줄 수 있습니다.

 


1-2. StackView와 Auto Layout

스택뷰는 arrangeSubviews들의 포지션과 사이즈를 맞추기 위해 오토 레이아웃을 사용합니다.

오토 레이아웃을 적용해주기 위해 StackView의 속성들을 세팅해주어야 하는데요.

속성들에는 1. Axis 2. Distribution 3. Alignment 4. Spacing 이 있습니다.

이제부터 그 속성들을 톺아보도록 하겠습니다..!

 

1-2-1. UIStackView.Axis

StackView는 Stack의 Axis(축)의 각 Edges(가장자리)에 맞춰 Arranged View의 첫번째, 마지막 View를 배치한다.
이는 Horizontal stack(가로축의 스택뷰)에서는 첫번째 뷰의 왼쪽 엣지가 스택뷰의 왼쪽 엣지와 동일하게 배치됨을 의미하며, 마지막 뷰의 오른쪽 엣지가 스택뷰의 오른쪽 엣지와 동일하게 배치됨을 의미한다.
Vertical stack(세로축 스택뷰)에서는 맨 위쪽 엣지와 맨 아래쪽 엣지가 스택뷰의 맨 위, 맨 아래 엣지와 동일하게 배치됨을 의미한다.

정리해보면,

 

1. Axis가 Horizontal(가로 방향)로 설정된 StackView의 경우

  • 첫번째 View의 왼쪽 Edge == StackView의 왼쪽 Edge
  • 마지막 View의 오른쪽 Edge == StackVIew의 오른쪽 Edge

2. Axis가 Vertical(세로 방향)로 설정된 StackView의 경우

  • 첫번째 View의 Top Edge == StackView의 Top Edge
  • 마지막 View의 Bottom Edge == StackView의 Bottom Edge

라고 정리할 수 있겠네요.

 

이와 관련된 프로퍼티가 하나 있는데요.

isLayoutMarginsRelativeArrangement

플래그를 True로 설정할 시, StackView는 Content의 Edge 기준이 아닌 스택뷰 - 컨텐츠 사이의 Layout에 마진 값을 두어 ArrangeViews들을 배치할 수 있습니다.

        let stvPractice = UIStackView()
        stvPractice.then {
            $0.isLayoutMarginsRelativeArrangement = true
            $0.axis = .horizontal
            $0.layoutMargins.left = 15.0
            $0.layoutMargins.right = 15.0
        }

이런식으로 설정한다면,

첫번째 뷰와 스택뷰의 leading Edge가 15.0, 마지막 뷰와 스택뷰의 Trailing Edge가 15.0만큼의 마진값을 두고 배치될 것 입니다.

 

여기까지는 Axis에 따른 StackView의 Auto Layout 사용법인 것 같습니다.

Axis(축)은 의미 그대로 가로 방향(Horizontal)축, 세로 방향(Vertical)축이 있습니다.

 

 


1-2-2. UIStackView.Distribution

다음은 Distribution(분배)에 관한 내용입니다.

StackView의 Axis(축)을 따라 Arrange views들을 어떻게 분배할건지에 대한 속성.

UIStackView.Distribution.fillEqually 플래그만 제외하고 모든 Distribution 설정에서, StackView는 Axis(축)의 사이즈를 계산할 때 각 arrangedSubview들의 intrinsicContentSize프로퍼티를 이용한다.
UIStackView.Distribution.fillEqually 플래그는 StackView의 Axis(축)에 따라 StackView를 채우기 위해 모든 Arrange Views들의 사이즈를 동일하게 재조정 한다. 가능하다면, StackView는 모든 Arrange View들을 가장 긴 Size를 가진 View에 맞춰 늘리기를 시도한다.

 

intrinsicContentSize는 직역하면 "고유한 컨텐츠 사이즈" 입니다.

 

새롭게 배운 개념이라 따로 정리해두기 전에 간단히 정리하자면,

대부분의 View들은 Content 크기에 맞게 따로 width, height을 지정해주지 않아도 자동으로 auto layout이 적용됩니다.

그 이유는 intrinsicContentSize에서 컨텐츠 크기에 맞는 사이즈를 계산해주기 때문입니다!

 

따라서 StackView에서도 Distribution.fillEqaully 플래그를 제외하고는 ArrangeView들의 intrinsicContentSize 프로퍼티를 이용해 Axis에 따른 StackView의 사이즈를 계산해줍니다.

 

Distribution 속성의 플래그에 무엇이 있는지 잠시 정리하고 갈 필요가 있겠군요.

 

1. fill

StackView의 Axis(축)을 따라 가능한 공간을 모두 채우기 위해 Arrange view들의 사이즈를 재조정하는 플래그.
arrange views들이 StackView의 크기를 초과한다면, 각 뷰의 compression resistance priority에 따라 각 뷰의 크기를 감소시킨다.
arrange views들이 StackView의 크기에 미달이라면, 각 뷰의 hugging priority에 따라 각 뷰를 늘린다.
만약 뭔가 모호한 점(ambiguity)가 생긴다면, StackView는 arrangedSubviews 배열의 인덱스에 기초하여 각 뷰들의 크기를 재조정한다.

오.. 처음보는 개념이 너무 많습니다.

compression resistance priority, hugging priority가 뭘까요?

나중에 따로 정리하겠지만...우선 간단히 정리하자면

 

  • compression resistance priority
    • 최소 크기에 대한 저항
    • compression(짜부) 에 대한 resistance(저항)
    • 숫자가 클수록 안작아질거야! 가 강해짐
  • hugging priority
    • 최대 크기에 대한 저항
    • 숫자가 클수록 안늘어날꺼야! 가 강해짐

따라서 shrink 해야할 때는 각 view들의 compression resistance priority를 비교해 제일 낮은 뷰 부터 크기를 감소시키고,

stretch 해야할 때는 각 view들의 hugging priority를 비교해 제일 낮은 뷰 부터 크기를 증가시킵니다.

 

따라서 각 뷰의 priority를 모르는 상태에서는 무슨 view가 늘어나거나 줄어들지 모르겠네요.

추가로 뭔가 모호한 점이 있으면 ArrangeSubviews 배열에 들어간 인덱스순대로 뷰들의 크기가 재조정 된다고 합니다.

 

2. fillEqually

StackView의 Axis(축)을 따라서 가능한 공간을 채우기위해 ArrangedSubView들을 리사이징 합니다. 뷰들은 StacKView의 축을 따라 모두 같은 사이즈를 갖기위해 재조정됩니다.

말 그대로 스택뷰의 축을 따라 모두 같은 크기로 분배되도록 하는 옵션입니다.

 

3. fillProportionally

StackView는 Axis(축)에 따라 가능한 공간을 모두 채우기위해 arrangedViews들을 리사이징한다. 뷰들은 그들의 intrinsic content size에 기초하여 비례적으로 사이즈를 재조정한다.

일단 StackView를 채우고, 남은 공간이 생긴다면 intrinsic content size 의 비율에 맞게 공간을 분배하여 resize된다고 합니다.

남은 공간이 100이고, view 3개의 intrinsic content size의 비율이 1:4:5라면, 남은 공간도 10:40:50 대로 분배되어 재조정된다는 뜻이겠네요.

 

4. equalSpacing

StackView의 Axis(축)을 따라 가능한 공간을 채우기 위해 ArrangedSubview들의 위치를 재조정한다.
뷰들이 StackView를 채우기에 부족하다면, StackView는 뷰들 사이의 공간을 균일하도록 재배치 한다.
뷰들이 StackView를 초과한다면, 뷰들의 compression resistance priority 에 따라 뷰의 사이즈를 감소시닌다.
만약 모호함이 있다면, StackView는 arrangedsSubViews 배열의 인덱스에 따라 뷰들을 감소시킨다.

StackView 내부의 View들 사이의 공간을 균등하게 배치하는 옵션입니다.

 

5. equalCentering

StackView의 Axis(축)에 따라 각 View들의 center - center간 거리를 동일하게 View의 위치를 재조정한다.
이 때, StackView의 spacing 프로퍼티의 값만큼의 거리는 유지해야 한다.
뷰들이 StackView를 초과한다면, spacing 프로퍼티에 정의된 최소 거리만큼을 채울 때 까지 간격을 좁힌다.
그래도 초과한다면, 각 뷰의 compression resistance priority에 의거해서 View들을 감소시킨다.
만일 더 모호함이 있다면, StackView는 arrangedSubView 배열의 인덱스에 따라 뷰들을 감소시킨다.

각 뷰의 center - center간 길이를 동일하게 맞추는 옵션입니다.

어떤 예시가 있을지 궁금하군요..

 

Distribution 플래그에 관한 기본적인 내용은 여기까지 입니다.

 


1-2-3. UIStackView.Alignment

StackView의 Axis(축)에 수직인 뷰들의 레이아웃을 정하는 Flag

라고 뭔가 좀 어렵게 적혀있지만 StackView가 어떤식으로 하위 뷰들을 정렬할 것인지에 대한 플래그입니다.

 

1. fill

StackVIew의 Axis(축)과 수직인 방향으로 가능한 공간을 채우기위해 View들의 사이즈를 재조정 합니다.

Axis가 Horizontal인 경우, 아래 위의 공간을 fill 하기 위해 늘리고,

Axis가 Vertical인 경우, 좌우 공간을 fill 하기 위해 늘리는 플래그입니다.

 

2. leading

세로 축(Vertical) StackView에서 View들의 leading Edge와 StackView의 leading Edge에 맞춰 정렬합니다.
가로 축(Horizontal) StackView에서 Top 플래그와 동일합니다.

 

3. top

가로 축(Horizontal) StackVIew에서 View들의 Top Edge와 StackView의 Top Edge에 맞춰 정렬합니다.
세로 축(Vertical) StackView에서 leading 플래그와 동일합니다.

 

4. firstBaseline

View들의 first baseline에 맞춰 StackView가 View들을 정렬하는 것을 말합니다.
이 정렬은 오직 가로 축 (Horizontal) StackView에서만 유효합니다.

 

5. center

StackView가 축을 따라서 View들의 center를 StackView의 Center에 맞춰 정렬하는 것을 말합니다.

6. trailing

세로 축(Vertical) Stack의 Trailing Edge와 뷰들의 Trailing Edge를 맞춰 정렬하는 플래그 입니다.
가로 축(Horizontal) Stack의 Bottom 정렬과 동일합니다.

7. bottom

가로 축(Horizontal) Stack의 Bottom Edge와 뷰들의 Bottom Edge를 맞춰 정렬하는 플래그 입니다.
세로 축(Vertical) Stack의 Trailing 정렬과 동일합니다.

8. lastBaseline

StackView가 View들의 last baseline에 맞춰 View들을 정렬하는 플래그 입니다.
이 정렬은 오직 가로 축(Horizontal) StackView에서만 유효합니다.

 

StackView에 대한 이론은 이쯤하면 얼추 살펴본듯 합니다.

이제 예제를 만들어보죠..!


 

2. 코드로 구현해보기

많이 등장하는 형태를 코드로 구현해보겠습니다.

스택뷰 속성들에 대해 공부했음에도 불구하고 막상 코드로 구현하려니 헷갈리고 어렵네요...ㅠㅠ!

 

위의 예시에 들었던 카톡의 형태를....

저의 개인 프로젝트에 적용해보겠습니다....!

 

위에도 설명했듯, 스택뷰 안에 [ (이미지뷰), (정보뷰), (하트뷰) ] 를 넣어보았습니다.

코드는 아래와 같습니다.

 

       let vContent = UIView()
        self.addSubview(vContent)
        vContent.snp.makeConstraints {
            $0.top.bottom.equalToSuperview()
            $0.left.equalToSuperview().offset(16.0)
            $0.right.equalToSuperview().offset(-16.0)
        }
        
        let stvCell = UIStackView()
        vContent.addSubview(stvCell)
        stvCell.then {
            $0.axis = .horizontal
            $0.distribution = .fill
            $0.alignment = .center
            $0.spacing = 10.0
        }.snp.makeConstraints {
            $0.left.right.centerY.equalToSuperview()
        }

우선 Cell 전체를 감싸주는 컨테이너 뷰를 만들었고,

그 안에 StackView를 넣어줬습니다.

 

StackView의 속성 설정은...

가로 축이기 때문에 Axis는 horizontal,

각 View가 Spacing을 유지하며 Cell을 채우게 하기 위해 Distribution은 fill,

정렬은 역시 가운데가 예쁘기 때문에 center로 잡아주었습니다.

 

stackview의 높이는 안의 컨텐츠가 잡아주게 하기위해 top, bottom은 설정하지 않았고

가로, 세로, center을 cell과 동일시하기 위해 equalToSuperView로 잡아주었습니다.

 

       stvCell.addArrangedSubview(self.ivFestival)
        self.ivFestival.then {
            $0.contentMode = .scaleAspectFill
            $0.clipsToBounds = true
        }.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.height.equalTo(80.0)
            $0.width.equalTo(ivFestival.snp.height).multipliedBy(1.5)
        }

ImageView입니다.

가운데 정렬을 위해 centerY를 부모와 맞춰주었고,

이미지별로 ImageView의 크기가 달라지지 않게 하기 위해 사이즈를 정해주고, contentMode를 scaleAspectFill로 지정해주고 clipsToBounds를 주었습니다.

 

        let vFestivalInfo = UIView()
        stvCell.addArrangedSubview(vFestivalInfo)
        vFestivalInfo.snp.makeConstraints {
            $0.top.bottom.equalToSuperview()
        }
        
        
        // 행사 이름
        vFestivalInfo.addSubview(self.lbTitle)
        self.lbTitle.then {
            $0.textColor = .black
            $0.textAlignment = .left
        }.snp.makeConstraints {
            $0.top.left.right.equalToSuperview()
            $0.height.equalTo(20.0)
        }
        
        
        // 행사 주소
        vFestivalInfo.addSubview(self.lbAddr)
        self.lbAddr.then {
            $0.textColor = .black
            $0.textAlignment = .left
        }.snp.makeConstraints { [unowned self] in
            $0.top.equalTo(self.lbTitle.snp.bottom).offset(10)
            $0.height.equalTo(20.0)
            $0.left.right.equalTo(self.lbTitle)
        }
        
        // 행사 일정란
        vFestivalInfo.addSubview(self.lbDate)
        self.lbDate.then {
            $0.textColor = .black
            $0.textAlignment = .left
        }.snp.makeConstraints { [unowned self] in
            $0.left.right.equalTo(self.lbTitle)
            $0.top.equalTo(self.lbAddr.snp.bottom).offset(10)
            $0.bottom.equalToSuperview()
            $0.height.equalTo(20.0)
        }
        

인포뷰입니다.

스택뷰 자체에 top, bottom을 주지않았기 때문에 이 인포뷰로 StackView의 Top,bottom이 정해지게 됩니다.

(다른게 더 커지면 그 뷰를 기준으로 얘도 늘어나겠죠. 이걸 방지하려면 StackView자체에 사이즈를 정해주거나 각 View에 사이즈를 직접 지정해주면 될 것 같습니다.)

그리고 안에 들어가는 라벨들의 height을 정해주고, 제일 위에있는 라벨의 Top과 제일 아래에 있는 라벨의 Bottom을 View와 맞춰줍니다. 이렇게 함으로써 StackView 내부의 컨텐츠로 StackView 자체의 높이가 정해지게 되겠죠!

 

       let vIconInfo = UIView()
        stvCell.addArrangedSubview(vIconInfo)
        vIconInfo.snp.makeConstraints {
            $0.top.bottom.equalToSuperview()
            $0.width.equalTo(30.0).priorityHigh()
        }
        
        let ivHeart = UIImageView()
        vIconInfo.addSubview(ivHeart)
        ivHeart.then {
            $0.image = UIImage(named: "heart_full")
            $0.contentMode = .scaleAspectFit
            $0.translatesAutoresizingMaskIntoConstraints = false
        }.snp.makeConstraints {
            $0.top.equalToSuperview()
            $0.left.right.equalToSuperview()
        }
        
        let lbHeart = UILabel()
        vIconInfo.addSubview(lbHeart)
        lbHeart.then {
            $0.text = "하트"
            $0.textColor = .black
        }.snp.makeConstraints {
            $0.bottom.equalToSuperview()
            $0.left.right.equalToSuperview()
        }

하트뷰입니다.

인포뷰와 기본적으로 동일하지만, 하트뷰의 width를 고정시키고 싶어서 width를 따로 설정해주었습니다.


 

업무 중 예시에 나와있는 형태를 StackView로 구현하려다가 어려움을 겪어서 StacKView 속성에 대해 낱낱히 파헤쳐보자! 라는 마음으로 이 글을 쓰게 되었는데요,

개발자 문서를 정독하고 직접 코드로 구현도 해봤지만 아직 StackView를 능숙하게 쓰진 못하겠네요 ㅠㅠㅠ

속성도 제가 머리로 알고있는것과 직접 실행시켰을 때의 결과가 달라 지금도 많이 애먹고 있습니다.. 계속 하다보면 늘겠죠

아무튼 더 많은 형태의 예시를 이 글에 계속 추가하도록 하겠습니다!