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

SOLVED: Day 95 challenge, help

Forums > 100 Days of SwiftUI

Hi guys,

I have no idea how to implement a save functionality to the app. Is it better to use JSON or Core Data in my case?

Regarding the code I wrote, is it good or does it need improvement? Could you do a code review and give me some ideas for saving?

Thank you

import SwiftUI

struct ContentView: View {
    @StateObject var dice = Dice(side: 4)
    @State private var isShowingSettings = false
    @State private var result: [Int] = []
    var diceColors: [Color] = [.red, .blue, .green, .orange, .purple, .yellow, .indigo, .brown, .cyan, .gray, .mint, .pink]
    @State private var color = 0
    @State private var timer: Timer?
    @State private var isActive = true
    @State private var isDiceRolling = false
    @Environment(\.scenePhase) var scenePhase
    @State private var feedback = UINotificationFeedbackGenerator()

    let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedDiceInfo")

    var body: some View {
        NavigationView {
            VStack {
                Image(systemName: "dice")
                    .resizable()
                    .frame(width: 250, height: 250)
                    .padding(20)
                    .foregroundColor(diceColors[color])

                Button {
                    withAnimation {
                        rollDice()
                        feedback.notificationOccurred(.success)
                        color = Int.random(in: 0...diceColors.count)
                    }
                } label: {
                    Text("Roll")
                        .font(.largeTitle)
                        .padding()
                        .background(.blue)
                        .foregroundColor(.white)
                        .clipShape(Capsule())
                        .padding([.top, .bottom], 20)
                }
                .disabled(isDiceRolling)

                Text("Result: \(result.last ?? 1)")
                    .font(.title)
            }
            .onChange(of: scenePhase, perform: { newPhase in
                if newPhase == .active {
                    isActive = true
                } else {
                    isActive = false
                }
            })
            .toolbar {
                Button {
                    isShowingSettings = true
                } label: {
                    Image(systemName: "gearshape")
                }
            }
            .sheet(isPresented: $isShowingSettings, content: {
                SettingsView(dice: dice)
            })
            .navigationTitle("High Rollers")
        }
    }

    func rollDice() {
        isDiceRolling = true
        feedback.prepare()
        guard isActive else { return }
        timer?.invalidate()

        var rollCounter = 0

        timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true, block: { _ in
            result.append(dice.side == 1 ? 1 : Int.random(in: 1...dice.side))
            rollCounter += 1
            if rollCounter == 5 {
                timer?.invalidate()
                dice.results[Date.now] = result.last
                isDiceRolling = false
                print(result)
                result = []
            }
        })
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
import SwiftUI

struct SettingsView: View {
    @ObservedObject var dice: Dice
    @State private var sideSelected = 4
    @State private var number = 5
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker("Select sides", selection: $sideSelected) {
                        ForEach(dice.sides, id: \.self) { side in
                            Text("\(side)-sided")
                        }
                    }
                } header: {
                    Text("Dice Sides")
                }
                Section {
                    Picker("Add number", selection: $number) {
                        ForEach(0..<100) { num in
                            Text("\(num + 1)")
                        }
                    }
                    Button("Default", role: .destructive) {
                        dice.sides = [4, 6, 8, 10, 12, 20, 100]
                    }
                } header: {
                    Text("Add Side Between 1 and 100")
                }
                Section {
                    List {
                        ForEach(dice.results.sorted(by: <), id: \.key) { key, value in
                            HStack {
                                Image(systemName: "dice.fill")
                                Spacer()
                                VStack(alignment: .leading) {
                                    Text("\(value)")
                                        .bold()
                                    Text("\(key.formatted(date: .abbreviated, time: .omitted))")
                                        .font(.caption)
                                }
                            }
                        }
                        .onDelete(perform: removePastRolls)
                    }
                } header: {
                    Text("Past Rolls")
                }

            }
            .navigationTitle("Settings")
            .toolbar {
                Button {
                    dice.side = sideSelected
                    if !dice.sides.contains(number + 1) {
                        dice.sides.append(number + 1)
                        dice.sides.sort()
                    }
                    dismiss()
                } label: {
                    Text("Done")
                }

            }
        }
    }
    func removePastRolls(at offset: IndexSet) {
        if let ndx = offset.first {
            let item = dice.results.sorted(by: >)[ndx]
            dice.results.removeValue(forKey: item.key)
        }
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView(dice: Dice(side: 4))
    }
}
import Foundation

class Dice: ObservableObject, Identifiable {
    var id: UUID
    @Published var side: Int
    @Published var sides = [4, 6, 8, 10, 12, 20, 100].sorted()
    @Published var results = [Date:Int]()

    init(id: UUID = UUID(), side: Int, sides: [Int] = [4, 6, 8, 10, 12, 20, 100].sorted(), results: [Date : Int] = [Date:Int]()) {
        self.id = id
        self.side = side
        self.sides = sides
        self.results = results
    }
}

3      

Hi @andreasara-dev, There are a few things i noticed,

  1. The result on the main view allways ends in "1"
  2. The settings do not remember the values you enter
  3. In the settings it appears you have the user select the dice sides twice
  4. The onDelete it's not deleting the correct past roll
  5. When you press the default button nothing in the ui changes
  6. There is a part of the code that is crashing your app color = Int.random(in: 0...diceColors.count) it should be 0..<diceColors.count so it doesn't end in index out of range

as for saving i personally went with JSON i think Core Data is kind of an over kill, i use an extension on FileManager

extension FileManager {
    enum ValidationError: Error {
        case failedToLoad
        case failedToDecode
        case failedToEncode
        case failedToWrite
    }

    static var documentsDirectory: URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }

    func decode< T: Codable >(_ type: T.Type, from file: String) throws -> T {
        let url = FileManager.documentsDirectory.appendingPathComponent(file)

        guard let data = try? Data(contentsOf: url) else {
            throw ValidationError.failedToLoad
        }

        guard let loaded = try? JSONDecoder().decode(type, from: data) else {
            throw ValidationError.failedToDecode
        }

        return loaded
    }

    func encode< T: Codable >(_ object: T, to file: String) throws {
        let url = FileManager.documentsDirectory.appendingPathComponent(file)

        guard let encoded = try? JSONEncoder().encode(object) else {
            throw ValidationError.failedToEncode

        }

        do {
            try encoded.write(to: url, options: [.atomic])
        } catch {
            throw ValidationError.failedToWrite
        }
    }
}

so i would just call the encode and decode

do {
    try FileManager.default.encode(settings, to: "SavePath.txt")
} catch {
    print(error)
}
do {
    let decoded = try FileManager.default.decode(Settings.self, from: "SavePath.txt")
    numberOfDice = decoded.numberOfDice
    numberOfSides = decoded.numberOfSides
    let history = try FileManager.default.decode([Dice].self, from: "HistorySave.txt")
    self.history = history

} catch {
    print(error)
}

2      

Ok I resolved the bugs, but I can't figure it out where to call the functions for decode and encode.

I need to create an enum CodingKey to add Codable conformance to the class?

How can I tackle this?

Thank you guys

2      

Hi, Yes, you need three things,

  1. an enum CodingKeys
  2. a required init(from decoder: Decoder) throws {}
  3. a func encode(to encoder: Encoder) throws {}

and that will add Codable conformance to the class. after that you would call your decode and encode functions, for example you could call the decode function from the original init (Not the required one ) so it populates the variables and you could call the encode in your rollDice() function so it saves after each roll.

check out this article on cupcake corner to see how you would add conformance to the class. Adding Codable conformance for @Published properties

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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.