THIS IS ELLIE

스탠포드 SwiftUI강의 복습하기 Lecture6 본문

개발/SwiftUI

스탠포드 SwiftUI강의 복습하기 Lecture6

Ellie Kim 2021. 2. 14. 20:52

@State

뷰 안에서 완전히 로컬라이즈 된 것.
얼럿 띄우기, 편집, 애니메이션과 같은 일시적인 상태에만 사용한다.

View 구조체는 (read - only) 읽기 전용이다.
예로 SwiftUI가 모든 뷰를 유지하는 데 사용하는 변수는 let! 이다.
let이면 immutable 하기 때문에 아무도 변경할 수 없으니까 어지럽힐 수도 없다.

또한 뷰 생성 시 초기화되는 변수 외에는 변수가 있는 것이 소용이 없다.
읽기 전용이거나 계산된 변수만 의미가 있다.

뷰는 대부분 "stateless"이어야 하며 모델을 그리는 역할을 한다.
그래서 대부분 뷰는 어떤 상태가 필요하지 않기 때문에 읽기 전용이다. 

영구적인 상태는 모델에 속하고 일시적인 상태를 사용할 때 State를 사용한다.

@State private var somthingTemporary: SomeType 

SomeType은 아무 타입이나 사용 가능하다. 
@State var은 뷰의 변경이 필요할 때 사용된다. 
이를 사용하면 힙에 공간이 만들어지고 read-only뷰가 다시 만들어지면 새 버전이 해당 포인트를 가리키게 된다.
즉 뷰가 변경이 있어도 상태는 버려지지 않는다.

Animation

애니메이션을 실행하는 방법은 두 가지가 있다.

- 암시적 애니메이션 implictly
.animation(Animation)와 같이 view modifider를 사용해서 실행한다.
아래 코드는 Text뷰에 view modifider를 사용해 애니메이션을 적용한 것이다.

Text("👻")
    .opacity(scary ? 1 : 0)
    .rotationEffect(Angle.degrees(upsideDown ? 180 : 0))
    .animation(Animation.easeInOut)

일반적으로 컨테이너가 아닌, 다른 뷰와 독립적으로 작동하는 뷰에서 사용한다.

- 명시적 애니메이션 explicilty
withAnimation(Animation) { }와 같이 변경이 필요한 코드를 랩핑하여 실행한다.

withAnimation(.linear(duration: 2)) {
	// do something that will cause ViewModifier/Shape arguments to change somewhere 
}

명시적 애니메이션은 블록 실행의 결과로 이루어진 모든 변경 사항이 함께 애니메이션 되는 세션을 생성한다.
또한 명시적 애니메이션은 암시적 애니메이션을 재정의하지 않는다.

animation curve의 종류와 이는 rate를 조정할 수 있다.
- linear: 항상 동일하게
- easeInOut: 처음엔 천천히 점점 빨라짐
- spring: 바운스가 있다. (soft landing)


이번에 추가된 작업
- shuffle()함수를 통한 카드 재정렬
- 애니메이션을 통한 카드 뒤집기
- 매칭되면 카드 없애기
- 파이 애니메이션 추가하기

코드 

더보기
struct Cardify: AnimatableModifier {
    var rotation: Double
    
    init(isFaceUp: Bool) {
        rotation = isFaceUp ? 0 : 180
    }
    
    var isFaceUp: Bool {
        rotation < 90
    }
    
    // rotation을 animationData로 이름만 변경한 것
    var animationData: Double {
        get { return rotation }
        set { rotation = newValue }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            Group {
                RoundedRectangle(cornerRadius: cornerRadius).fill(Color.white)
                RoundedRectangle(cornerRadius: cornerRadius).stroke(lineWidth: edgeLindWidth)
                content
            }
                .opacity(isFaceUp ? 1 : 0)
            RoundedRectangle(cornerRadius: cornerRadius).fill()
                .opacity(isFaceUp ? 0 : 1)
        }
        .rotation3DEffect(Angle.degrees(rotation), axis: (0,1,0))
    }
    
    private let cornerRadius: CGFloat = 10.0
    private let edgeLindWidth: CGFloat = 3
}

extension View {
    func cardify(isFaceUp: Bool) -> some View {
        self.modifier(Cardify(isFaceUp: isFaceUp))
    }
}

 

struct Pie: Shape {
    var startAngle: Angle
    var endAngle: Angle
    var clockwise: Bool = false
    
    
    var animatableData: AnimatablePair<Double, Double> {
        get {
            AnimatablePair(startAngle.radians, endAngle.radians)
        }
        set {
            startAngle = Angle.radians(newValue.first)
            endAngle = Angle.radians(newValue.second)
        }
    }
    
    func path(in rect: CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius = min(rect.width, rect.height) / 2
        let start = CGPoint(
            x: center.x + radius * cos(CGFloat(startAngle.radians)),
            y: center.y + radius * sin(CGFloat(endAngle.radians))
        )
        
        var p = Path()
        p.move(to: center)
        p.addLine(to: start)
        p.addArc(
            center: center,
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: clockwise
        )
        p.addLine(to: center)
        return p
    }
}

 

struct MemoryGame<CardContent> where CardContent: Equatable {
    private(set) var cards: Array<Card>
    
