Skip to content

Instantly share code, notes, and snippets.

@tarrouye
Last active September 20, 2022 07:42
Show Gist options
  • Save tarrouye/d384492b8e9f5dec031f9de614a586a5 to your computer and use it in GitHub Desktop.
Save tarrouye/d384492b8e9f5dec031f9de614a586a5 to your computer and use it in GitHub Desktop.
Simple SwiftUI Charades game
//
// CharadeView.swift
//
//
// Created by Théo Arrouye on 9/14/22.
//
import SwiftUI
// MARK: Custom Clip Shape
let RRCLIPSHAPE = RoundedRectangle(
cornerRadius: 5,
style: .continuous
)
// MARK: CardView
struct CardView: View {
let prompt: String
@Binding var time: String
@Binding var result: CardResult
private var bgColor: Color {
result.cardColor
}
var body: some View {
ZStack {
RRCLIPSHAPE
.strokeBorder(Color.primary, lineWidth: 4)
.background(RRCLIPSHAPE.fill(bgColor.opacity(0.7)))
Text(prompt)
.font(.largeTitle)
.lineLimit(2)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
Text(time)
.font(.title)
.lineLimit(1)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxHeight: .infinity, alignment: .bottom)
.padding(.bottom)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: Enums
enum GameState {
case start
case playing
case finished
}
enum CardResult {
case unknown
case correct
case pass
var iconColor: Color {
switch self {
case .unknown:
return .gray
case .correct:
return .green
case .pass:
return .orange
}
}
var cardColor: Color {
switch self {
case .unknown:
return .blue
case .correct:
return .green
case .pass:
return .orange
}
}
}
// MARK: CharadeView
struct CharadeView: View {
let prompts = ["Ellen", "Your", "Game", "Is", "So", "Copyable"]
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State var time: Float = 60.0 {
didSet {
updateTimeString()
}
}
@State var timeString = ""
@State var gameState: GameState = .start
@State var currentPrompt = 0 {
didSet {
resetTime()
}
}
@State var cardResults: [CardResult] = []
private var correctResults: Int {
cardResults.filter { $0 == .correct }.count
}
@State var wasGestureTriggered: Bool = true /* start true so device has to be reset into starting position before first trigger */
// MARK: Timer
private func timerRunLoop() {
guard gameState == .playing else { return }
decrementTime()
}
private func decrementTime() {
time -= 1.0
guard time < 0 else { return } /* lose game if time runs out */
loseCard()
}
private func resetTime() {
time = 60.0
}
private func updateTimeString() {
timeString = "\(time.formattedString(maxPrecision: 2))s"
}
// MARK: - Game State
private func loseCard() {
moveToNextCardSettingResultAs(.pass)
}
private func winCard() {
moveToNextCardSettingResultAs(.correct)
}
private func moveToNextCardSettingResultAs(_ result: CardResult) {
/* set result */
cardResults[currentPrompt] = result
/* move to next card, or end game if last card */
guard currentPrompt < prompts.count - 1 else {
finishGame()
return
}
withAnimation {
currentPrompt += 1
}
}
private func startGame() {
/* reset game state */
currentPrompt = 0
gameState = .playing
cardResults = [CardResult](repeating: .unknown, count: prompts.count)
MotionManager.shared.startMotionUpdates()
}
private func finishGame() {
withAnimation {
gameState = .finished
}
MotionManager.shared.stopMotionUpdates()
}
// MARK: - Subviews
var startScreen: some View {
VStack {
AsyncImage(url: URL(string: "https://static.wikia.nocookie.net/characters/images/6/6b/Latest_%281%29-3.jpg")) { image in
image.resizable().scaledToFit()
} placeholder: {
ProgressView()
}
.frame(width: 100)
.padding(.top)
Text("Sandy Cheeks")
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
Text("Tap to start the game")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
}
}
var gameView: some View {
ZStack {
CardView(prompt: prompts[currentPrompt], time: $timeString, result: $cardResults[currentPrompt])
.id("CRD\(currentPrompt)")
.padding([.horizontal, .top], 5)
.transition(.slideUp)
statusView
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxHeight: .infinity, alignment: .top)
.padding(.top, 15)
}
.transition(.slideUp)
}
var endView: some View {
VStack {
Text("Game over")
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
statusView
.frame(maxWidth: .infinity, alignment: .center)
.padding()
Text("You got \(correctResults)/\(cardResults.count)")
.font(.title)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
}
.transition(.slideUp)
}
var statusView: some View {
HStack(spacing: 10) {
ForEach(cardResults, id: \.self) { res in
Circle()
.strokeBorder(.primary, lineWidth: 1)
.background(
Circle()
.fill(res.iconColor)
)
.frame(width: 25, height: 25)
}
}
}
// MARK: - Roll Gesture Handling
private func checkCardGesture(_ rollAmount: Double) {
/* we set < -2.25 as the down gesture and > -0.25 as the up gesture */
/* and [-1.5, -1] as the reset area */
guard !wasGestureTriggered else {
checkForResetGesture(rollAmount)
return
}
let correct = checkForCorrectGesture(rollAmount)
let pass = checkForPassGesture(rollAmount)
guard correct || pass else { return }
wasGestureTriggered = true
}
private func checkForResetGesture(_ rollAmount: Double) {
guard rollAmount > -1.5 && rollAmount < -1 else { return }
wasGestureTriggered = false
}
private func checkForCorrectGesture(_ rollAmount: Double) -> Bool {
guard rollAmount < -2.25 else { return false }
winCard()
return true
}
private func checkForPassGesture(_ rollAmount: Double) -> Bool {
guard rollAmount > -0.25 else { return false }
loseCard()
return true
}
// MARK: - Body
var body: some View {
switch gameState {
case .start:
startScreen
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {
startGame()
}
case .playing:
gameView
.onReceive(timer) { _ in
timerRunLoop()
}
.onReceive(MotionManager.shared.$x) { roll in
checkCardGesture(roll)
}
case .finished:
endView
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {
gameState = .start
}
}
}
}
// MARK: Custom Slide Animation
extension AnyTransition {
static var slideUp: AnyTransition {
AnyTransition.asymmetric(
insertion: .move(edge: .bottom),
removal: .move(edge: .top)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment