Skip to content

Instantly share code, notes, and snippets.

@allfinlir
Last active October 17, 2023 08:39
Show Gist options
  • Save allfinlir/ab5c23d8d4ee46142f97c235f27d37d4 to your computer and use it in GitHub Desktop.
Save allfinlir/ab5c23d8d4ee46142f97c235f27d37d4 to your computer and use it in GitHub Desktop.
import SwiftUI
struct BetDetailsView: View {
var bet: BetModel
@ObservedObject var betViewModel: BetViewModel
// States for the button animations
@State var scaleWinButton: CGFloat = 1.0
@State var scaleLooseButton: CGFloat = 1.0
@State var scalePushButton: CGFloat = 1.0
// Variables for the matched geometry effect
@Binding var showBetDetails: Bool
var animation: Namespace.ID
@State var loadContent: Bool = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
HStack {
Button(action: {
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
showBetDetails.toggle()
}
}, label: {
Image(systemName: "chevron.left")
.font(.title)
.foregroundColor(.black)
})
Spacer()
}
HStack {
Spacer()
Text(bet.dateForBet, style: .date)
}
Text(bet.league)
.foregroundColor(.secondary)
.font(.title3)
.fontWeight(.semibold)
HStack {
Text(bet.homeTeam)
Text("vs")
.foregroundColor(.secondary)
.font(.title3)
Text(bet.awayTeam)
}
.font(.title2)
HStack(alignment: .center, spacing: 15) {
RoundedRectangle(cornerRadius: 4)
.frame(width: 50, height: 25)
.foregroundColor(bet.winBet ? .green : .green.opacity(0.1))
.overlay {
Text("W")
.font(.system(size: 13))
.foregroundColor(bet.winBet ? .white : .black)
.fontWeight(bet.winBet ? .semibold : .light)
}
.scaleEffect(scaleWinButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scaleWinButton)
.onTapGesture {
betViewModel.updateWinBet(bet: bet)
scaleWinButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scaleWinButton = 1.0
}
}
RoundedRectangle(cornerRadius: 5)
.frame(width: 50, height: 25)
.foregroundColor(bet.looseBet ? .red : .red.opacity(0.1))
.overlay {
Text("L")
.font(.system(size: 13))
.foregroundColor(bet.looseBet ? .white : .black)
.fontWeight(bet.looseBet ? .semibold : .light)
}
.scaleEffect(scaleLooseButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scaleLooseButton)
.onTapGesture {
betViewModel.updateLooseBet(bet: bet)
scaleLooseButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scaleLooseButton = 1.0
}
}
RoundedRectangle(cornerRadius: 5)
.frame(width: 50, height: 25)
.foregroundColor(bet.pushBet ? .blue : .blue.opacity(0.1))
.overlay {
Text("P")
.font(.system(size: 13))
.foregroundColor(bet.pushBet ? .white : .black)
.fontWeight(bet.pushBet ? .semibold : .light)
}
.scaleEffect(scalePushButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scalePushButton)
.onTapGesture {
betViewModel.updatePushBet(bet: bet)
scalePushButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scalePushButton = 1.0
}
}
}
.padding(.top, 15)
.frame(maxWidth: .infinity)
.matchedGeometryEffect(id: bet.id, in: animation)
VStack(alignment: .leading, spacing: 25) {
HStack {
Text("Bet:")
Text(bet.typeOfBet)
.foregroundStyle(.secondary)
.padding(.trailing, 8)
Text("Odds:")
Text("\(bet.odds, specifier: "%.2f")")
.foregroundStyle(.secondary)
.padding(.trailing, 8)
Text("Amount:")
Text("\(bet.betAmount, specifier: "%.2f")")
.foregroundStyle(.secondary)
}
.padding(.top, 25)
HStack(alignment: .bottom, spacing: 20) {
Text("Result:")
Text("\(bet.potentialWin, specifier: "%.2f")")
.foregroundStyle(bet.potentialWin < 0.0 ? .red : .green)
.font(.title2)
.opacity(bet.winBet || bet.looseBet || bet.pushBet ? 1 : 0)
}
}
Text("Motivation")
.foregroundColor(.secondary)
.font(.title3)
.fontWeight(.semibold)
.padding(.top, 15)
if bet.description.isEmpty {
VStack {
Text("No motivation for bet")
}
} else {
Text(bet.description)
.font(.title3)
.multilineTextAlignment(.center)
.padding(.vertical, 15)
.padding(.horizontal, 25)
.background(
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.white)
.shadow(color: .black, radius: 5, x: 5, y: 5)
}
)
}
}
.padding()
}
.onAppear {
withAnimation(Animation.spring().delay(1.15)) {
loadContent.toggle()
}
}
}
}
struct BetDetailsView_Previews: PreviewProvider {
static var betItem1 = BetModel(league: "LaLiga", homeTeam: "Real Madrid", awayTeam: "Atletico Madrid", dateForBEt: Date(), typeOfBet: "1", odds: 1.78, betAmount: 100, bettor: ["Rikard", "Petros"], potentialWin: 178, winBet: false, looseBet: false, pushBet: false, betCount: 1, description: "I think Real Madrid will win because they are so much better than Atletico :)")
@Namespace static var namespace
@State static var bool: Bool = false
static var previews: some View {
BetDetailsView(bet: betItem1, betViewModel: BetViewModel(), showBetDetails: $bool, animation: namespace)
}
}
import Foundation
import SwiftUI
struct BetModel: Identifiable, Codable { // Identifiable so we have id on each item and codable so it can handle JSON data
let id: String
var league: String
let homeTeam: String
let awayTeam: String
var dateForBet: Date
let typeOfBet: String
let odds: Double
let betAmount: Double
var bettor: [String]
var potentialWin: Double
var winBet: Bool
var looseBet: Bool
var pushBet: Bool
var betCount: Int
let description: String
init(id: String = UUID().uuidString, league: String, homeTeam: String, awayTeam: String, dateForBEt: Date, typeOfBet: String, odds: Double, betAmount: Double, bettor: [String], potentialWin: Double, winBet: Bool, looseBet: Bool, pushBet: Bool, betCount: Int, description: String) {
self.id = id
self.league = league
self.homeTeam = homeTeam
self.awayTeam = awayTeam
self.dateForBet = dateForBEt
self.typeOfBet = typeOfBet
self.odds = odds
self.betAmount = betAmount
self.bettor = bettor
self.potentialWin = potentialWin
self.winBet = winBet
self.looseBet = looseBet
self.pushBet = pushBet
self.betCount = betCount
self.description = description
}
func updateWinCompletion() -> BetModel {
return BetModel(id: id, league: league, homeTeam: homeTeam, awayTeam: awayTeam, dateForBEt: dateForBet, typeOfBet: typeOfBet, odds: odds, betAmount: betAmount, bettor: bettor, potentialWin: winBet ? 0.0 : (odds * betAmount) - betAmount, winBet: !winBet, looseBet: looseBet, pushBet: pushBet, betCount: betCount, description: description)
}
func updateLostCompletion() -> BetModel {
return BetModel(id: id, league: league, homeTeam: homeTeam, awayTeam: awayTeam, dateForBEt: dateForBet, typeOfBet: typeOfBet, odds: odds, betAmount: betAmount, bettor: bettor, potentialWin: looseBet ? 0.0 : -betAmount, winBet: winBet, looseBet: !looseBet, pushBet: pushBet, betCount: betCount, description: description)
}
func updatePushCompletion() -> BetModel {
return BetModel(id: id, league: league, homeTeam: homeTeam, awayTeam: awayTeam, dateForBEt: dateForBet, typeOfBet: typeOfBet, odds: odds, betAmount: betAmount, bettor: bettor, potentialWin: 0.0, winBet: winBet, looseBet: looseBet, pushBet: !pushBet, betCount: betCount, description: description)
}
}
import SwiftUI
struct BetRowView: View {
var bet: BetModel
@EnvironmentObject var betViewModel: BetViewModel
@State var scaleWinButton: CGFloat = 1.0
@State var scaleLooseButton: CGFloat = 1.0
@State var scalePushButton: CGFloat = 1.0
@State var currentTime = Date()
var animation: Namespace.ID
var body: some View {
ZStack(alignment: .top) {
VStack(spacing: 5) {
if currentTime <= bet.dateForBet {
VStack {
Text(bet.dateForBet, style: .offset)
Text("to game time")
}
.font(.system(size: 10))
} else if currentTime >= bet.dateForBet.addingTimeInterval(5) {
VStack {
Text("Game ended")
.font(.system(size: 10))
}
.padding(2)
.background(
Color.red
.opacity(0.4)
)
} else {
Text("Game started")
.font(.system(size: 10))
}
}
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
currentTime = Date()
}
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 5) {
Text(bet.league)
.foregroundColor(.secondary)
.font(.system(size: 10))
.fontWeight(.semibold)
Text(bet.homeTeam)
.font(.system(size: 15))
Text(bet.awayTeam)
.font(.system(size: 15))
HStack(spacing: 3) {
Text("Bet:")
.fontWeight(.semibold)
Text(bet.typeOfBet)
.foregroundColor(.secondary)
.padding(.trailing, 8)
Text("Odds:")
.fontWeight(.semibold)
Text("\(bet.odds, specifier: "%.2f")")
.foregroundColor(.secondary)
.padding(.trailing, 8)
Text("Amount:")
.fontWeight(.semibold)
Text("\(bet.betAmount, specifier: "%.2f")")
.foregroundColor(.secondary)
}
.font(.system(size: 10))
}
.lineLimit(1)
Spacer()
VStack(spacing: 5) {
RoundedRectangle(cornerRadius: 4)
.frame(width: 35, height: 17)
.foregroundColor(bet.winBet ? .green : .green.opacity(0.1))
.overlay {
Text("W")
.font(.system(size: 10))
.foregroundColor(bet.winBet ? .white : .black)
.fontWeight(bet.winBet ? .semibold : .light)
}
.scaleEffect(scaleWinButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scaleWinButton)
.onTapGesture {
betViewModel.updateWinBet(bet: bet)
scaleWinButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scaleWinButton = 1.0
}
}
RoundedRectangle(cornerRadius: 5)
.frame(width: 35, height: 17)
.foregroundColor(bet.looseBet ? .red : .red.opacity(0.1))
.overlay {
Text("L")
.font(.system(size: 10))
.foregroundColor(bet.looseBet ? .white : .black)
.fontWeight(bet.looseBet ? .semibold : .light)
}
.scaleEffect(scaleLooseButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scaleLooseButton)
.onTapGesture {
betViewModel.updateLooseBet(bet: bet)
scaleLooseButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scaleLooseButton = 1.0
}
}
RoundedRectangle(cornerRadius: 5)
.frame(width: 35, height: 17)
.foregroundColor(bet.pushBet ? .blue : .blue.opacity(0.1))
.overlay {
Text("P")
.font(.system(size: 10))
.foregroundColor(bet.pushBet ? .white : .black)
.fontWeight(bet.pushBet ? .semibold : .light)
}
.scaleEffect(scalePushButton)
.animation(.easeInThenBackOut(duration: 0.3), value: scalePushButton)
.onTapGesture {
betViewModel.updatePushBet(bet: bet)
scalePushButton = 1.4
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
scalePushButton = 1.0
}
}
}
.matchedGeometryEffect(id: bet.id, in: animation)
}
.frame(maxWidth: .infinity)
}
}
}
extension Animation {
static var easeInThenBackOut: Animation {
Animation.timingCurve(0.5, -1, 0.5, 1.5)
}
static func easeInThenBackOut(duration: TimeInterval = 0.2) -> Animation {
Animation.timingCurve(0.5, -1, 0.5, 1.5, duration: duration)
}
}
struct BetRowView_Previews: PreviewProvider {
static var betItem1 = BetModel(league: "LaLiga", homeTeam: "Real Madrid", awayTeam: "Atletico Madrid", dateForBEt: Date(), typeOfBet: "U2.5", odds: 1.73, betAmount: 100, bettor: ["Rikard", "Petros"], potentialWin: 178, winBet: false, looseBet: false, pushBet: false, betCount: 1, description: "Hello there")
@Namespace static var namespace
static var previews: some View {
BetRowView(bet: betItem1, animation: namespace)
.previewLayout(.sizeThatFits)
}
}
import SwiftUI
struct BetView: View {
@EnvironmentObject var betViewModel: BetViewModel
// Variables for the matched geometry effect animation to betDetailsView
@State var selectedBet: BetModel?
@State var showBetDetails: Bool = false
@Namespace var animation
var body: some View {
ZStack {
NavigationStack { // ändra till NavigationStack o mdet inte funkar med matched geometry effecten
VStack {
if betViewModel.betItems.isEmpty {
NoBetView()
} else {
ZStack {
List {
ForEach(betViewModel.betItems) { bet in
BetRowView(bet: bet, animation: animation)
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.spring(response: 0.35, dampingFraction: 0.6)) {
selectedBet = bet
showBetDetails.toggle()
}
}
/* .background {
NavigationLink("", destination: BetDetailsView(betDetail: bet))
.opacity(0)
} */ // lägg tillbaka denna om du vill köra med navigation link istället för matched geometry effect
}
.onDelete(perform: betViewModel.deleteBet)
.onMove(perform: betViewModel.moveBet)
//.listRowSeparator(.hidden)
}
.listStyle(.inset)
VStack {
Spacer()
HStack {
Spacer()
NavigationLink(destination: AddBetView(), label: {
Image(systemName: "plus")
.foregroundColor(.white)
.font(.system(size: 30))
.padding()
.background(
Circle()
.foregroundColor(.green)
.shadow(color: .black.opacity(0.2), radius: 1, x: 1, y: 1)
)
})
.padding(20)
}
}
}
}
}
.navigationBarItems(leading: EditButton(), trailing: NavigationLink("Add", destination: AddBetView()))
}
.opacity(showBetDetails ? 0 : 1)
if showBetDetails {
BetDetailsView(bet: selectedBet!, betViewModel: betViewModel, showBetDetails: $showBetDetails, animation: animation)
}
}
}
}
import Foundation
class BetViewModel: ObservableObject {
@Published var betItems: [BetModel] = []
@Published var newBettors: [AddBettorDataModel] = []
init() {
getBets() // fetches my data from getBets() function
}
public var totalWinningsLosses: Double {
return betItems.reduce(0) { sum, betResults in
sum + betResults.potentialWin
}
}
func newBettorAdded(bettor: String) {
let newBettor = AddBettorDataModel(newBettor: bettor)
newBettors.insert(newBettor, at: 0)
// saveBet()
}
func makeChartData(from bets: [BetModel]) -> [BetDataPoint] {
let sortedBets = betItems.sorted { $0.dateForBet < $1.dateForBet }
var currentTotal = 0.0
let dataPoints = sortedBets.map { bet in
currentTotal += bet.potentialWin
return BetDataPoint(date: bet.dateForBet, total: currentTotal, league: bet.league)
}
return dataPoints
}
func chartDataPerWeek(from bets: [BetModel]) -> [BetDataPointPerWeek] {
let lastWeek = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
let sortedBetsLastWeek = betItems.filter { $0.dateForBet >= lastWeek }.sorted(by: { $0.dateForBet < $1.dateForBet })
// let sortedBetsLastWeek = betItems.sorted { $0.dateForBet < $1.dateForBet }
var currentWeekTotal = 0.0
let weekDataPoints = sortedBetsLastWeek.map { weekBets in
currentWeekTotal += weekBets.potentialWin
return BetDataPointPerWeek(date: weekBets.dateForBet, totalLastWeek: currentWeekTotal)
}
return weekDataPoints
}
let betItemKey: String = "Bets"
func getBets() {
guard let data = UserDefaults.standard.data(forKey: betItemKey),
let savedBetItems = try? JSONDecoder().decode([BetModel].self, from: data)
else { return }
self.betItems = savedBetItems.sorted(by: { $0.dateForBet > $1.dateForBet }) // this will sort all bets in the array by date.
}
// MARK: Sorts all bets by date in the WinChart View
func sortListofBetsByDate() -> [BetModel] {
betItems.sorted(by: { $0.dateForBet > $1.dateForBet })
}
// MARK: sorts the bets to only the latest 7 days bet results
func resultsLastWeek() -> [BetModel] {
let lastWeek = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
return betItems.filter { $0.dateForBet >= lastWeek }.sorted(by: { $0.dateForBet < $1.dateForBet})
}
// MARK: So you can delete bets from the list
func deleteBet(indexSet: IndexSet) {
betItems.remove(atOffsets: indexSet)
saveBet()
}
// MARK: So you can move items in the bet list
func moveBet(from: IndexSet, to: Int) {
betItems.move(fromOffsets: from, toOffset: to)
}
// MARK: Counts all winning bets
func winningBets() -> Int {
betItems.filter { $0.winBet == true }.count
}
// MARK: Counts all loosing bets.
func loosingBets() -> Int {
betItems.filter { $0.looseBet == true }.count
}
// MARK: counts all bet that are push bets
func pushBets() -> Int {
betItems.filter { $0.pushBet == true }.count
}
// MARK: calculates all bets that have not been decided yet, those that are ongoing
func ongoingBets() -> Int {
betItems.filter { $0.winBet == false && $0.looseBet == false && $0.pushBet == false }.count
}
// func addBettor() -> [String] {
//}
// MARK: So you can add a bet in the bet view, save saveBet() is there so the list is saved when app is closed by user.
func addBet(league: String, homeTeam: String, awayTeam: String, dateForBet: Date, typeOfBet: String, odds: Double, betAmount: Double, bettor: [String], potentialWin: Double, betCount: Int, description: String) {
let newBet = BetModel(league: league, homeTeam: homeTeam, awayTeam: awayTeam, dateForBEt: dateForBet, typeOfBet: typeOfBet, odds: odds, betAmount: betAmount, bettor: bettor, potentialWin: potentialWin, winBet: false, looseBet: false, pushBet: false, betCount: betCount, description: description)
// betItems.append(newBet)
betItems.insert(newBet, at: 0) // use this instead if you want your bet to append at the top of your list
saveBet()
}
// MARK: Updates the bet row view and toggles the winbet boolean from false to true.
func updateWinBet(bet: BetModel) {
if let betWinIndex = betItems.firstIndex(where: {$0.id == bet.id }) {
betItems[betWinIndex] = bet.updateWinCompletion() // updateWinCompletion sätter vi ju in vår BetModel
}
}
// MARK: Updates the bet row view and toggles the loosebet boolean from false to true.
func updateLooseBet(bet: BetModel) {
if let betLooseIndex = betItems.firstIndex(where: {$0.id == bet.id }) {
betItems[betLooseIndex] = bet.updateLostCompletion() // updateLostCompletion sätter vi ju in vår BetModel
}
}
// MARK: Updates the bet row view and toggles the pushbet boolean from false to true.
func updatePushBet(bet: BetModel) {
if let betPushIndex = betItems.firstIndex(where: {$0.id == bet.id}) {
betItems[betPushIndex] = bet.updatePushCompletion()
}
}
// MARK: So data get saved once the app is closed by the user,
func saveBet() {
if let encodedBetData = try? JSONEncoder().encode(betItems) {
UserDefaults.standard.set(encodedBetData, forKey: betItemKey)
getBets() // by adding the function here, all new items in the array will be sorted on date as well.
} else {
print("Couldn't save bet")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment