티스토리 뷰
@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/
'Tech > SwiftUI' 카테고리의 다른 글
스탠포드 SwiftUI강의 복습하기 Lecture8 (0) | 2021.02.28 |
---|---|
스탠포드 SwiftUI강의 복습하기 Lecture7 (0) | 2021.02.21 |
스탠포드 SwiftUI강의 복습하기 Lecture5 (0) | 2021.02.12 |
스탠포드 SwiftUI강의 복습하기 Lecture4 (0) | 2021.02.03 |
스탠포드 SwiftUI강의 복습하기 Lecture3 (0) | 2021.01.29 |
- Total
- Today
- Yesterday
- Xcode
- 스위프트UI
- 머신러닝
- 책 후기
- string
- swiftUI
- Algorithm
- 스위프트
- RX
- ARC
- 책
- SWIFT
- Animation
- ios
- rxswift
- swift5
- objective-c
- iOS SwiftUI
- 알고리즘
- 애니메이션
- 딥러닝
- ReactiveX
- 문자열
- 책 추천
- 독서
- leetcode
- Deep learning
- wwdc
- objc
- stanford SwiftUI
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |