본문 바로가기

iOS/CS193p - Developing Apps for iOS

[CS193p] Assignment 4 - Animated Set 솔루션

728x90

 

CS193P Assignment #4 Animated Set

각 task 별 solution을 정리했습니다.

All code in github

 

Required Task

1. Your assignment this week must still play a solo game of Set.

 

2. In this version, though, when there is a match showing and the user chooses another card, do not replace the matched cards; instead, discard them (leaving fewer cards in the game).

👉  card를 선택했을 때의 동작을 replaceNewCard에서 discard로 변경했습니다.

mutating func choose(_ card: Card) {
	if indexOfchosen.count == GameConstants.numberOfMatchCard {
		if isMatch {
        	//replaceNewCard()
            discard()
        }
    ....
}

private mutating func discard() {
    indexOfchosen.reversed().forEach({ i in
        var card = table.remove(at: i)
        card.isSet = nil
        card.isChoose = false
        discardPile.append(card)
    })
}

3. Add a “deck” and a “discard pile” to your UI. They can be any size you want and you can put them anywhere you want on screen, but they should not be part of your main grid of cards and they should each look like a stack of cards (for example, they should have the same aspect ratio as the cards that are in play).

👉 deck view와 discardPile view를 추가했습니다. ZStack으로 gameBody의 grid에는 영향이 가지 않도록 했습니다.

    var body: some View {
        ZStack(alignment: .bottom) {
            VStack {
                gameBody
                HStack {
                    newGame
                }
                .padding(.horizontal)
            }
            HStack {
                deckBody
                Spacer()
                dicardPileBody
            }
        }
        .padding()
    }
    
    var deckBody: some View {
        ZStack {
            ForEach(game.deck) { card in
                CardView(card: card)
                    .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                    .transition(.asymmetric(insertion: .opacity, removal: .identity))
            }
        }
        .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
        //.foregroundColor(CardConstants.color)
        .onTapGesture {
            withAnimation(.easeInOut(duration: CardConstants.dealDuration)) {
                game.dealThreeMoreCard()
            }
        }
    }
    
    var dicardPileBody: some View {
        ZStack {
            ForEach(game.discardPile) { card in
                CardView(card: card)
                    .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                    .transition(.asymmetric(insertion: .identity, removal: .opacity))
            }
        }
        .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
        //.foregroundColor(CardConstants.color)
    }

 

4. The deck should contain all the not-yet-dealt cards in the game. They should be “face down” (i.e. you should not be able to see the symbols on them).

👉  

Card struct에 isFaceUp 프로퍼티를 추가하고, CardView에는 isFaceUp 속성을 가진 Cardify viewModifer를 적용해서 카드가 노출 여부를 결정할 수 있도록 하였습니다.

struct Card: Identifiable {
    var isFaceUp = false
    ...

 

5. The discard pile should contain all the cards that have been discarded from the game (i.e. the cards that were discarded because they matched). These cards should be face up (i.e. you should be able to see the symbols on the last discarded card). Obviously the discard pile is empty when your game starts.

👉 SetGame 모델에 버려지는 카드가 모이는 discardPile을 추가하고, 그 데이터를 UI에서 보이도록 했습니다.

// model
struct SetGame {
	...
    private(set) var discardPile: [Card] = []
    ...
// View
	var dicardPileBody: some View {
        ZStack {
            ForEach(game.discardPile) { card in
                CardView(card: card)
                    .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                    .transition(.asymmetric(insertion: .identity, removal: .opacity))
            }
        }
        .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
    }

 

6. Any time matched cards are discarded, they should be animated to “fly” to the discard pile.\

7. You don’t need your “Deal 3 More Cards” button any more. Instead, tapping on the deck should deal 3 more cards.

8. Whenever more cards are dealt into the game for any reason (including to start the game), their appearance should be animated by “flying them” from the deck into place.

9. Note that dealing 3 more cards when a match is showing on the board still should replace those cards and that those matched cards would be flying to the discard pile at the same time as the 3 new cards are flying from the deck (see Extra Credit too).

10. All the card repositioning and resizing that was required by Required Task 2 in last week’s assignment must now be animated. If your cards from last week never changed their size or position as cards were dealt or discarded, then fix that this week so that they do.

👉 하나의 카드는 deck -> table -> discardPile 순서로 이동(fly)합니다. 이동에 대한 애니메이션을 주기 위해서 matchedGeometryEffect를 같은 namespace에 적용했습니다.

transition은 identity를 이용해서 카드 이동은 fly 되도록했습니다.

struct FirstSetGameView: View {
    @Namespace private var dealingNamespace
    
    var gameBody: some View {
        AspectVGrid(items: game.table, aspectRatio: 2/3, content: { card in
            CardView(card: card)
                .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                .padding(4)
                .transition(.asymmetric(insertion: .identity, removal: .identity))
                .onTapGesture {
                    withAnimation(.easeInOut(duration: CardConstants.dealDuration)) {
                        game.choose(card)
                    }
                }
        })
    }
    
    var deckBody: some View {
        ZStack {
            ForEach(game.deck) { card in
                CardView(card: card)
                    .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                    .transition(.asymmetric(insertion: .opacity, removal: .identity))
            }
        }
        .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
        .onTapGesture {
            withAnimation(.easeInOut(duration: CardConstants.dealDuration)) {
                game.dealThreeMoreCard()
            }
        }
    }
    
    var dicardPileBody: some View {
        ZStack {
            ForEach(game.discardPile) { card in
                CardView(card: card)
                    .matchedGeometryEffect(id: card.id, in: dealingNamespace)
                    .transition(.asymmetric(insertion: .identity, removal: .opacity))
            }
        }
        .frame(width: CardConstants.undealtWidth, height: CardConstants.undealtHeight)
    }

 

11. When a match occurs, use some animation (your choice) to draw attention to the match.

12. When a mismatch occurs, use some animation (your choice) to draw attention to the mismatch. This animation must be very noticeably different from the animation used to show a match (obviously).

👉 카드 선택은 회색, match는 초록색, unmatch는 빨간색으로 차차 변하도록 애니메이션 했습니다.

 

Screenshots