SwiftUI

SwiftUI - 스택 정렬과 정렬 가이드

iosos 2024. 2. 2. 20:05
  • 컨테이너 정렬, 정렬 가이드, 커스텀 정렬, 서로 다른 스택들 간의 정렬 구현까지 스택 정렬의 고급 기술 설명

 

 

컨테이너 정렬

  • 스택 사용 시 가장 기본적인 정렬 방법은 컨테이너 정렬
    • 스택에 포함된 하위 뷰들이 스택 내에서 정렬되는 방식을 정의함
  • ‘암묵적으로 정렬되었다’
    • 스택에 포함된 각각의 뷰에 지정된 정렬이 따로 없어, 스택에 적용한 정렬이 하위 뷰에 적용되는 것
    • 개별적으로 적용된 정렬이 없는 하위 뷰에 상위 뷰의 정렬 방법이 적용되는 것
  • 수평 스택 (HStack) : 하위 뷰를 수직 방향 정렬
  • 수직 스택 (VStack) : 하위 뷰를 수평 방향 정렬
  • ZStack : 하위 뷰를 수평/수직 방향 정렬
  • 세 개의 하위 뷰를 포함하는 VStack 구성
VStack{
            Text("This is some text")
            Text("This is some longer text")
            Text("This is short")
}

 

 

 

→ 컨테이너 정렬 값이 없으면 하위 뷰들은 모두 중앙 정렬(.center)이 디폴트 값

 

 

 

 

 

  • leading, trailing 정렬 사용
      	VStack{
            Text("This is some text")
            Text("This is some longer text")
            Text("This is short")
        }
        .padding(50)
        
        VStack(alignment: .leading){
            Text("This is some text")
            Text("This is some longer text")
            Text("This is short")
        }
        .padding(50)
        
        VStack(alignment: .trailing){
            Text("This is some text")
            Text("This is some longer text")
            Text("This is short")
        }
        .padding(50)

 

 

 

 

  • HStack(수평 스택) 또한, 디폴트로 중앙 정렬이 되지만, 텍스트 베이스 라인 정렬을 위한 값뿐만 아니라 상단 정렬과 하단 정렬 제공
  • 정렬을 지정할 때 여백 값 포함 가능
  • HStack의 디폴트로 여백이 있는 중앙 정렬 사용, 서로 다른 폰트 크기를 가진 세 개의 하위 뷰 포함
HStack(spacing: 30){
            Text("This is some text")
                .font(.largeTitle)
            
            Text("This is some much longer text")
                .font(.body)
            
            Text("This is short")
                .font(.headline)
        }

 

 

 

 

 

 

 

  • 텍스트 베이스라인 정렬은 텍스트 기반 뷰의 첫 줄(.firstTextBaseline) 또는 마지막 줄(.lastTextBaseline)을 기준으로 둠
	HStack(alignment: .firstTextBaseline, spacing: 30){
            Text("This is some text")
                .font(.largeTitle)
            
            Text("This is some much longer text")
                .font(.body)
            
            Text("This is short")
                .font(.headline)
        }
        .padding(.bottom, 50)
        
        
        
        
        HStack(alignment: .lastTextBaseline, spacing: 30){
            Text("This is some text")
                .font(.largeTitle)
            
            Text("This is some much longer text")
                .font(.body)
            
            Text("This is short")
                .font(.headline)
        }

 

 

 

 

 

 

 

 

 

 

 

정렬 가이드(alignment guide)

  • 뷰가 스택에 포함된 다른 뷰와 정렬해야 할 때 사용되는 커스텀 포지션을 정의하는 데 사용
  • 복잡한 정렬 구현 가능
  • ex) 길이의 3분의 2 위치 또는 상단에서 20포인트를 기준으로 뷰를 정렬
  • 표준 정렬 타입과 클로저를 인자로 받는 alignmentGuide() 수정자를 사용하여 적용
  • 클로저는 표준 정렬 기준으로 하는 뷰 내에 위치(포인트)를 가리키는 값을 계산하여 반환
  • 뷰 내의 정렬 위치 계산을 돕기 위해 뷰의 폭과 넓이를 얻는데 사용하는 ViewDimensions 객체와 뷰의 표준 정렬 위치 (.top .bottom 등)가 클로저에 전달됨
  • VStack은 세 개의 서로 다른 길이와 색상을 가진 사각형을 가지고, 모두 앞쪽 정렬한 코드
VStack(alignment: .leading){
            Rectangle()
                .foregroundColor(.green)
                .frame(width:120, height: 50)
            Rectangle()
                .foregroundColor(.red)
                .frame(width:200, height: 50)
            Rectangle()
                .foregroundColor(.blue)
                .frame(width:180, height: 50)
        }

 

 

 

 

 

  • 두 번째 뷰만 앞쪽에서 120포인트 안쪽으로 들어가게 정렬
VStack(alignment: .leading){
            Rectangle()
                .foregroundColor(.green)
                .frame(width:120, height: 50)
            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(.leading, computeValue: { dimension in
                    120.0
                })
                .frame(width:200, height: 50)
            Rectangle()
                .foregroundColor(.blue)
                .frame(width:180, height: 50)
        }

→ 정렬 가이드를 사용할 때는 alignmentGuide() 수정자에 지정된 정렬 타입은 부모 스택에 적용된 정렬 타입과 일치해야 함

VStack(alignment: .leading){
           ...

            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(.leading, computeValue: { dimension in
                    120.0
                })
                .frame(width:200, height: 50)

          ...
        }

 

 

 

 

 

 

 

 

  • offset을 하드코딩하는 대신, 클로저에 전달된 ViewDimensions 객체 (위 예제의 ‘dimension’)의 프로퍼티를 정렬 가이드 위치를 계산하는 데 이용 가능함
  • ex) width 프로퍼티를 이용하면 뷰의 앞쪽 3분의 1 위치로 배치
VStack(alignment: .leading){
            Rectangle()
                .foregroundColor(.green)
                .frame(width:120, height: 50)
            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(.leading, computeValue: { dimension in
                    dimension.width / 3
                })
                .frame(width:200, height: 50)
            Rectangle()
                .foregroundColor(.blue)
                .frame(width:180, height: 50)
        }

 

 

 

 

 

 

  • ViewDimension 객체는 뷰의 HorizontalAlignment와 VerticalAlignment 프로퍼티에 대한 접근 제공
  • 뷰의 끝쪽에 20포인트를 추가로 더하는 예제
Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(.leading, computeValue: { dimension in
                    dimension[HorizontalAlignment.trailing] + 20
                })

→ 뷰의 끝 쪽은 다른 뷰의 앞쪽에서 20포인트 더해진 위치로 정렬 됨

 

 

 

 

 

 

 

커스텀 정렬 타입

  • 커스텀 정렬 타입을 선언하여 표준 타입들을 확장
  • ex) .oneThird 이름의 커스텀 정렬 타입은 뷰의 지정된 끝쪽에서부터 3분의 1 거리 위치로 정렬하게 생성
  • 수직 방향으로 중앙에 위치하는 네 개의 사각형으로 구성된 HStack
HStack(alignment: .center){
            Rectangle()
                .foregroundColor(.green)
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.red)
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.blue)
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.orange)
                .frame(width: 50, height: 200)
        }

 

→ 위 사각형 중 하나 이상의 사각형만 정렬을 변경할 때 정렬 가이드 사용 가능

 

 

 

  • 다른 방법으로는 여러 뷰에 적용될 수 있는 커스텀 정렬 생성하는 방법
  • 계산된 값을 반환하는 새로운 정렬 타입을 추가하기 위하여 VerticalAlignment, HorizontalAlignment를 확장
  • 새로운 수직 정렬 타입을 생성하는 예제
extension VerticalAlignment{
    private enum OneThird : AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context.height / 3   // 뷰 높이의 3분의 1이 반환 됨 
        }
    }
    
    static let oneThird = VerticalAlignment(OneThird.self)
}
  • 이 extension은 AlignmentID 프로토콜을 따라는 열거형(enum)을 포함해야 함
  • defaultValue()라는 이름의 함수가 구현되도록 지시함
  • 이 함수는 뷰에 대한 ViewDimensions 객체를 받음
  • 정렬 가이드 위치를 가리키는 계산된 CGFloat 값을 반환함

 

  • 커스텀 정렬은 HStack 선언부에 사용 가능
HStack(alignment: .oneThird){
            Rectangle()
                .foregroundColor(.green)
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(.oneThird, computeValue: { dimension in
                    dimension[VerticalAlignment.top]
                })
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.blue)
                .frame(width: 50, height: 200)
            Rectangle()
                .foregroundColor(.orange)
                .alignmentGuide(.oneThird, computeValue: { dimension in
			dimension[VerticalAlignment.bottom]
                })
                .frame(width: 50, height: 200)
        }

→ 빨간 뷰는 상단 기준, 오렌지 뷰는 하단 기준으로 정렬됨

 

 

 

 

 

