Quick game made as a reply to a Reddit post.
This is only meant to demonstrate ForEach and LazyXGrid, not game logic...
Video:
Image:
| // | |
| // 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() | |
| } | |
| } |
Quick game made as a reply to a Reddit post.
This is only meant to demonstrate ForEach and LazyXGrid, not game logic...
Video:
Image: