Skip to content

Instantly share code, notes, and snippets.

@end3r117
Last active July 29, 2020 08:25
Show Gist options
  • Save end3r117/c9f25446865c502562c0db9fbac40db7 to your computer and use it in GitHub Desktop.
Save end3r117/c9f25446865c502562c0db9fbac40db7 to your computer and use it in GitHub Desktop.
20200728 - SwiftUI | LazyXGrid | Combine - ColorFinder Flash Card Game (Reddit)
//
// 20200728_ColorFinderGameExample.swift
//
// Created by Anthony Rosario on 7/28/20.
//
import Combine
import SwiftUI
struct CardView: View {
@Environment(\.colorScheme) var colorScheme
@Binding var winningCardID: Int
@Binding var selectedCardID: Int?
@State private var saturation: Double = 1
@State private var animation: Animation = Animation.easeInOut(duration: 0.3)
let cardID: Int
let cardColor: Color
let opacity: Double
var body: some View {
Button(action: {
guard selectedCardID == nil else { return }
checkWinner()
}, label: {
RoundedRectangle(cornerRadius: 25.0, style: .continuous)
.fill(Color.white)
.colorMultiply((selectedCardID == nil ? cardColor : postSelectionColor).opacity(opacity))
.saturation(saturation)
.animation(animation)
.frame(width: 200, height: 250)
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 10, y: 10)
.shadow(color: Color.white.opacity(colorScheme == .dark ? 0 : 0.7), radius: 10, x: -5, y: -5)
})
.buttonStyle(PlainButtonStyle())
.allowsHitTesting(selectedCardID == nil)
}
var baseAnimation: Animation {
Animation.easeIn(duration: 0.1)
}
var loserAnimation: Animation {
Animation.easeInOut.repeatCount(8)
}
var postSelectionColor: Color {
guard let selectedID = selectedCardID else { return cardColor }
if winningCardID == selectedID {
if winningCardID == cardID {
return .green
}else {
return cardColor.opacity(0.1)
}
}else {
if selectedID == cardID {
return .red
}else if winningCardID == cardID {
return Color.green.opacity(0.7)
}else {
return cardColor.opacity(0.1)
}
}
}
func checkWinner() {
if winningCardID != cardID {
animation = loserAnimation
}else {
animation = baseAnimation
}
withAnimation {
selectedCardID = cardID
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animation = baseAnimation
}
}
}
}
class ColorFinderViewModel: ObservableObject {
var colorScheme: ColorScheme = {
UIScreen.main.traitCollection.userInterfaceStyle == .dark ? .dark : .light
}()
var baseOpacityRange: ClosedRange<Double> {
colorScheme == .dark ? 0.8...0.95 : 0.5...0.9
}
@Published var mainColor: Color = .blue
@Published var streak: Int = 0
@Published var longestStreak: Int = 0
@Published var roundWinningCardID: Int = Int.random(in: 0..<4)
@Published var difficultyMultiplier: Double = 1
@Published var roundWinningCardOpacity: Double = Double.random(in: 0.5...0.9)
@Published var selectedCardID: Int? = nil
@Published var roundResultText: Text? = nil
var cancelBucket = Set<AnyCancellable>()
init() {
$selectedCardID
.receive(on: DispatchQueue.main)
.filter({ $0 != nil })
.sink(receiveValue: {[weak self] selectedID in
guard let self = self else { return }
DispatchQueue.main.async {
if self.roundWinningCardID == selectedID {
self.nextRound()
}else {
self.reset()
}
}
})
.store(in: &cancelBucket)
$streak
.receive(on: DispatchQueue.main)
.filter({[weak self] in $0 > self?.longestStreak ?? 0 })
.sink { [weak self] newRecord in
DispatchQueue.main.async {
self?.longestStreak = newRecord
}
}
.store(in: &cancelBucket)
}
private func randomize(quickly: Bool = false) {
DispatchQueue.main.asyncAfter(deadline: .now() + (quickly ? 1.5 : 2.5)) {
withAnimation {
let R = Double.random(in: 0...255)
let G = Double.random(in: 0...255)
let B = Double.random(in: 0...255)
self.roundResultText = nil
self.selectedCardID = nil
self.mainColor = Color(red: R / 255, green: G / 255, blue: B / 255)
self.roundWinningCardOpacity = {
let lower = self.baseOpacityRange.lowerBound * self.difficultyMultiplier
let upper = min(self.baseOpacityRange.upperBound * self.difficultyMultiplier, 0.99)
return Double.random(in: lower...upper)
}()
self.roundWinningCardID = Int.random(in: 0..<4)
}
}
}
private func nextRound() {
difficultyMultiplier = max(1.0, (Double(streak) / 5.0))
withAnimation {
var text: String = "WINNER!"
switch streak {
case 4:
text = "NICE!"
case 8, 12:
text = "WOW!"
case 15:
text = "INCREDIBLE!"
case 18:
text = "....cool."
default: break
}
roundResultText = Text(text).font(.largeTitle).foregroundColor(.green).italic()
streak += 1
randomize(quickly: true)
}
}
private func reset() {
difficultyMultiplier = 1
withAnimation {
roundResultText = Text("YOU LOSE.").font(.largeTitle).foregroundColor(.red).bold()
streak = 0
randomize()
}
}
}
struct ColorFinderView: View {
@Environment(\.colorScheme) var colorScheme
@StateObject var model = ColorFinderViewModel()
var backgroundColor: Color {
if colorScheme == .dark {
return Color(.systemBackground)
}else {
return Color(UIColor(red: 225 / 255, green: 225 / 255, blue: 235 / 255, alpha: 1))
}
}
var body: some View {
VStack {
title
topScore
resultText
cardStack
score
Spacer()
}
.background(backgroundColor.edgesIgnoringSafeArea(.all))
.onChange(of: colorScheme) { value in
model.colorScheme = value
}
.onAppear {
model.colorScheme = colorScheme
}
}
var title: some View {
Text("Color Finder!")
.font(.system(size: 65))
.fontWeight(.bold)
.foregroundColor(.white)
.colorMultiply(model.mainColor)
.animation(.easeInOut(duration: 0.3))
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 10, y: 10)
.shadow(color: Color.white.opacity(colorScheme == .dark ? 0 : 0.7), radius: 10, x: -5, y: -5)
.frame(maxWidth: .infinity)
.fixedSize(horizontal: false, vertical: true)
}
var topScore: some View {
HStack {
Text("Top score: ")
.padding(.leading, 4)
Text("\(model.longestStreak)")
.bold()
Spacer()
}
.padding([.horizontal, .top])
}
var resultText: some View {
HStack {
Rectangle()
.fill(backgroundColor)
.frame(maxHeight: 20)
.overlay(
VStack {
if model.roundResultText != nil {
model.roundResultText
.opacity(model.roundResultText == nil ? 0 : 1)
.scaleEffect(model.streak > 0 ? 2.0 : 1)
.offset(y: model.streak > 0 ? -8 : -12)
.transition(AnyTransition.asymmetric(insertion: .scale(scale: 5), removal: .opacity))
}
}
.padding(.top)
.rotationEffect(.degrees(model.roundResultText == nil ? 720 : 0), anchor: .center)
.animation(.easeIn)
, alignment: .top)
}
}
var cardStack: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())],
alignment: .center, spacing: 4, pinnedViews: []) {
ForEach(0..<4) { i in
CardView(winningCardID: $model.roundWinningCardID,
selectedCardID: $model.selectedCardID,
cardID: i,
cardColor: model.mainColor,
opacity: model.roundWinningCardID == i ? model.roundWinningCardOpacity : 1)
}
}
.padding(.horizontal, 8)
.padding(.bottom)
.scaleEffect(model.roundResultText == nil ? 1 : 0.85)
}
var score: some View {
VStack(spacing: 0) {
Spacer()
Text("Score")
.font(.system(size: 40))
.textCase(.uppercase)
.padding(.leading, 4)
.lineLimit(1)
.layoutPriority(1)
Divider()
.frame(width: 200)
Text("\(model.streak)")
.font(.system(size: 60))
.bold()
.transition(.identity)
.animation(nil)
.padding(.horizontal)
.frame(maxWidth: .infinity)
.foregroundColor(.green)
Spacer()
}
.frame(maxWidth: .infinity)
.padding([.horizontal, .top])
}
}
struct ColorFinder_Previews: PreviewProvider {
static var previews: some View {
ColorFinderView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment