SwiftUI - Stanford Lecture 2 : Learning more about SwiftUI

SeBin·2023년 4월 2일
0

SwiftUI Stanford

목록 보기
2/4
post-thumbnail

Card View struct 정의

  • HStack
    ZStack과 달리 Horizental 방향으로 UI 컴포넌트를 나열해준다.

HStack안에 ZStack을 반복적으로 넣으면 중복되는 코드가 너무 많을 것이다. 이 ZStack을 활용해서 Card View struct를 정의하자

이처럼 레고를 조립해나가는 느낌으로 구조체를 정의하는 것이 Swift UI 프로그래밍의 핵심이다.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .preferredColorScheme(.dark)
        ContentView()
            .preferredColorScheme(.light)
    }
}

ContentView_Previews는 보다시피 위에 정의한 ContentView를 화면에 보여준다. 오른쪽 Insector에서 light모드와 dark모드를 설정할 수 있다.

light모드와 dark모드를 동시에 보려면 ContentPreview 창을 하나 더 만들면 된다.

유의할 점은 stroke와 fill은 한 직사각형에 동시에 사용할 수 없다. .stroke().fill()이 의미는 stroke 안을 채운다는 뜻이다. 배경색을 채우기 위해선 또 하나의 사각형을 만들어야한다.

struct CardView: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .fill()
                .foregroundColor(.white)
            RoundedRectangle(cornerRadius: 20)
                .stroke(lineWidth: 3)
            Text("✈️")
                .font(.largeTitle)
        }
    }
}

카드 뒤집기

카드 상태를 의미하는 isFaceUp 변수를 boolean 타입의 초기값으로 설정해서 구조체 안에 추가한다. 참고로 값이 없는 변수는 존재할 수 없다.

그리고 RoundedRectangle이 중복되므로 shape라는 상수로 지정해준다.

shape가 타입을 명시하지 않아도 되는 이유 :
Swift는 Type inference 기능을 제공하기 때문. Swift 스스로가 타입을 추론할 수 있다.

터치 이벤트 감지하기

Tap gesture를 인식하는 이벤트 핸들러가 있다.

onTapGesture(count:perform:)

하지만 안에 isFaceUp 변수를 !isFaceUp하거나 .toggle()하면 오류가 발생하는데 그 이유는 struct의 value 타입 특성에 의해 한번 인스턴스의 값이 결정되고난 후에는 수정할 수 없기 때문이다.

이 때, @State를 붙이면 해결은 되지만 그래도 UI 구조체가 mutable해지진 않는다.

struct CardView: View {
    @State var isFaceup: Bool = true
    var body: some View {
        let shape = RoundedRectangle(cornerRadius: 20)
        ZStack {
            if isFaceup {
                shape
                    .fill()
                    .foregroundColor(.white)
                shape
                    .stroke(lineWidth: 3)
                Text("✈️")
                    .font(.largeTitle)
            } else {
                shape
            }
        }
        .onTapGesture {
            isFaceup = !isFaceup
        }
    }
}

@State

상태 프로퍼티는 뷰 레이아웃의 현재 상태를 저장하기 위해서만 사용되고, String이나 Int값 처럼 간단한 데이터 타입을 저장하기 위해 사용된다.

  • State value 값이 변경되면 뷰는 해당 value 의 appearance 를 무효화 하고 다시 body 값을 계산

  • state 변수값이 변경되면 view 를 다시 랜더링 하기 때문에 항상 최신 값을 가짐

버튼 만들기

var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))] ) {
                    ForEach(emojis[0..<emojiCount], id: \.self) {
                        emoji in CardView(content:emoji)
                            .aspectRatio(2/3, contentMode: .fit)
                    }
                }
            }
            .foregroundColor(.red)
            Spacer()
            HStack {
                remove
                Spacer()
                add
            }
            .font(.largeTitle)
            .padding(.horizontal)
        }
        .padding(.horizontal)
    }
    var remove: some View {
        Button(action: {
            if emojiCount > 1 {
                emojiCount -= 1
            }
        }) {
            Image(systemName: "minus.circle")
        }

    }
    var add: some View {
        Button(action: {
            if emojiCount < emojis.count {
                emojiCount += 1
            }
        }) {
            Image(systemName: "plus.circle")
        }
    }
}

SF-Symbole은 시스템에서 기본적으로 제공하는 이미지 셋이다. 이를 이용해 쉽게 버튼 이미지를 추가할 수 있다.
V-Stack으로 카드 밑에 두고 H-Stack으로 add버튼과 remove버튼을 나란히 둔다.
그리고 버튼을 따로 변수로 선언해둔다. 이러는 편이 좀 더 코드가 깔끔해지기 때문이다.

Spacer를 사용하면 View와 View 사이에 여백을 만들어주는데 수동으로 여백을 지정할 수도 있지만 기본값으로 그대로 둔다면 다른 iPad, Apple Watch 등에서도 알맞게 여백을 맞춰준다.

LazyGrid

A container view that arranges its child views in a grid that grows vertically, creating items only as needed.

그리드 시스템으로 columens 파라미터를 통해 열의 개수만큼 GridItem() 배열을 넘겨주면 열이 생성된다.
특정 열에 고정값을 줘서(.fixed)
각 열의 크기를 다르게 조절할 수 있다. 이 경우 GridItem의 기본값은 flexible이기 때문에 나머지 열들은 자동으로 조절된다.

  • 만약 카드가 너무 많아서 버튼을 가리고 튀어나간다면 ScrollView를 이용하자.

  • .aspectRatio를 이용하면 가로와 세로 비율을 지정할 수 있다.

  • .adaptive(minimun: )을 이용하면 열 크기의 최솟값을 지정하고 이에 맞게 최대한 뷰에 열을 추가하게 된다.
    이를 활용하여 가로로 회전해도 카드가 커지지 않고 일정한 크기에다 한 줄에 여러개를 더 넣어 딱맞게 만들 수 있다.

이슈

ZStack {
            let shape = RoundedRectangle(cornerRadius: 20)
            if isFaceup {
                shape.foregroundColor(.white)
                shape.strokeBorder(lineWidth: 3)
                Text(content).font(.largeTitle)
                    .padding(.vertical, 1)
            } else {
                shape
            }
        }

이 코드는 카드를 뒤집었을 때에 동작하는 조건문 코드인데, 분명 강의 내용과 똑같이 했음에도 불구하고 카드의 위아래로 이상한 여백이 자꾸 생겼다.
그것도 카드 뒤쪽으로 뒤집으면 괜찮아지고 다시 앞쪽으로 뒤집으면 갑자기 여백이 생겼다.
원인은 .font(.largeTitle) 때문이었다. 텍스트의 크기가 커지면서 기본값으로 어떠한 패딩이 있는 모양이었다.

그래서 텍스트의 패딩값을 지정하려고 .padding(.vertical, 1)을 추가해줬더니 해결되었다. 이상하게 .padding(0)은 동작하지 않았다. 기본값으로 인식되는 모양일까. 아무튼 주먹구구식으로 해결하였다.

Stanford Lecture
참고 블로그

0개의 댓글