Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Basic)

[Stanford] 카드 뒤집기 게임을 만들어 보자

by Toughie 2023. 3. 23.

SwiftUI로 코드를 작성 할 때, 뷰를 잘게 쪼개는 습관을 들이자. (함수도 최대한 쪼개는 것처럼)

-> 작은 뷰들을 모아서(VSTack, HStack 같은 View Combiner..등) 만드는 것이 스유의 디자인 패러다임

 

레고 안에 레고 안에 레고 안에 레고 안에 레고 레고 레고 레고 레고 레고 레고...

 

이번 시간의 키워드는 아래 코드블럭에 정리해 보았다.

@State

onTapGesture

identifiable

Button

HStack

VStack

SFSymbol

LazyVGrid

aspectRatio

scrollView

strokeborder

behave like a View!

@State 어트리뷰트..?

먼저 구조체에서 mutating 키워드가 생각났다.

값타입(ex.구조체)의 메서드가 값타입 내부의 프로퍼티를 수정하는 것은 기본적으로 불가능.

하지만 mutating 키워드를 붙여주면 가능하다.

struct Location {
var x = 0
var y = 0

mutating func motveTo(x: Int, y: Int) {
	self.x = x
    self.y = y
    }
}

 

위 코드는 구조체 내부의 계산속성이(메서드) 구조체 내부의 프로퍼티를 바꾸려 하는 상황이다.

.onTapGesture (계산속성은 메서드나 마찬가지지만, mutating 키워드는 func 앞에 붙여야 하기에 불가능하다.)

 

관련 공식문서를 살펴보자

@State는 SwiftUI에 의해 관리되는 값을 읽고 쓸 수 있도록 해주는 어트리뷰트이다.

그럼 @State를 붙이면 이제 해당 변수를 쓸 수 있고 그럼 View가 mutable인가?

-> 아니다

뷰는 여전히 immutable하고 

사실상 해당 변수는 Bool값이 저장된 메모리를 가리키는 포인터이다.

(메모리 영역에서 실제로 값이 바뀔 수 있다.)

요약하자면 구조체 내 프로퍼티에 @State 키워드를 붙여줌으로써

ZStack 안의 .onTapGesture 메서드를 통해 isFaceUP 변수가 가리키고 있는 메모리 영역의 Bool 값을 변경할 수 있게 된 것.

-> 값에 변화가 생기면 View를 다시 리빌드 하고 var body 부분을 다시 생성함 

 

스유가 자동으로 어떤 변화를 감지하고 해당 변화와 관련된 뷰를 다시 생성함

개념이 좀 어려운데.. struct CardView 자체가 읽기전용이고 변화가 생기면(내부 프로퍼티 등) body를 rebuild 한다니까

계속 새로운 인스턴스를 찍어낸다는 느낌이지 않나..싶다.🤔

 

 

identifiable

 

identifiable 이 뭐야?

고유한 ID값이 필요할 때 ! 말 그대로 식별(구별)가능하다는 것이다.

근데 왜 위와 같은 에러가 발생했는가?

지금 배열 안에 이모지들이 스트링으로 들어 있다. 

ForEach를 쓰기 위해서는 배열안의 요소들이 Identifiable 해야 한다.
(*구조체에서 identifiable 프로토콜을 채택하면 id라는 변수를 꼭 만들어야함)

근데 왜 uniquely Identifiable 해야 하는데?

 

왜나면 ForEach를 돌면서 각각의 View를 만들 것이기 때문이다.

만약 배열에 다른 요소가 추가되거나, 제거되거나 등등 변화가 생겼을 때 

배열에서 어떤 변화가 생겼는지 알아야 하고 그래야 변화에 맞게 뷰를 조정할 수 있기 때문이다.

즉 배열의 요소와 뷰를 정확히 매칭할 필요가 있기 때문이다!

 

근데 String(구조체이다)에는 그런게 없다 ㅎ..(지금 배열 안에 String으로 이모지들이 들어 있는 상황임)

만약 아래와 같이 중복된 이모지가 있다면? 컴퓨터는 알 수가 없다.

그래서 String 그 자체를 unique identifier로 사용하라!는 argument가 있다.

(String Structure 내부에는 identifiable을 채택하면 구현해야 하는 id 프로퍼티가 없음을 다시 기억하자)

 

그래서 String Literl마다 self라고 하는 고유의 id를 가지게 되는 것이다. (자기 자신 ㅋㅋ)

id: \.self argument를 추가하면 에러는 사라지지만..

기차 카드를 누르면 함께 한꺼번에 선택 된다. ㅋㅋㅋ String Literal이 아예 동일하니까.. 당연

중복되는 녀석을 없애고 편안하게 후행 클로저 문법 최적화를 해준다.

카드 추가,제거를 위한 간단한 로직 추가

버튼을 추가해보자!

HStack으로 묶여있는 카드 더미 밑에 버튼들이 위치하기 때문에

VStack으로 먼저  감싸준다. 그리고 버튼이 좌,우로 위치하길 원하기 때문에 버튼끼리 HStack으로 묶어줬다.

그리고 중간 공백을 주기 위해서 Spacer()를 추가해줬다.(기본적으로 컴포넌트끼리 최대한 멀리 떨어지도록 간격 설정됨)

