UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: @Published property is not updated in ContentView

Forums > SwiftUI

I have a Dice rolling game which I thought was working just fine. ContentView shows the dice as they roll. I have a diceRolls array that gets updated with the latest rolls. I have a DiceRollsListView that displays the list of rolls. All of that works. I have swipe to delete code, and some empty list code... all of that works...and the deletions do get saved to disk. However, when you roll the dice after a deletion...and go back to the list view... it shows all of the rolls that you last deleted. this ONLY happens after a dice roll. If you go the list BEFORE a dice roll, the list shows the deletions correct...close the app and reopen, it shows the deletions correct.

So it is as if ContentView does NOT seem to realize the array was emptied or modified.

The GitHub for this is: https://github.com/VulcanCCIT/DiceFun

This has to be something simple and I am sure Paul covered what this might be somewhere in the course, but I am stumped. I must be missing something. Any help on this would be totally appreciated. I posted the code that I think you will need below, but it might be easier to just load the GitHub as it has it all.

Thank you in advance!!!

ContentView-ViewController code:

import AVFoundation
import Foundation
import SwiftUI
import UIKit

extension ContentView {
  @MainActor class ViewModel: ObservableObject {

    @AppStorage("soundOn") var soundOn = false

    let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedRolls.json")

    @Published var feedback = UIImpactFeedbackGenerator(style: .rigid)

    @Published var diceRolls: [RollResult]
    @Published var showingDiceRollList = false

    @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 = -50
    @Published var dice2OffsetValX: CGFloat = 0
    @Published var dice2OffsetValY: CGFloat = -50

    @Published var dice1OffsetValZ: CGFloat = 1
    @Published var dice2OffsetValZ: CGFloat = 1

    @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 {
        print("viewModel Save Function shows dicerolls to be: \(diceRolls)")
        if diceRolls.isEmpty{ print("ContenView-ViewModel save function shows diceRolls is Empty...")}

        print("Saving....")
        let data = try JSONEncoder().encode(diceRolls)
        try data.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
      } catch {
        print("Unable to save data.")
      }
    }

    func updateRolls() {
      if diceRolls.isEmpty{ print("ContenView-ViewModel updateRolls shows diceRolls is Empty...")}
      rollTotal = diceVal1 + diceVal2
      diceRolls.append(RollResult(lastDice1RollValue: diceVal1, lastDice2RollValue: diceVal2, lastRollTotal: rollTotal))
      //print(diceRolls)
      save()
    }

    func playSounds(_ soundFileName : String) {
      guard let soundURL = Bundle.main.url(forResource: soundFileName, withExtension: nil) else {
        fatalError("Unable to find \(soundFileName) in bundle")
      }

      do {
        diceRollSound = try AVAudioPlayer(contentsOf: soundURL)
      } catch {
        print(error.localizedDescription)
      }
      diceRollSound.play()
    }

    func spin() {
      isActive = true
      if soundOn { playSounds("DiceRollCustom1.wav") }
      timeRemaining = 10
      dice1OffsetValZ = 1
      dice2OffsetValZ = 1
      print("time remaining: \(timeRemaining)")
      bounce.toggle()
      degree += 360
      degree2 += 360
      angle += 45
    }

    func diceOffset() {
      guard isActive else { return }

      if timeRemaining > 0 {
        timeRemaining -= 1
        diceVal1 = Int.random(in: 1...6)
        diceVal2 = Int.random(in: 1...6)

        dice1OffsetValX = CGFloat.random(in: -20...40)
        dice1OffsetValY = CGFloat.random(in: -100...100)
        dice2OffsetValX = CGFloat.random(in: -20...40)
        dice2OffsetValY = CGFloat.random(in: -100...100)

        dice1OffsetValZ = CGFloat.random(in: 1...2.0)
        dice2OffsetValZ = CGFloat.random(in: 1...2.0)

        if soundOn { feedback.impactOccurred() }

        print("Time Remaining is: \(timeRemaining)")
        print("ValX \(dice1OffsetValX)")
        print("ValY \(dice1OffsetValY)")
        print("Bounce State is \(bounce)")
      }
      if timeRemaining == 1 { isActive = false
        dice1OffsetValZ = 1
        dice2OffsetValZ = 1
        print("timerstopped")
        updateRolls()
      }
    }
  }
}

