SwiftUI

[SwiftUI] Background, Grid, ScrollView 등(+View)

1000JI 2024. 9. 8. 23:28

 

안녕하세요. 1000JI입니다 :)
저번 글에 이어서 계속 SwiftUI View 관련 코드를 이어가려고 합니다.

 


 

Background & Overlay
첫 번째 예제

Text 또는 Circle 등 View를 생성 한 뒤 배경 설정 또는 맨 앞에 배치하는 방법에 대한 내용 입니다.

Text("Hellow, World!")
    .frame(width: 100, height: 100)
    .background(
        Circle()
            .fill(
                LinearGradient(
                    gradient: Gradient(
                        colors: [
                            Color.red,
                            Color.blue
                        ]
                    ),
                    startPoint: .leading,
                    endPoint: .trailing
                )
            )
    )
    .frame(width: 120, height: 120)
    .background(
        Circle()
            .fill(Color.red)
    )

 

위 코드를 살펴보게 되면, Text("Hellow, World!")를 생성 한 뒤 frame, background 설정을 하고 있습니다.

코드 그대로 텍스트의 사이즈를 100으로 설정한 후 background에 Circle을 생성하고 있습니다.

frame의 크기를 그대로 받아 생성하기 때문에 Circle의 사이즈도 100으로 설정 됩니다.

첫 번째 예제 결과

 

두 번째 예제
Circle()
    .fill(Color.pink)
    .frame(width: 100, height: 100)
    .overlay(
        Text("1")
            .font(.title)
            .foregroundColor(Color.white)
    )
    .background( // Background 뒤쪽에 깔리는 도형/색상을 지정 할 수 있음.
        Circle()
            .fill(Color.purple)
            .frame(width: 120, height: 120)
    )

 

overlay를 통해 Circle 위에 텍스트를 삽입 하였습니다.

이후 background를 사용하여 뒤쪽에 도형을 배치하였습니다.

두번째 예제 결과

 

 

 

Stack

Stack 뷰는 정말 다양하게 활용 할 수 있으며, 필수적인 뷰 중에 하나 입니다 :)

 

첫 번째 예제(ZStack, VStack, HStack)

1. ZStack

  • 개요: ZStack은 자식 뷰를 Z축(깊이 축)으로 쌓는 스택입니다. 즉, 뷰를 겹쳐 쌓아올려서 배치합니다.
  • 사용 예시: 배경 이미지 위에 텍스트를 겹쳐 놓거나, 여러 개의 뷰를 한 위치에 겹쳐서 놓아야 할 때 사용합니다.

2. HStack

  • 개요: HStack은 자식 뷰를 가로축(X축)으로 나란히 배치하는 스택입니다.
  • 사용 예시: 여러 개의 뷰를 수평으로 정렬해야 할 때 사용합니다. 예를 들어, 여러 개의 버튼이나 이미지를 가로로 나란히 배치하고자 할 때 적합합니다.

3. VStack

  • 개요: VStack은 자식 뷰를 세로축(Y축)으로 나란히 배치하는 스택입니다.
  • 사용 예시: 여러 개의 뷰를 수직으로 정렬해야 할 때 사용합니다. 예를 들어, 리스트 형태로 텍스트나 이미지 등을 세로로 배치하고자 할 때 적합합니다.
ZStack (alignment: .top) {
    Rectangle()
        .fill(Color.yellow)
        .frame(width: 350, height: 500)

    VStack(alignment: .leading, spacing: 30) {
        Rectangle()
            .fill(Color.red)
            .frame(width: 150, height: 150)

        Rectangle()
            .fill(Color.green)
            .frame(width: 100, height: 100)

        HStack(alignment: .bottom, spacing: 10) {
            Rectangle()
                .fill(Color.purple)
                .frame(width: 50, height: 50)

            Rectangle()
                .fill(Color.pink)
                .frame(width: 75, height: 75)

            Rectangle()
                .fill(Color.blue)
                .frame(width: 25, height: 25)
        }
        .background(Color.white)
    }
    .background(Color.black)
}

첫 번째 예제 결과

두 번째 예제(ZStack vs Background)

ZStack과 Background를 활용해서 같은 뷰를 다른 방법으로 구현 할 수 있습니다.

 

VStack(spacing: 50) {
    // ZStack 을 사용해서 원에 1을 표현 - layer 가 복잡할때 ZStack 사용하면 좋음
    ZStack(alignment: .center) {
        Circle()
            .frame(width: 100, height: 100)

        Text("1")
            .font(.title)
            .foregroundColor(.white)
    }

    // Background 를 사용해서 원에 1을 동일하게 표현 - layer 가 단순할 경우 추천
    Text("1")
        .font(.title)
        .foregroundColor(.white)
        .background(
            Circle()
                .frame(width: 100, height: 100)
        )
}

 