    private var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get { cards.indices.filter {cards[$0].isFaceUp}.only }
        set {
            for index in cards.indices {
                cards[index].isFaceUp = index == newValue
            }
        }
    }
    
    mutating func choose(card: Card) {
        print("card가 선택되었다 \(card)")
        if let chosenIndex: Int = cards.firstIndex(matching: card), !cards[chosenIndex].isFaceUp, !cards[chosenIndex].isMatched {
            if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
                if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                    cards[chosenIndex].isMatched = true
                    cards[potentialMatchIndex].isMatched = true
                }
                self.cards[chosenIndex].isFaceUp = true
            } else {
                indexOfTheOneAndOnlyFaceUpCard = chosenIndex
            }
        }
    }
    
    init(numberOfPairsOfCards: Int, cardContentFactory: (Int) -> CardContent) {
        cards = Array<Card>()
        
        for pairIndex in 0..<numberOfPairsOfCards {
            let content: CardContent = cardContentFactory(pairIndex)
            cards.append(Card(content: content, id: pairIndex * 2))
            cards.append(Card(content: content, id: pairIndex * 2 + 1))
        }
        cards.shuffle()
    }
    
    struct Card: Identifiable {
        var isFaceUp: Bool = false {
            didSet {
                if isFaceUp {
                    startUsingBonusTime()
                } else {
                    stopUsingBonusTime()
                }
            }
        }
        var isMatched: Bool = false{
            didSet {
                stopUsingBonusTime()
            }
        }
        var content: CardContent
        var id: Int
        
        var bonusTimeLimit: TimeInterval = 6

        private var faceUpTime: TimeInterval {
            if let lastFaceUpDate = self.lastFaceUpDate {
                return pastFaceUpTime + Date().timeIntervalSince(lastFaceUpDate)
            } else {
                return pastFaceUpTime
            }
        }
        
        var lastFaceUpDate: Date?
        var pastFaceUpTime: TimeInterval = 0
        var bonusTimeRemaining: TimeInterval {
            max(0, bonusTimeLimit - faceUpTime)
        }
        var bonusRemaing: Double {
            (bonusTimeLimit > 0 && bonusTimeRemaining > 0) ? bonusTimeRemaining/bonusTimeLimit : 0
        }
        var hasEarnedBonus: Bool {
            isMatched && bonusTimeRemaining > 0
        }
        var isCosumingBonusTime: Bool {
            isFaceUp && !isMatched && bonusTimeRemaining > 0
        }
        
        private mutating func startUsingBonusTime() {
            if isCosumingBonusTime, lastFaceUpDate == nil {
                lastFaceUpDate = Date()
            }
        }
        
        private mutating func stopUsingBonusTime() {
            pastFaceUpTime = faceUpTime
            self.lastFaceUpDate = nil
        }
    }
}

 

struct ContentView: View {
    @ObservedObject var viewmodel: EmojiMemoryGame
    
    var body: some View {
        VStack {
            Grid(viewmodel.cards) { card in
                CardView(card: card).onTapGesture {
                    withAnimation(.linear(duration: 0.75)) {
                        viewmodel.choose(card: card)
                    }
                }
                .padding(5)
            }
            .padding()
            .foregroundColor(Color.orange)
            Button(action: {
                withAnimation(.easeInOut) {
                    self.viewmodel.resetGame()
                }
            }, label: { Text("New Game") })
        }
    }
}

struct CardView: View {
    var card: MemoryGame<String>.Card

    var body: some View {
        GeometryReader { geometry in
            self.body(for: geometry.size)
        }
    }
    
    @State private var animatedBonusRemaing: Double = 0
    
    private func startBonusTimeAnimation() {
        animatedBonusRemaing = card.bonusRemaing
        withAnimation(.linear(duration: card.bonusTimeRemaining)) {
            animatedBonusRemaing = 0
        }
    }
    
    @ViewBuilder
    private func body(for size: CGSize) -> some View {
        if card.isFaceUp || !card.isMatched {
            ZStack {
                Group {
                    if card.isCosumingBonusTime {
                        Pie(startAngle: Angle.degrees(0-90),
                            endAngle: Angle.degrees(-animatedBonusRemaing*360-90),
                            clockwise: true)
                            .onAppear {
                                startBonusTimeAnimation()
                            }
                    } else {
                        Pie(startAngle: Angle.degrees(0-90),
                            endAngle: Angle.degrees(-animatedBonusRemaing*360-90),
                            clockwise: true)
                    }
                }
                .padding(5).opacity(0.4)
                .transition(.identity)
                Text(card.content)
                    .font(Font.system(size: fontsize(for: size)))
                    .rotationEffect(Angle.degrees(card.isMatched ? 180 : 0))
                    .animation(card.isMatched ? Animation.linear(duration: 1).repeatForever(autoreverses: false) : .default)
            }
            .cardify(isFaceUp: card.isFaceUp)
            .transition(AnyTransition.scale)
        }
    }
    
    // MARK: - Drawing Constants

    private func fontsize(for size: CGSize) -> CGFloat {
        min(size.width, size.height) * 0.7
    }
}

 

실행 결과

 

resource:
cs193p.sites.stanford.edu/

반응형