ContentView code:

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("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)
        .padding(.bottom)

        Text("Roll Total: \(viewModel.rollTotal)")
          .frame(width: 200)
          .background(.red)
          .foregroundColor(.white)
          .font(.title.bold())
          .ignoresSafeArea()
          .clipShape(Capsule())
        ZStack {
          Image("Leather4")
            .resizable()
            .shadow(color: .secondary.opacity(0.7), radius: 20)
            .padding()
          HStack {
            DiceView(diceVal: "\(pickerColor)\(viewModel.diceVal1)", diceColor: pickerColor.color, degree: viewModel.degree, offsetX: viewModel.dice1OffsetValX, offsetY: viewModel.dice1OffsetValY, offsetZ: viewModel.dice1OffsetValZ)

            DiceView(diceVal: "\(pickerColor)\(viewModel.diceVal2)", diceColor: pickerColor.color, degree: viewModel.degree2, offsetX: viewModel.dice2OffsetValX, offsetY: viewModel.dice2OffsetValY, offsetZ: viewModel.dice2OffsetValZ)

          } //HStack
        }//ZStack
        .toolbar {
          ToolbarItem(placement: .navigationBarLeading) {
            Button {
              viewModel.soundOn.toggle()
            } label: {
              Label("Sound On/Off",  systemImage: viewModel.soundOn ? "speaker.wave.3" :  "speaker.slash")
            }
          }
          ToolbarItemGroup(placement: .navigationBarTrailing) {
            NavigationLink(destination: DiceRollListView()){
              Image(systemName: "dice")
            }
            .padding()
          }
        }
      }//VStack
      .onReceive(timer) { time in
        viewModel.diceOffset()
      }
      .onChange(of: scenePhase) { //newPhase in
        if scenePhase == .active {
          viewModel.isActive = true
        } else {
          viewModel.isActive = false
        }
      }
      Button("Roll Dice!") {
        print("RollDice Button Press shows diceRolls to be: \(viewModel.diceRolls)")
        viewModel.spin() }
      .padding()
      .background(.blue)
      .foregroundColor(.white)
      .clipShape(Capsule())
    }//Nav
  }//View

  func updatePickerColor() {
    let appearance = UISegmentedControl.appearance(for: .current)
    appearance.selectedSegmentTintColor = UIColor(pickerColor.color)
  }
}

#Preview {
  ContentView()
}

DiceRollListVIew code:

import SwiftUI

struct DiceRollListView: View {
  @StateObject private var viewModel = ContentView.ViewModel()
  @State private var showingAlert = false

  var body: some View {
    Text("Past Dice Results")
      .font(.title2).bold()

    List {
      ForEach(viewModel.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)

        }//HStack
      }//List      
      .onDelete(perform: removeRolls)
    }//ForEach
    .toolbar {
      ToolbarItem(placement: .destructiveAction) {
        Button {
          showingAlert.toggle()
          print("button pressed.......")
        }
      label: {
        Image(systemName: "trash.circle")
      }
      .alert(isPresented:$showingAlert) {
        Alert(
          title: Text("Delete All Rolls?"),
          message: Text("There is no undo"),
          primaryButton: .destructive(Text("Delete")) {
            viewModel.diceRolls = []
            print("Deleting...")
            if viewModel.diceRolls.isEmpty {
              viewModel.save()
            }
          },
          secondaryButton: .cancel()
        )
      }
      }//tb
    }
    .onAppear(perform: {
      print(viewModel.diceRolls)
    })
  }//View

  func removeRolls(at offsets: IndexSet) {
    viewModel.diceRolls.remove(atOffsets: offsets)
    if viewModel.diceRolls.isEmpty { print("remove rolls shows array is empty") }
    viewModel.save()
    print("DiceRollListView Save was called")
  }

}

#Preview {
  //DiceRollListView(diceRolls: [RollResult(lastDice1RollValue: 3, lastDice2RollValue:4, lastRollTotal: 7)])
  DiceRollListView()
}

3      

@Obelix unless I have totally mis-understood your reply, and Paul's lessons on MVVM, I have made a ContentView-ViewController exactly how he does...

"Right from the start, my head hurts seeing that you've defined the ViewModel class in an extension to your ContentView. Remember from the review above, the DataBox™ should exist outside your views so your view can all share the same DataBox™."

This is exactly how Paul has shown us in various lessons. I do have a dice model, I just didnt post in in my question...

Dice model:

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

Dice View:

struct DiceView: View {
  var diceVal = "Red1"
  var diceColor: Color = .red
  var degree = 0.0
  var offsetX: CGFloat = 0
  var offsetY: CGFloat = -50
  var offsetZ: CGFloat = 1

  var body: some View {
    Image(diceVal)
      .resizable()
      .frame(width: 100, height:  100)
      .shadow(color: diceColor.opacity(0.4), radius: 10, x: 10, y: -12)
      .rotation3DEffect(.degrees(degree), axis: (x: 0, y: 0, z: 1))
      .offset(x: offsetX, y: offsetY)

      .animation(Animation.interpolatingSpring(stiffness: 50, damping: 15), value: diceVal)
      .scaleEffect(offsetZ)
  }
}

#Preview {
  DiceView()
}

So given these little code snipits I am not questioning your answer, but just trying to see where I have deviated from some of the other lessons...

the FriendMinder is somewhat how i patterned the ViewModel-Controller...

3      

@Obelix, its part of the normal HackingWithSwift 100 Days of SwiftUI The FriendMinder is HackingWithSwift+ but my code is also similar to the BucketList lessons, at least as far as the ContentView and ContentView-ViewController ...at least I think it is. tomorrow I will look at your explinations and suggestions and compare to BucketList...

Of Course, thank you for your time already invested in helping my UnSwiftyNess lol

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

So close yet so far!. I bet that what you feeling at the moments, we all do!

The reason is that you are creating two instances of the ViewModel, one in ContentView and one DiceRollListView. A ViewModel should not be used in to Views other then the one it was made for, as it suggest it belongs to that View

I would look at a class to control the data (maybe DataController) then put all the code related to saving/deleting/reading data in there and then put in the Enviroment as it used in more then one View. (If you not sure Paul's uses DataControllers quite a bit.)

This will then leave only code for ContentView in the ViewModel. I did not look but if you have code for DiceRollListView in there (other then data control) either make a extension DiceRollListView and put a ViewModel and code for that there or use it View.

PS looks good (maybe bit need on animation of dice)

PSS It might be look good to do this Screenshot of RollDiceListView with dice as images

and if you save the color of dice could use that even better (maybe just have "Total").

3      

@NigelGee yes I remember some DataController lessons, I need to go back and look at those. Your explination makes sense and yes, as soon as I found that bug I went from feeling Jubliant to Depressed in a matter of minutes!!! lol. it was like climbing a huge mountain and just before I got to the top, I fell back down the hill!

I LOVE the idea of putting the dice in the List View! I am saving the color as an AppStorage right now...

Question, what did you mean here: "PS looks good (maybe bit need on animation of dice)"

I am now off to look up DataControllers.

3      

@NigelGee I am trying avoid CoreData

I never said use CoreData, I said make a DataController - this can be used to save/read/delete to FileManager!

You now made a ViewModel for DiceRollListView. Try changing @StateObject var viewModel: ViewModel to @StateObject var viewModel = ViewModel()

Have you update your Github. I will check

PS I did not mean all the dice to same color what they use to roll the dice that was saved. EG if user used on first time blue then next time yellow etc then those color show in the list. Means you have to add a color property to get the color used at time and save it.

3      

@NigelGee you must of read my reply just before I deleted it...I talked about CoreData for a bit, then I had some other questions on my code then I figured it out and deleted my post. I have not updated GitHub yet as I did, just for grins try CoreData but I am not sure it works well with the MVVM model I already had in place. So I am going to back out of that... Once I do that I will have a ViewModel controller for both ContentView and DiceRollListView, but coding the central location to save is eluding me. What I tried still has the same issues syncing....but I think maybe im a lot closer. I hope it is slow at my work tomorrow and I can do some more coding...once I have it updated ill let you know.

I do appreciate your assistance and all of this is helping me learn. I thank you a ton for that! :)

Coding is a hobby and a passion and not my real job. My goal is to someday, get something into the appStore ... I have only been doing Swift for a year... prior to that I did some C# , C, c++ and Delphi, but all of those just off and on. My real job is a Radio Broadcast engineer :D

So hang tight and I hope to have something tomorrow :)

3      

@NigelGee, actually, while it all was fresh in my mind, I pulled out the CoreData stuff and its back to the addition of the DiceRollListView-ViewController

Now it just needs some sort of DataController. Its about 12:20 am Central Time in the states, so off to bed for now :). Not sure what timezone you are in but have a look at my Github :D

3      

Hi @VulcanCCIT

I live in UK (FYI). I have put this on GitHub - DiceFun-main

I ended up calling the "dataController" DiceController as it does all the dice controls too. First create a Filemanger Helper file or change the FileManager-DocumentsDirectory to this. It useful bit of code to be able to encode/decode data from File Manager.

extension FileManager {
    private func getDocumentsDirectory() -> URL {
        let paths = self.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }

    func encode<T: Encodable>(_ input: T, to file: String) throws {
        let url = getDocumentsDirectory().appendingPathComponent(file)
        let encoder = JSONEncoder()

        let data = try encoder.encode(input)
        try data.write(to: url, options: [.atomic, .completeFileProtection])
    }

    func decode<T: Decodable>(_ type: T.Type = T.self,
                              from file: String,
                              dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
                              keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys
    )  throws -> T {
        let url = getDocumentsDirectory().appendingPathComponent(file)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = dateDecodingStrategy
        decoder.keyDecodingStrategy = keyDecodingStrategy

        let data = try Data(contentsOf: url)
        let loaded = try decoder.decode(T.self, from: data)
        return loaded
    }
}

I then started to move code from Content View Model to a new file called DiceController. and ended with this

import SwiftUI
import UIKit

@Observable
class DiceController {
    var rollResults: [RollResult]

    var isActive = false
    var timeRemaining = 0

    var rollTotal = 0
    var diceVal1 = 1
    var diceVal2 = 1

    var dice1OffsetValX: CGFloat = 0
    var dice1OffsetValY: CGFloat = -50
    var dice2OffsetValX: CGFloat = 0
    var dice2OffsetValY: CGFloat = -50

    var dice1OffsetValZ: CGFloat = 1
    var dice2OffsetValZ: CGFloat = 1

    var degree = 0.0
    var degree2 = 0.0
    var angle: Double = 0
    var bounce = false

    var soundOn = UserDefaults.standard.bool(forKey: "soundOn")
    var feedback = UIImpactFeedbackGenerator(style: .rigid)

    let fileName = "results.json"

    init() {
        do {
            rollResults = try FileManager().decode(from: fileName)
        } catch {
            rollResults = []
        }
    }

    func save() {
        do {
            try FileManager().encode(rollResults, to: fileName)
        } catch {
            print("Failed to encode results")
        }
    }

    func delete(_ offsets: IndexSet) {
        rollResults.remove(atOffsets: offsets)
        save()
    }

    func updateRolls() {
        rollTotal = diceVal1 + diceVal2
        rollResults.append(RollResult(lastDice1RollValue: diceVal1, lastDice2RollValue: diceVal2, lastRollTotal: rollTotal))

        save()
    }

    func diceOffset() {
        guard isActive else { return }

        if timeRemaining > 0 {
            timeRemaining -= 1
            diceVal1 = Int.random(in: 1...6)
            diceVal2 = Int.random(in: 1...6)

            dice1OffsetValX = CGFloat.random(in: -20...40)
            dice1OffsetValY = CGFloat.random(in: -100...100)
            dice2OffsetValX = CGFloat.random(in: -20...40)
            dice2OffsetValY = CGFloat.random(in: -100...100)

            dice1OffsetValZ = CGFloat.random(in: 1...2.0)
            dice2OffsetValZ = CGFloat.random(in: 1...2.0)

            if soundOn { feedback.impactOccurred() }

        }
        if timeRemaining == 1 {
            isActive = false
            dice1OffsetValZ = 1
            dice2OffsetValZ = 1
            print("timerstopped")
            updateRolls()
        }
    }

    func spin() {
        isActive = true
        timeRemaining = 10
        dice1OffsetValZ = 1
        dice2OffsetValZ = 1
        bounce.toggle()
        degree += 360
        degree2 += 360
        angle += 45
    }
}

Now inject to enviroment in DiceFunApp

@main
struct DiceFunApp: App {
    @State private var diceController = DiceController()

  var body: some Scene {
    WindowGroup {
      ContentView()
            .environment(diceController)
    }
  }
}

Then in ContentView used it. (Also add a new function to call spin from diceController).

import AVFoundation
import SwiftUI

struct ContentView: View {
    @Environment(DiceController.self) var diceController
    @StateObject private var viewModel = ViewModel()

    @Environment(\.scenePhase) var scenePhase
    @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)
                .padding(.bottom)

                Text("Roll Total: \(diceController.rollTotal)")
                    .frame(width: 200)
                    .background(.red)
                    .foregroundColor(.white)
                    .font(.title.bold())
                    .ignoresSafeArea()
                    .clipShape(Capsule())
                ZStack {
                    Image("Leather4")
                        .resizable()
                        .shadow(color: .secondary.opacity(0.7), radius: 20)
                        .padding()
                    HStack {
                        DiceView(diceVal: "\(pickerColor)\(diceController.diceVal1)", diceColor: pickerColor.color, degree: diceController.degree, offsetX: diceController.dice1OffsetValX, offsetY: diceController.dice1OffsetValY, offsetZ: diceController.dice1OffsetValZ)

                        DiceView(diceVal: "\(pickerColor)\(diceController.diceVal2)", diceColor: pickerColor.color, degree: diceController.degree2, offsetX: diceController.dice2OffsetValX, offsetY: diceController.dice2OffsetValY, offsetZ: diceController.dice2OffsetValZ)

                    } //HStack
                }//ZStack
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button {
                            viewModel.soundOn.toggle()
                        } label: {
                            Label("Sound On/Off",  systemImage: viewModel.soundOn ? "speaker.wave.3" :  "speaker.slash")
                        }
                    }
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
                        NavigationLink(destination: DiceRollListView()){
                            Image(systemName: "dice")
                        }
                        .padding()
                    }
                }
            }//VStack
            .onReceive(timer) { time in
                diceController.diceOffset()
            }
            .onChange(of: scenePhase) { //newPhase in
                if scenePhase == .active {
                    diceController.isActive = true
                } else {
                    diceController.isActive = false
                }
            }
            Button("Roll Dice!", action: start)
            .padding()
            .background(.blue)
            .foregroundColor(.white)
            .clipShape(Capsule())
        }//Nav
    }//View

    func start() {
        viewModel.playSounds("DiceRollCustom1.wav")
        diceController.spin()
    }

    func updatePickerColor() {
        let appearance = UISegmentedControl.appearance(for: .current)
        appearance.selectedSegmentTintColor = UIColor(pickerColor.color)
    }
}

#Preview {
    ContentView()
        .environment(DiceController())
}

And also used it DiceRollListView

struct DiceRollListView: View {
    @Environment(DiceController.self) var dataController

    @State private var showingAlert = false

    var body: some View {
        Text("Past Dice Results")
            .font(.title2).bold()

        List {
            ForEach(dataController.rollResults) { roll in
                HStack {
                    Image("Blue\(roll.lastDice1RollValue)")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 50)
                        .padding(.horizontal)

                    Image("Blue\(roll.lastDice2RollValue)")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 50)

                    Spacer()
                    Text("Roll Total: ")
                        .fontWeight(.bold)
                    Text("\(roll.lastRollTotal)")
                        .font(.title2)
                        .foregroundColor(.red)
                        .fontWeight(.bold)

                }//HStack
            }//List
            .onDelete(perform: dataController.delete)
        }//ForEach
        .toolbar {
            ToolbarItem(placement: .destructiveAction) {
                Button {
                    showingAlert.toggle()
                    print("button pressed.......")
                }
            label: {
                Image(systemName: "trash.circle")
            }
            .alert(isPresented:$showingAlert) {
                Alert(
                    title: Text("Delete All Rolls?"),
                    message: Text("There is no undo"),
                    primaryButton: .destructive(Text("Delete")) {
                        dataController.rollResults = []
                        print("Deleting...")
                        if dataController.rollResults.isEmpty {
                            dataController.save()
                        }
                    },
                    secondaryButton: .cancel()
                )
            }
            }//tb
        }
    }//View
}

This now read/write the dice rolls correctly.

3      

@NigelGee

Check it out!

I added a var lastDiceColor: String to the DiceData.swift. Then in UpdateRolls, I update the last rolled color :)

Thank you SOOOO much for your help!!! If you ever got to Memphis, Tennessee where there is fantastic BBQ it will be ribs on me! or a Steak!! lol

Again The GitHub for this is: https://github.com/VulcanCCIT/DiceFun

I gave you credit in the push description and left your //Created by NigelGee comments in as you are of course the creator of not only these changes, but creator of great code! I was telling my wife that in a lot of posts here, you seem to help tons of people! This forum is lucky and blessed to have your help sir!!!

Thank you again!

3      

Look great, more dice like! Now you have the DiceController you could improve this call to DiceView at the moment you have alot of variable passed in

DiceView(diceVal: "\(pickerColor)\(diceController.diceVal2)", diceColor: pickerColor.color, degree: diceController.degree2, offsetX: diceController.dice2OffsetValX, offsetY: diceController.dice2OffsetValY, offsetZ: diceController.dice2OffsetValZ)

Add a new enum file called Dice

enum Dice {
    case diceOne, diceTwo
}

then change DiceView to

struct DiceView: View {
    @AppStorage("pickerColor") var pickerColor: PickerColor = .red
    @Environment(DiceController.self) var diceController

    let dice: Dice

    var diceVal: String { dice == .diceOne ? "\(pickerColor)\(diceController.diceVal1)" : "\(pickerColor)\(diceController.diceVal2)" }
    var degree: Double { dice == .diceOne ? diceController.degree : diceController.degree2 }
    var offsetX: CGFloat { dice == .diceOne ? diceController.dice1OffsetValX : diceController.dice2OffsetValX }
    var offsetY: CGFloat { dice == .diceOne ? diceController.dice1OffsetValY : diceController.dice2OffsetValY }
    var offsetZ: CGFloat { dice == .diceOne ? diceController.dice1OffsetValZ : diceController.dice2OffsetValZ }

    var body: some View {
        Image(diceVal)
            .resizable()
            .frame(width: 100, height:  100)
            .shadow(color: pickerColor.color.opacity(0.4), radius: 10, x: 10, y: -12)
            .rotation3DEffect(.degrees(degree), axis: (x: 0, y: 0, z: 1))
            .offset(x: offsetX, y: offsetY)
            .animation(Animation.interpolatingSpring(stiffness: 50, damping: 15), value: diceVal)
            .scaleEffect(offsetZ)
    }

    init(for dice: Dice) {
        self.dice = dice
    }
}

#Preview {
    DiceView(for: .diceOne)
        .environment(DiceController())
}

Then in ContentView you can do

DiceView(for: .diceOne)
DiceView(for: .diceTwo)

3      

@NigelGee,

Oh I love that as I love the ternary operator! I will add that tomorrow, when I get into the office. I remember thinking in the back of my head, when I saw all of my variables, "I bet there is a way to simplify this".

I was telling my wife just yesterday that software is never quite finished!

Thank you again sir!! So very thankful! <3

3      

@NigelGee,

I had to modify DiceView slightly from your code...mainly I had to cast Color( in the .shadow line

and to change the preview code.

import SwiftUI

enum Dice {
  case diceOne, diceTwo
}

struct DiceView: View {
  @AppStorage("pickerColor") var pickerColor: PickerColor = .red
  @Environment(DiceController.self) var diceController

  let dice: Dice

  var diceVal: String { dice == .diceOne ? "\(pickerColor)\(diceController.diceVal1)" : "\(pickerColor)\(diceController.diceVal2)" }
  var degree: Double { dice == .diceOne ? diceController.degree : diceController.degree2 }
  var offsetX: CGFloat { dice == .diceOne ? diceController.dice1OffsetValX : diceController.dice2OffsetValX }
  var offsetY: CGFloat { dice == .diceOne ? diceController.dice1OffsetValY : diceController.dice2OffsetValY }
  var offsetZ: CGFloat { dice == .diceOne ? diceController.dice1OffsetValZ : diceController.dice2OffsetValZ }

  var body: some View {
    Image(diceVal)
      .resizable()
      .frame(width: 100, height:  100)
      .shadow(color: Color(pickerColor.rawValue).opacity(0.4), radius: 10, x: 10, y: -12)
      .rotation3DEffect(.degrees(degree), axis: (x: 0, y: 0, z: 1))
      .offset(x: offsetX, y: offsetY)

      .animation(Animation.interpolatingSpring(stiffness: 50, damping: 15), value: diceVal)
      .scaleEffect(offsetZ)
  }
}

#Preview {
  DiceView(dice: .diceOne)
    .environment(DiceController())
}

then ContentView a small change:

         HStack {            
            DiceView(dice: .diceOne)
            DiceView(dice: .diceTwo)
          }

basically removing the for:

Not sure if I did that right...but it works

what do you think?

3      

It's your code!

I only added init to make it read more natural as "dice view for dice one" rather the "dice view dice dice one". you could change the enum to case one, two then read "dice view dice one"

4      

@NigelGee, I understand I was just seeing if I fixed what you suggested correctly, as it was not compiling...specifically the Cast of Color...

I just edited my post as I re-read your reply about init... NO WONDER it was not compiling...I didnt put the init in there! OOOOPS

Now it works just as you had it. My apologies...I just had missed that and I do like how that works!

AGAIN, THANK YOU SO MUCH!

I have so much more to study...you help has been immense! I do most of this during off hours as my real job is not coding... I just love it so I do it when I have time...

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.