GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

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 Transform your career with the iOS Lead Essentials. Unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a FREE crash course.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.