스택 정렬 교차

  • 표준 정렬 타입의 단점 : 스택 내의 뷰가 다른 스택에 있는 뷰와 정렬되도록 하는 방법을 제공하지 않음
  • HStack 안에 VStack이 포함된 스택 구성도
HStack(alignment: .center, spacing: 20){
            Circle()
                .foregroundColor(.purple)
                .frame(width: 100, height: 100)
            
            VStack{
                Rectangle()
                    .foregroundColor(.green)
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.red)
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.blue)
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.orange)
                    .frame(width: 100, height: 100)
            }
        }

 

  • VStack과 원을 나타내는 뷰는 HStack 내에서 수직 방향으로 중앙 정렬되어 있음
  • 만약 원을 HStack에 있는 맨 위의 사각형이나 맨 아래 사각형과 정렬하고 싶다면 HStack 정렬을 .top, .bottom으로 변경하면 됨
  • 그러나, 두 번째, 세 번째 사각형과 정렬 가능한 표준 정렬 타입은 없음 → 커스텀 정렬 사용

 

  • 뷰의 아래쪽을 기준으로 한 정렬 값을 반환하는 커스텀 정렬 구현
extension VerticalAlignment {
    private enum CrossAlignment : AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[.bottom]
        }
    }
    static let crossAlignment = VerticalAlignment(CrossAlignment.self)
}

 

 

 

  • 만든 커스텀 정렬은 서로 다른 스택에 포함된 뷰를 정렬하기 위해 사용 가능함
  • 원 뷰의 아래쪽이 VStack에 포함된 세 번째 사각형과 정렬 코드
HStack(alignment: .crossAlignment, spacing: 20){
            Circle()
                .foregroundColor(.purple)
                .alignmentGuide(.crossAlignment, computeValue: { dimension in
                    dimension[VerticalAlignment.center]
                })
                .frame(width: 100, height: 100)
            
            VStack{
                Rectangle()
                    .foregroundColor(.green)
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.red)
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.blue)
                    .alignmentGuide(.crossAlignment, computeValue: { dimension in
                        dimension[VerticalAlignment.center]
                    })
                    .frame(width: 100, height: 100)
                Rectangle()
                    .foregroundColor(.orange)
                    .frame(width: 100, height: 100)
            }
        }

 

 

 

 

 

ZStack 커스텀 정렬

  • 디폴트로 하위 뷰는 중앙 정렬된 상태로 위로 겹치게 쌓임
  • 표준 정렬을 이용하면 정렬을 바꿀 수 있음
  • ZStack에서 커스텀 정렬을 사용하려면 수평 커스텀 정렬과 수직 커스텀 정렬을 결합해야 함
extension HorizontalAlignment {
    enum MyHorizontal : AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[HorizontalAlignment.center]
        }
    }
    static let myAlignment = HorizontalAlignment(MyHorizontal.self)
}

extension VerticalAlignment {
    enum MyVertical : AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[VerticalAlignment.center]
        }
    }
    static let myAlignment = VerticalAlignment(MyVertical.self)
}

extension Alignment {
    static let myAlignment = Alignment(horizontal: .myAlignment, vertical: .myAlignment)
}

→ 커스텀 정렬 구현 (ZStack)에서 사용 가능 myAlignment

 

 

 

 

 

ZStack(alignment: .myAlignment){
            Rectangle()
                .foregroundColor(.green)
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.trailing]
                })
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[VerticalAlignment.bottom]
                })
                .frame(width: 100, height: 100)
            
            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[VerticalAlignment.top]
                })
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[HorizontalAlignment.center]
                })
                .frame(width: 100, height: 100)
            
            Circle()
                .foregroundColor(.orange)
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.leading]
                })
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.bottom]
                })
                .frame(width: 100, height: 100)
        }

 

 

 

 

 

 

 

 

  • 정렬 설정 변경 후 테스트
ZStack(alignment: .myAlignment){
            Rectangle()
                .foregroundColor(.green)
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.leading]
                })
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[VerticalAlignment.bottom]
                })
                .frame(width: 100, height: 100)
            
            Rectangle()
                .foregroundColor(.red)
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[VerticalAlignment.center]
                })
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[HorizontalAlignment.trailing]
                })
                .frame(width: 100, height: 100)
            
            Circle()
                .foregroundColor(.orange)
                .alignmentGuide(HorizontalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.leading]
                })
                .alignmentGuide(VerticalAlignment.myAlignment, computeValue: { dimension in
                    dimension[.top]
                })
                .frame(width: 100, height: 100)
        }