두번째 예제 결과

 

 

Spacer

 

Spacer는 SwiftUI에서 레이아웃을 구성할 때 빈 공간을 추가하는 데 사용하는 뷰입니다. Spacer는 가변적인 크기의 빈 공간을 만들어서 다른 뷰들 사이에 일정한 간격을 유지하거나, 원하는 위치로 뷰를 밀어낼 수 있도록 해줍니다.

Spacer의 특징

  • 유연한 공간 제공: Spacer는 레이아웃에서 가능한 모든 여유 공간을 차지하려고 시도합니다. 여러 Spacer가 있을 경우, 해당 공간을 균등하게 나누어 가집니다.
  • 수평 및 수직 정렬에 사용: Spacer는 HStack(수평 스택)과 VStack(수직 스택)에서 모두 사용되어 빈 공간을 추가할 수 있습니다.
  • 간격 조절: Spacer를 사용하면 뷰 간의 간격을 자유롭게 조절할 수 있습니다. 예를 들어, Spacer를 HStack이나 VStack 내에서 사용하여 뷰들을 원하는 위치로 밀어낼 수 있습니다.
VStack {
    HStack(spacing: 0) {
        Image(systemName: "xmark")

        Spacer() // Spacer는 최대한 공간을 차지하려는 속성이 있기 때문에 좌우로 밀어냄

        Image(systemName: "gear")
    }
    .font(.title)
    .padding(.horizontal)

    Spacer()
}

 

 

ForEach

 

ForEach는 SwiftUI에서 반복적으로 뷰를 생성하고 배열하거나 목록을 만들 때 사용하는 구조입니다. 주어진 데이터 컬렉션을 순회하며 각 요소에 대해 뷰를 생성하여 화면에 표시할 수 있도록 도와줍니다.

ForEach의 특징

  • 반복적으로 뷰 생성: 주어진 데이터 컬렉션의 각 요소에 대해 반복적으로 뷰를 생성합니다.
  • 식별 가능한 데이터 사용: SwiftUI는 각 뷰를 고유하게 식별해야 하기 때문에, ForEach에서 사용하는 데이터는 고유한 식별자를 가져야 합니다. 식별자는 데이터가 변경되었을 때 SwiftUI가 뷰를 효율적으로 업데이트할 수 있도록 도와줍니다.
  • 동적 목록 생성: ForEach를 사용하면 고정된 개수가 아닌 데이터 기반의 동적인 개수의 뷰를 생성할 수 있습니다.
var data: [String] = ["Hi", "Hello", "Hey everyone"]

VStack {
    // View를 반복 사용하기 위해서 ForEach 구분을 사용해야 함.
    // 컬렉션 데이터 값을 기반으로 뷰를 계산하는 구조임.
    ForEach(0..<10) { index in
        HStack {
            Circle()
                .frame(width: 20, height: 20)

            Text("인덱스 번호 : \(index)번")
        }
    }

    Divider()

    // 2번
    /*
     * ForEach는 랜덤 액세스 컬렉션 타입임. 따라서 데이터 값에 하나씩 아이디 값을 넣어줘야함.
     * id 파라미터는 ForEach에서 각 항목을 고유하게 식별하기 위한 값입니다.
     * SwiftUI는 이 값을 사용하여 각 항목의 상태를 추적하고, 컬렉션의 데이터가 변경될 때 뷰를 최소한으로 업데이트할 수 있도록 합니다.
     * id: \.self를 사용하는 것은 데이터가 고유한 값을 가지고 있을 때 적합합니다.
       예를 들어, String이나 Int와 같은 기본 데이터 유형은 이미 고유성을 가지므로 id: \.self를 사용할 수 있습니다.
       하지만 커스텀 데이터 유형을 사용하는 경우, 그 데이터 타입이 Hashable을 준수하거나, 특정 속성(id)을 사용하여 고유 식별자를 제공해야 합니다.
     */
    ForEach(data, id: \.self) { item in
        Text(item)
    }
}

 

ScrollView

 

ScrollView는 SwiftUI에서 콘텐츠가 현재 화면에 다 담기지 않을 때 스크롤 가능한 영역을 만들어주는 뷰입니다. ScrollView는 사용자가 손가락을 움직여서 화면을 위아래 또는 양옆으로 이동할 수 있게 해주며, 긴 텍스트, 목록, 이미지, 또는 다양한 뷰들을 스크롤하여 볼 수 있도록 합니다.

