So everything works well and this app is about they way I want it, except that I want to have the ability in the DiceRollListView to delete a dice roll... I can get that to work only while on that view, but when I go back to the view, the change is not saved, nor does it save after closing to the app. None of that code code I used to delete/save is shown as it just was not working... Can someone point me in the right direction? I have tried various methods used from other apps in the HWS course but I am missing something...I think I have been looking at it too long. I have the full GitHub available as well at:
https://github.com/VulcanCCIT/DiceFun
Code below:
I have a data model of:
import SwiftUI
struct RollResult: Identifiable, Codable {
var id = UUID()
var lastDice1RollValue: Int
var lastDice2RollValue: Int
var lastRollTotal: Int
init(lastDice1RollValue: Int, lastDice2RollValue: Int, lastRollTotal: Int) {
self.lastDice1RollValue = lastDice1RollValue
self.lastDice2RollValue = lastDice2RollValue
self.lastRollTotal = lastRollTotal
}
}
I have a DiceRollListView like this:
struct DiceRollListView: View {
var diceRolls: [RollResult]
var body: some View {
Text("Past Dice Results")
.font(.title2).bold()
List(diceRolls) { roll in
HStack {
Text("Dice 1: ")
.fontWeight(.bold)
Text("\(roll.lastDice1RollValue)")
.font(.title2)
.foregroundColor(.blue)
.fontWeight(.bold)
Text("Dice 2: ")
.fontWeight(.bold)
Text("\(roll.lastDice2RollValue)")
.font(.title2)
.foregroundColor(.green)
.fontWeight(.bold)
Text("Roll Total: ")
.fontWeight(.bold)
Text("\(roll.lastRollTotal)")
.font(.title2)
.foregroundColor(.red)
.fontWeight(.bold)
}
}
}
}
#Preview {
DiceRollListView(diceRolls: [RollResult(lastDice1RollValue: 3, lastDice2RollValue:4, lastRollTotal: 7)])
}
I have a ContentView-ViewController like this:
import AVFoundation
import Foundation
import SwiftUI
import UIKit
extension ContentView {
@MainActor class ViewModel: ObservableObject {
let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedRolls.json")
@Published var feedback = UIImpactFeedbackGenerator(style: .rigid)
@Published private(set) var diceRolls: [RollResult]
@Published var showingDiceRollList = false
//@Published var segmentColor: UIColor = .blue
@Published var rollTotal = 2
@Published var degree = 0.0
@Published var degree2 = 0.0
@Published var angle: Double = 0
@Published var bounce = false
@Published var dice1OffsetValX: CGFloat = 0
@Published var dice1OffsetValY: CGFloat = 0
@Published var dice2OffsetValX: CGFloat = 0
@Published var dice2OffsetValY: CGFloat = 0
@Published var diceVal1 = 1
@Published var diceVal2 = 1
@Published var timeRemaining = 0
@Published var isActive = false
@Published var diceRollSound: AVAudioPlayer!
init() {
do {
let data = try Data(contentsOf: savePath)
diceRolls = try JSONDecoder().decode([RollResult].self, from: data)
} catch {
diceRolls = []
}
}
func save() {
do {
let data = try JSONEncoder().encode(diceRolls)
try data.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
} catch {
print("Unable to save data.")
}
}
func updateRolls() {
rollTotal = diceVal1 + diceVal2
diceRolls.append(RollResult(lastDice1RollValue: diceVal1, lastDice2RollValue: diceVal2, lastRollTotal: rollTotal))
print(diceRolls)
save()
}
}
}
ContentView like this:
import AVFoundation
import SwiftUI
enum PickerColor: String, Hashable, Identifiable, CustomStringConvertible, CaseIterable {
case red
case yellow
case green
case blue
case purple
var id: String { rawValue }
var description: String { rawValue.capitalized }
var color: Color {
switch self {
case .red: .red
case .yellow: .yellow
case .green: .green
case .blue: .blue
case .purple: .purple
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
@Environment(\.scenePhase) var scenePhase
@AppStorage("soundOn") var soundOn = false
@AppStorage("pickerColor") var pickerColor: PickerColor = .red
var timer = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationStack {
VStack {
Picker(selection: $pickerColor, content: {
ForEach(PickerColor.allCases) { color in
Text(color.description)
.tag(color)
}
}, label: EmptyView.init)
.pickerStyle(.segmented)
.tint(pickerColor.color) .onAppear(perform: updatePickerColor)
.onChange(of: pickerColor,
updatePickerColor)
.frame(height: 50)
Text("Roll Total: \(viewModel.rollTotal)")
.frame(width: 200)
.background(.red)
.foregroundColor(.white)
.font(.title.bold())
.ignoresSafeArea()
.clipShape(Capsule())
.padding()
HStack {
Image("\(pickerColor)\(viewModel.diceVal1)")
.resizable()
.frame(width: 125, height: 125)
.shadow(color: pickerColor.color.opacity(0.4), radius: 10, x: 10, y: -12)
.rotation3DEffect(.degrees(viewModel.degree), axis: (x: 0, y: 0, z: 1))
.offset(x: viewModel.bounce ? 0 : viewModel.dice1OffsetValX, y: viewModel.bounce ? 100 : viewModel.dice1OffsetValY)
.animation(Animation.interpolatingSpring(stiffness: 50, damping: 15), value: viewModel.diceVal1)
Image("\(pickerColor)\(viewModel.diceVal2)")
.resizable()
.frame(width: 125, height: 125)
.shadow(color: pickerColor.color.opacity(0.4), radius: 10, x: 10, y: -12)
.rotation3DEffect(.degrees(viewModel.degree2), axis: (x: 0, y: 0, z: 1))
.offset(x: viewModel.bounce ? 0 : viewModel.dice2OffsetValX, y: viewModel.bounce ? 100 : viewModel.dice2OffsetValY)
.animation(.interpolatingSpring(stiffness: 50, damping: 15), value: viewModel.diceVal2)
} //HStack
.frame(width: 350, height:550)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
soundOn.toggle()
} label: {
Label("Sound On/Off", systemImage: soundOn ? "speaker.wave.3" : "speaker.slash")
}
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
NavigationLink(destination: DiceRollListView(diceRolls: viewModel.diceRolls)) {
Image(systemName: "dice")
}
}
}
.padding()
}//nav
.onReceive(timer) { time in
print(viewModel.isActive)
guard viewModel.isActive else { return }
if viewModel.timeRemaining > 0 {
viewModel.timeRemaining -= 1
viewModel.diceVal1 = Int.random(in: 1...6)
viewModel.diceVal2 = Int.random(in: 1...6)
viewModel.dice1OffsetValX = CGFloat.random(in: -30...30)
viewModel.dice1OffsetValY = CGFloat.random(in: -250...150)
viewModel.dice2OffsetValX = CGFloat.random(in: -30...30)
viewModel.dice2OffsetValY = CGFloat.random(in: -275...150)
if soundOn { viewModel.feedback.impactOccurred() }
print(viewModel.timeRemaining)
print(viewModel.dice1OffsetValX)
print(viewModel.dice1OffsetValY)
}
if viewModel.timeRemaining == 1 { viewModel.isActive = false
print("timerstopped")
viewModel.updateRolls()
}
}
.onChange(of: scenePhase) { //newPhase in
if scenePhase == .active {
viewModel.isActive = true
} else {
viewModel.isActive = false
}
}
Spacer()
Button("Roll Dice!") {
viewModel.isActive = true
viewModel.dice1OffsetValX = CGFloat.random(in: -40...40)
viewModel.dice1OffsetValY = CGFloat.random(in: -275...275)
viewModel.dice1OffsetValX = CGFloat.random(in: -40...40)
viewModel.dice1OffsetValY = CGFloat.random(in: -275...275)
spin()
viewModel.bounce.toggle()
viewModel.degree += 360
viewModel.degree2 += 360
viewModel.angle += 45
}
.padding()
.background(.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}
func spin() {
if soundOn { playSounds("DiceRollCustom1.wav") }
viewModel.timeRemaining = 10
print(viewModel.timeRemaining)
}
func playSounds(_ soundFileName : String) {
guard let soundURL = Bundle.main.url(forResource: soundFileName, withExtension: nil) else {
fatalError("Unable to find \(soundFileName) in bundle")
}
do {
viewModel.diceRollSound = try AVAudioPlayer(contentsOf: soundURL)
} catch {
print(error.localizedDescription)
}
viewModel.diceRollSound.play()
}
func updatePickerColor() {
let appearance = UISegmentedControl.appearance(for: .current) // <- here
appearance.selectedSegmentTintColor = UIColor(pickerColor.color)
}
}
#Preview {
ContentView()
}
FileManager like this:
import Foundation
extension FileManager {
static var documentsDirectory: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}