하지만 버튼이 너무 끝에 가 있는거 같아서 버튼과 Spacer()가 묶여 있는 HStack에 horizontal Padding을 준다.

 

하지만 위의 코드를 보면 너무 복잡하고 이해하기도 어렵다. 따라서 코드를 좀 더 정리해 보자.

편 - 안

이제 버튼을 +, - 모양으로 바꿔주자.

(디자인을 따로 하지 않는다면, 애플에서 제공하는 심볼을 이용할 수 있다.) (SF symbols)

 Image(systemName: "plus.circle")

SF symbols

 

버튼을 기존 Text들을 VStack으로 감쌌던 것에서 Image로 변경해 주었고,

버튼의 크기가 너무 작아 .font(.largeTitle) 적용, 그리고 카드들과 간격이 너무 좁아서

카드 HStack과 버튼 HStack 사이에 Space()를 넣어주었다.

 

그리고 아래에는 - 버튼을 너무 많이 눌러서 emojicount 변수가 0이 되었을 경우와,

+버튼을 너무 많이 눌러서 이모지 배열의 길이를 넘겼을 경우 크래시가 나기 때문에

이를 방지하는 조건을 추가해 주었고, 클로저 문법 최적화를 진행한 코드이다.

 

 

LazyVGrid

이제 카드를 격자로 나열해야 하니 

LazyVGrid를 써보자.

몇개의 컬럼이 필요한 지 설정할 수 있는데, 단순하게 Int를 넣는 것이 아니라 배열 안에 GridItem()을 필요한 컬럼 수만큼 넣으면 된다.

이렇게 하면 아래 예시와 같이 컬럼마다 길이를 다르게 할 수 있는 등 더 다양한 것들을 구현할 수 있다는 장점이 있다.

(default는 GridItem(.flexible())이다. 공간에 따라 유연하게 자동으로 맞춰진다고 생각하면 될 듯 하다.

 

근데 애들이 너무 쪼그라들어있는데? (HStack의 경우 가능한 모든 공간을 꽉 채워서 컴포넌트를 보여줬었는데.. 가로, 높이 꽉 꽉)

반면 LazyVGrdi는 폭은 가로의 공간을 최대한 사용하고(GridItem 수에 맞게) 세로로는 최대한 많이 적재할 수 있도록

최소한의 높이로 컴포넌트를 보여준다.(아래 예시를 참고해 보자)

 

이 경우 Spacer()가 아래 빈 공간을 다 차지하고 있다.(카드 영역이나 버튼 영역이 필요한 공간 제외하고 전부 다!)

만약 Space()를 코드에서 삭제하면 카드 그리드와 버튼이 우측 사진과 같이 중앙으로 모이는 것을 알 수 있다.

 

 

이제 카드처럼 보이도록 Aspect Ratio를 2:3 정도로 맞춰보자.

정말 간단하게 점문법으로 아래 메서드를 사용하면 된다.

func aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) -> some View

 

어.. 카드 사이즈는 마음에 드는데 다른 카드들이 다 짤리고 버튼이 사라졌다..ㅋㅋ

그래서 스크롤을 하며 볼 수 있게 ScrollView 안에 담아줬다.

 

Lazy 키워드를 생각하면 될 것 같은데..(지연 프로퍼티 공부 했다면.. 알 것이다.)

View를 생성하는 것은 비교적 가벼운 작업이다. (몇몇 변수만 있으면 된다.)

하지만 View의 body 변수에 접근하는 것은 다른 얘기다(내부 구현이 복잡한 경우가 많기 때문)

(다음에 좀 더 자세히 알아보자)

 

strokeborder

stroke는 외곽선을 그리는 것이라 생각하면 되는데.. 극단적으로 외곽선의 굵기를 확 굵게 하면

아래와 같은 현상이 발생한다. (스크롤 뷰 영역 외의 부분은 잘려버린다.)

이럴 때는 strokeborder를 사용하면 좋다.

외곽선이 안쪽을 향하게 그려진다 !(우측 사진 참고, 아주 마음에 든다)_너무 두꺼운 거 같아서 이후 5 정도로 굵기 수정함.

 

근데 화면을 돌려보니.. 너무 거대하게 나오는 문제가 있다. (공간 아깝게..)



이 경우 아래와 같은 방법을 쓸 수 있다. LazyVGrid(columns: [GridItem(.adaptive(minimum: __width__))]) { }

LazyVGrid의 컬럼에서 GridItem을 하나만 남겨두고, .adaptive(minimum: )을 설정해 주는 것이다.

여기서 minimum은 최소 width를 말한다. 위 코드를 보면 최소 너비가 100으로, 최대한 많이 보여줘! 정도로 생각하면 될 것 같다.

적당히 조절하며 원하는 크기로 만들 수 있다 :)

 

중간 과정 시뮬레이터 ~~

 

 

1시간 반짜리 강의인데.. 멈추고 코드치고 개념 정리하고 하느라 4시간이나 써버렸다 🤦🏻‍♂️

그래도 점점 스유가 뭔지  감이 오는 느낌? 급할수록 돌아가자.. 어짜피 대충대충 보면 다시 돌아와서 보게 될 테니까..