ScrollView의 특징

  • 스크롤 가능한 콘텐츠 영역: ScrollView를 사용하면 콘텐츠가 화면 밖으로 넘칠 때 해당 콘텐츠를 스크롤할 수 있도록 만들어줍니다.
  • 수직 및 수평 스크롤: ScrollView는 수직(vertical) 또는 수평(horizontal)으로 스크롤할 수 있으며, 필요에 따라 스크롤 방향을 지정할 수 있습니다.
  • 동적 콘텐츠: 긴 리스트, 다량의 데이터 또는 동적 콘텐츠를 스크롤을 통해 쉽게 표시할 수 있습니다.
  • 내부 레이아웃 구성 지원: ScrollView 안에 다양한 레이아웃(VStack, HStack, ZStack 등)을 사용할 수 있습니다.

HStack과 LazyHStack은 SwiftUI에서 뷰를 수평으로 나란히 배치하는 데 사용하는 레이아웃 컨테이너입니다. 두 가지 모두 수평 방향으로 뷰를 배치하지만, 주요 차이점은 뷰의 로딩 방식에 있습니다.

주요 차이점

  1. 뷰 로딩 방식
    • HStack: 모든 자식 뷰를 한 번에 메모리에 로드합니다. 즉, HStack에 포함된 모든 뷰가 초기화될 때 즉시 메모리에 로드되고, 레이아웃이 설정됩니다.
    • LazyHStack: 필요할 때만 자식 뷰를 로드합니다. 스크롤 가능한 컨테이너 안에 있을 때, 화면에 나타나는 뷰들만 로드하고 나머지는 스크롤할 때 필요할 때 로드합니다. 따라서 많은 수의 뷰가 있을 때 성능이 더 효율적입니다.
  2. 사용 목적
    • HStack: 소수의 뷰가 있을 때 적합합니다. 모든 뷰를 한 번에 메모리에 로드하기 때문에, 소규모의 정적인 뷰를 나열할 때 사용합니다.
    • LazyHStack: 많은 수의 뷰가 있을 때 적합합니다. 특히 스크롤 가능한 컨테이너(예: ScrollView) 내에서 사용하여 성능을 최적화하고, 불필요한 메모리 사용을 줄입니다.
// 기본 값은 .vertical && showIndicators = true
ScrollView(.vertical, showsIndicators: true) {
    // 데이터 양이 많아지는 경우 LazyVStack, LazyHStack을 사용하게 되면 화면에 보여지는 부분만 로딩이 되고,
    // 스크롤을 하게 되면 나중에 정보가 업데이트 하는 방식임.
    LazyVStack {
        ForEach(0..<10) { _ in
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack {
                    ForEach(0..<20) { _ in
                        RoundedRectangle(cornerRadius: 25.0)
                            .fill(Color.white)
                            .frame(width: 200, height: 150)
                            .shadow(radius: 10)
                            .padding()
                    }
                }
            }
        }
    }
}

 

Grid

Grid는 SwiftUI에서 iOS 17, macOS 14, watchOS 10 및 tvOS 17 이후 사용할 수 있는 새로운 레이아웃 시스템으로, 행(row)과 열(column)로 구성된 격자 형태의 레이아웃을 만들 수 있게 해줍니다. Grid는 HStack과 VStack보다 유연하게 뷰를 배치할 수 있으며, 복잡한 레이아웃을 보다 직관적이고 간단하게 만들 수 있도록 도와줍니다.

Grid의 특징

  • 격자 형태의 배치: Grid는 행과 열을 사용하여 뷰를 배치합니다. 이를 통해 가로와 세로로 복잡한 레이아웃을 쉽게 구성할 수 있습니다.
  • 유연한 레이아웃 구성: 각 열과 행의 크기와 위치를 자유롭게 설정할 수 있어, 고정된 크기나 비율, 자동 크기 조절 등의 다양한 요구에 맞게 레이아웃을 구성할 수 있습니다.
  • 자동화된 셀 크기 조정: Grid는 자식 뷰의 크기에 따라 셀 크기를 자동으로 조정할 수 있습니다.
  • 컬럼 설정: 열의 개수와 크기를 정의하고, 각각의 열이 고정 너비 또는 비율에 따라 크기를 조정하도록 설정할 수 있습니다.

Grid의 주요 구성 요소

  • Grid: 행과 열로 구성된 격자 형태의 기본 레이아웃 컨테이너입니다.
  • GridRow: Grid 내에서 하나의 행을 정의하며, 행 안에 여러 개의 열을 포함할 수 있습니다.
  • 열 크기 및 정렬: 열의 크기를 고정된 값으로 지정하거나, 콘텐츠에 따라 자동으로 조정되도록 설정할 수 있습니다.
/*
 * List: 일반적 목록 배열 방식(일반적인 수직 방향으로 목록 표출)
 * Grid: box 형태로 나타내는 배열 방식(가로 세로 방향으로 사진 배열 같은 형태 표출)
 */

// LazyVGrid
// columns 의 갯수를 3개로 설정
// .flexible() -> Single Flexible Item 그 크기를 화면 프로엠이 맞춰줌
// .flexible(minimum: 10, maximum: .infinity)
let columns: [GridItem] = [
    GridItem(.flexible(), spacing: 6, alignment: nil),
    GridItem(.flexible(), spacing: 6, alignment: nil),
    GridItem(.flexible(), spacing: 6, alignment: nil),
]

var body: some View {

    // 1 - LazyVGrid
    ScrollView {

        // Hero 부분 (위에 사진 부분)
        Rectangle()
            .fill(Color.orange)
            .frame(height: 400)

        LazyVGrid(
            columns: columns,
            alignment: .center,
            spacing: 6,
            pinnedViews: [.sectionHeaders]) {
                // Section 1
                Section(
                    header: Text("Section1")
                        .foregroundColor(.white)
                        .font(.title)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(Color.blue)
                        .padding()
                ) {
                    ForEach(0..<20) { index in
                        Rectangle()
                            .fill(Color.gray)
                            .frame(height: 150)
                            .overlay(
                                Text("\(index) 번")
                                    .frame(maxWidth: .infinity)
                                    .background(Color.red)
                            )
                    }
                } //: Section 1

                // Section 2
                Section(header: Text("Section2")
                    .foregroundColor(.white)
                    .font(.title)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .background(Color.red)
                    .padding()
                ) {
                    ForEach(0..<20) { index in
                        Rectangle()
                            .fill(Color.green)
                            .frame(height: 150)
                            .overlay(
                                Text("\(index) 번")
                            )
                    }
                } //: Section 2
            }
    } // :1번
}

 

Button

가장 많이 사용하게 될 View 중 하나인 Button이다.

다양한 예제로 살펴보자!

 

// property

// @State: UI 화면에 변경이 되면 자동으로 알아차려서 UI 화면에 업데이트를 해줌.
@State var mainTitle: String = "아직 버튼 안눌림"

var body: some View {
    VStack (spacing: 20) {
        // 리셋 버튼
        Button  {
            // Action: 사용자가 버튼을 클릭할 경우 작업 수행 메서드
            mainTitle = "리셋"
        } label: {
            // Label: 텍스트, 아이콘 등 버튼을 라벨링하기
            Text("리셋버튼")
        }


        // 라벨
        Text(mainTitle)
            .font(.title)

        Divider()

        // 1번 버튼
        // action 은 button을 눌렀을때 실행할 event 넣기
        // label 은 button 모양을 디자인 하기
        Button {
            // action
            mainTitle = "기본 버튼 눌림"
        } label: {
            // label
            Text("기본 버튼")
        }
        .accentColor(.red)

        Divider()

        // 2번 버튼
        Button {
            mainTitle = "저장 버튼 눌림"
        } label: {
            Text("저장")
                .font(.headline)
                .fontWeight(.semibold)
                .foregroundColor(.white)
                .padding() // 왜 padding을 2개를 쓰는거지?
                .padding(.horizontal, 20)
                .background(
                    Color.blue
                        .cornerRadius(10)
                        .shadow(radius: 10)
                )
        }

        Divider()

        // 3번 버튼
        Button {
            mainTitle = "하트 버튼 눌림"
        } label: {
            Circle()
                .fill(Color.white)
                .frame(width: 75, height: 75)
                .shadow(radius: 10)
                .overlay(
                    Image(systemName: "heart.fill")
                        .font(.largeTitle)
                        .foregroundColor(Color.red)
                )
        }

        // 4번 버튼
        Button {
            mainTitle = "완료 버튼 눌림"
        } label: {
            Text("완료")
                .font(.caption)
                .bold()
                .foregroundColor(.gray)
                .padding() // 상하좌우에 균등한 기본 패딩(여백)을 추가 함.
                .padding(.horizontal, 10) // 가로 방향으로 10포인트 추가 패딩을 적용
                .background(
                    Capsule()
                        .stroke(Color.gray, lineWidth: 2.5)
                )
        }


    }
}

 

 

이상으로 SwiftUI의 주요한 뷰들을 예제와 함께 살펴보았습니다.

다음에는 @state, @binding 같은 상태 관리 로직도 살펴보겠습니다.

 

감사합니다!