LAST CHANCE: Save 50% on all my Swift books and bundles! >>

SOLVED: Deleting items from a ListView but also saving the data to reflect the deletion...

Forums > 100 Days of SwiftUI

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]
  }
}

3      

I figured this out...mainly by passing in the @StateObject private var viewModel = ContentView.ViewModel()

I now have swipe to delete for the list rows, as well as a delete all rolls button.

Even though it works, I may not be using MVVM correctly when it comes to my DiceRollListView...

To see the changes, check out my GitHub for it and I would love some critques PLEASE :D

https://github.com/VulcanCCIT/DiceFun

3      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.