하루 날 잡고 하려면 절대 안 쓸 것 같아서 틈틈이 업데이트 해 볼 생각이다
AspectVGrid
가 스크롤이 되도록 ScrollView
에 넣으면 아래로 스크롤이 안됐다.. import SwiftUI
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
let items: [Item]
let aspectRatio: CGFloat
let content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, @ViewBuilder content: @escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
VStack {
let width: CGFloat = max(widthThatBestFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio), 80)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) {
content($0)
.aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
private func adaptiveGridItem(width: CGFloat) -> GridItem {
var gridItem = GridItem(.adaptive(minimum: width))
gridItem.spacing = 0
return gridItem
}
private func widthThatBestFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = Int(ceil(Double(itemCount) / Double(columnCount)))
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
AspectVGrid
를 지우고 LazyVGrid
에 고정된 카드 크기로 CardView
를 만들어서 ScrollView
에 넣어봤더니 멀쩡하게 스크롤이 됐다...그래서 일단 문제는 AspectVGrid
에 있구나 싶었다ScrollView
에 LazyVGrid
를 때려박았을 때는 스크롤이 됐으니까 AspectVGrid
에서 View
를 반환하기 전에 만들어진 그리드를 ScrollView
에 넣어서 아예 AspectVGrid
자체를 스크롤이 되는 뷰로 만들면 어떨까 하는 생각이 들었다. var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
let width: CGFloat = max(widthThatBestFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio), 80)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) {
content($0)
.aspectRatio(aspectRatio, contentMode: .fit)
}
}
}
Spacer(minLength: 0)
}
}
}
ScrollView
안에 AspectVGrid
를 넣으면 스크롤이 안되는데 그 반대는 될까에 대해 스스로 내린 결론은 다음과 같다!View
는 기본적으로 스크롤 기능을 지원하지 않기 때문에 최대로 보여줄 수 있는 영역이 인터페이스 크기로 제한된다CardView
들이 잔뜩 들어있는 LazyVGrid
는 최대 크기가 인터페이스 크기로 제한되는 AspectVGrid
안에 담기게 되면서 이 시점에 View
크기가 인터페이스 크기로 제한되고, 이에 따라 AspectVGrid
를 담고 있는 ScrollView
는 인터페이스 크기만큼이 자신이 담고 있는 전부이므로 스크롤을 할 필요가 없다고 여긴다LazyVGrid
를 바로 ScrollView
안에 넣음으로써 카드 개수에 따라 영역이 인터페이스를 벗어나더라도 스크롤을 통해 볼 수 있게 한다!View
에 대한 이해가 부족해서 발생했던 문제였고, 덕분에 View
에 대해 다시 한 번 공부할 수 있었다. count (3) != its initial count (2). 'ForEach(_:content:)/' should only be used for *constant* data. Instead conform data to 'Identifiable' or use 'ForEach(_:id:content:)' and provide an explicit 'id'
View
파트의 코드에서 문제가 발생하고 있구나 싶었다. 나는 CardView
에서 .symbol.numberOfShapes
변수로 모양의 개수를 확인하고 switch
문으로 분기한 다음 ForEach
문을 돌면서 해당 개수만큼 문양을 생성하고 있었는데 여기 어딘가에서 계속 문제가 발생하는 것 같았다. ForEach
문을 쓰려면 순회하는 데이터/범위가 고정되어 있거나, 가변적인 경우 각 요소가 식별 가능한 id를 갖고 있어야 한다!struct CardView: View {
let card: SquareSetGame.Card
var body: some View {
ZStack {
...
VStack {
switch card.content.shape {
case .roundedRectangle:
ForEach(0..<card.content.numberOfShapes) { _ in
createRoundedRectangleView(by: card.content)
}
case .square:
ForEach(0..<card.content.numberOfShapes) { _ in
createSquareView(by: card.content)
}
case .diamond:
ForEach(0..<card.content.numberOfShapes) { _ in
createDiamondView(by: card.content)
}
}
...
}
id로 자신의 값
을 지정해줬더니 해당 문제가 해결됐다! VStack {
ForEach(0..<card.symbol.numberOfShapes, id: \.self) { _ in
createSymbol(for: card)
}
}
일단 완성본! {height=500px width=400px}
패턴 만드는 법, 무늬 만드는 법, 줄무늬 그리는 법 등으로 엄청 검색해봤는데 지금 소개할 방법이 가장 쉬워서 이 방법으로 했다!
사실 줄무늬를 만들려면 엄청 복잡할 줄 알았는데 생각보다 간단했다! 요약하면 VStack
안에 색깔별로 줄을 쌓으면 줄무늬 뚝딱 완성! 좀 더 자세한 과정은 다음과 같다.
VStack
안에 ForEach 문
을 이용해서 줄무늬 개수만큼 색깔을 선언을 반복하면 가로 줄무늬가 생긴다
Color
자체가 적절한 문맥에서는 해당 View
처럼 작동해 해당 색깔의 직사각형을 나타내기 때문! ( 8강 3분 부터 참고 )VStack
에 mask(//someShape)
메서드를 적용하면 원하는 모양에 맞게 줄무늬가 잘린다!overlaying(//someshape)
로 모양 테두리를 얹어주면 맨 위의 첫 번째 사진처럼 마치 해당 모양이 줄무늬로 채워진 것과 같은 뷰가 완성된다!struct StripeView<SymbolShape>: View where SymbolShape: Shape {
let numberOfStripes: Int = 8
let borderLineWidth: CGFloat = 1.3
let shape: SymbolShape
let color: Color
var body: some View {
VStack(spacing: 0.5) {
ForEach(0..<numberOfStripes) { _ in
Color.white
color
}
Color.white
}.mask(shape)
.overlay(shape.stroke(color, lineWidth: borderLineWidth))
}
}
CardView
를 반환하는 함수를 만드려고 했는데 계속 이런 에러가 떴다...Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements
...수업 때도 언급된 내용이긴 했는데 구글링 결과 Shape 프로토콜
자체가 generic
을 쓰기 때문에 타입이 모호해서 인자로 쓸 수 없었던 것...간신히 구현해놨는데 갈아엎어야되나 싶어서 여기서 눈물이 날 뻔 했으나...인자 shape
의 타입을 SymbolShape
이라는 제네릭으로 선언하고 해당 타입이 shape 프로토콜
에 순응하도록 했더니 해결됐다...! 인자로 보내는 방법이 절대 없을 줄 알고 안그래도 더러운 코드 더 더러워질 것 같아서 눈 앞이 캄캄했는데...다행히 해결...!@ViewBuilder
private func createSymbolView<SymbolShape>(of symbol: SunSetGame.Card.CardContent, shape: SymbolShape) -> some View where SymbolShape: Shape {
switch symbol.pattern {
case .filled:
shape.fill().foregroundColor(symbol.color.getColor())
.aspectRatio(DrawingConstants.symbolAspectRatio, contentMode: .fit).opacity(DrawingConstants.symbolOpacity)
case .shaded:
StripeView(shape: shape, color: symbol.color.getColor())
.aspectRatio(DrawingConstants.symbolAspectRatio, contentMode: .fit).opacity(DrawingConstants.symbolOpacity)
case .stroked:
shape.stroke(lineWidth: DrawingConstants.defaultLineWidth).foregroundColor(symbol.color.getColor())
.aspectRatio(DrawingConstants.symbolAspectRatio, contentMode: .fit).opacity(DrawingConstants.symbolOpacity)
}
수업+이전 과제들에서 만들었던 Memorize
는 Cards 배열
이 게임 시작부터 끝까지 하나만 필요하고, 순서도 그대로 유지되어서 그냥 모든 과정을 인덱스로 접근해도 문제가 없었다...그래서 처음에 Set
도 선택된 카드를 위치 인덱스만 배열에 저장하는 방식, 즉 이렇게 private var chosenCards = [Int]()
짰으나...
Set
의 경우 짝을 맞춘 카드를 계속 화면에서 제거하고 또 덱에서 새로운 카드를 추가해야 하는데 이때 채워넣기 위해 제거했던 카드들을 위치 인덱스로 기억했더니 카드를 맞추고 제거한 뒤에는 남은 카드들의 인덱스가 땡겨져서 엉뚱한 카드가 선택된 상태로 남아있는 문제와 1-2 인덱스 정도 차이나는 잘못된 위치에 카드가 채워지는 문제가 있었다...
결국 선택된 카드도 private var chosenCards = [Card]()
와 같이 카드 자체를 기억하고, 짝이 맞았을 때 playingCards 배열
에서 id
가 같은 카드를 찾아 제거하고, 인덱스를 기억할 필요 없이, 카드를 바로 교체해야 하는 경우 dealThreeCards() 함수
에서 세트를 제거하면서 동시에 바로 새로운 카드로 교체하는 방식으로 바꿔줬다...!
struct SetGame<CardSymbolShape, CardSymbolColor, CardSymbolPattern, NumberOfShapes> where CardSymbolShape: Hashable, CardSymbolColor: Hashable, CardSymbolPattern: Hashable {
...
private var chosenCards = [Card]()
...
mutating func dealOneCard(at index: Int) {
if numberOfPlayedCards < totalNumberOfCards {
let symbol = createCardSymbol(numberOfPlayedCards)
playingCards.insert(Card(symbol: symbol, id: numberOfPlayedCards), at: index)
numberOfPlayedCards += 1
}
}
mutating func dealThreeCards() {
cheat() // should change this..not clear to others
if remainingSet != nil { score -= 3 }
turnOffCheat()
switch chosenCards.count {
case 3:
if playingCards.first(where: {$0 == chosenCards.first})!.isMatched {
chosenCards.forEach { card in
if let matchedIndex = playingCards.firstIndex(of: card) {
playingCards.remove(at: matchedIndex)
dealOneCard(at: matchedIndex)
}
}
chosenCards = []
}
else {
chosenCards.forEach { card in
if let failedMatchIndex = playingCards.firstIndex(of: card) {
playingCards[failedMatchIndex].isChosen = false
playingCards[failedMatchIndex].isNotMatched = false
}
}
chosenCards = []
fallthrough
}
default:
for _ in 0..<3 {
dealOneCard(at: playingCards.endIndex)
}
}
if numberOfPlayedCards == totalNumberOfCards {
isEndOfGame = checkEndOfGame(in: playingCards)
}
}
...
}
restart 버튼
의 위치가 이동해서 의도치 않았는데 새 게임을 시작하게 되는 불편 사항이 있었다...그래서 결국 오른쪽 사진처럼 카드가 다 소진되면 버튼이 회색으로 바뀌고 눌리지 않도록 변경했다!View
가 나타나도록 했다...근데 지금 보니까 다음 버전에서는 Cheat 버튼
도 회색 처리하거나 아님 Cheat 버튼
과 Deal 3 Cards
버튼이 둘 다 사라지게 하면 좋을 것 같다...화면 전환 시에 .transition
으로 애니메이션도 추가하면 좋을 것 같고...!choose() 함수
의 경우 경우의 수도 많고, 각 경우의 수마다 다른 결과? 기능?을 하게 되니까 어디까지가 이 단일 함수가 가지고 있어도 되고, 어디부터는 다른 함수로 분리해야 될 지 정말 고민이 많이 됐다...결국에는 쪼개는 것도 힘들어서 너무나도 길지만 그냥 현 상태로 두기는 했는데...완강하고 다시 한번 Set
을 처음부터 짜 볼 생각이라...(코드 리뷰 받고 싶음...!) 그때 가서 새로운 포스팅이나 현재 포스팅에 한번 지금 코드랑 그 때 코드를 비교해서 포스팅해 볼 생각이다...!미루고 미루다가 결국 이 포스팅을 시작한 지 거의 3주...? 만에 얼렁뚱땅 마무리했는데 앞에서도 말했지만 다음부터는 무조건 3일 기한으로 잡고 써야겠다...내 자신...믿을 수 없네...
사실 올 여름부터 Set
에 엄청 꽂혀서 온라인으로 친구들이랑도 엄청하고 혼자서도 엄청해서 처음에는 과제가 Set
구현이래서 운명인가!? 싶었는데 만들면서 계속 고통받고, 버그없이 잘 작동하나 확인하느라 너무 많이 해서 이제 질려버렸다...