Trying to update local json file

Viewed 71

To start off, I want to say that I am new to using JSON, so if this code needs some cleanup also, let me know. So I am trying to update json file. I have created an empty jsonfile named data.json in my project, inside a folder I named Data. The file looks like this:

{
    "fullname": "Testuser2",
    "answered": 0,
    "correct": 0
}

This is my JSONData.swift:

import Foundation

struct UserData: Codable {
    let fullname: String
    let answered: Int
    let correct: Int
}

struct GlobalVariable{
    static var fullname = String()
    static var answered = Int()
    static var correct = Int()
}

These function to read the json file:

private func readLocalFile(forName name: String) -> Data? {
        do {
            if let bundlePath = Bundle.main.path(forResource: name,
                                                 ofType: "json"),
                let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
                return jsonData
            }
        } catch {
            print(error)
        }
        return nil
    }
    
    private func parse(jsonData: Data) {
        do {
            let decodedData = try JSONDecoder().decode(UserData.self,
                                                       from: jsonData)
            GlobalVariable.fullname = decodedData.fullname
            GlobalVariable.answered = decodedData.answered
            GlobalVariable.correct = decodedData.correct
        } catch {
            print("decode error")
        }
    }

And this is how I use it:

func getJSON() {
        
        if let localData = self.readLocalFile(forName: "data") {
            self.parse(jsonData: localData)
            
            print(GlobalVariable.fullname)
            print(GlobalVariable.answered)
            print(GlobalVariable.correct)
            
        }
    }

So the getJSON successfully shows me the correct values.

Now after the GlobalVariable.answered and GlobalVariable.correct has been incrementet, I want to save it.

This is the code I am trying to use to save it:

func saveJSON {
        
        print("---------")
        print(GlobalVariable.fullname)
        print(GlobalVariable.answered)
        print(GlobalVariable.correct)
        print("---------")
        
        
        let dataToUpdate = [UserData(fullname: GlobalVariable.fullname, answered: GlobalVariable.answered, correct: GlobalVariable.correct)]

        do {
            let jsonData = try JSONEncoder().encode(dataToUpdate)
            let jsonString = String(data: jsonData, encoding: .utf8)!
            print(jsonString)
            
            if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
                                                                in: .userDomainMask).first {
                let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
                do {
                    try jsonString.write(to: pathWithFilename,
                                         atomically: true,
                                         encoding: .utf8)
                } catch {
                    // Handle error
                }
            }
            
        } catch { print(error) }
    }

The three print codes inside saveJSON shows the correct value, but someone nothing gets saved to the json file. The json file stays untouched.. How come?

I've also tried removing the .json from let pathWithFilename = documentDirectory.appendingPathComponent("data.json"), but still not working..

1 Answers

You are starting with your data.json in your bundle.

You read that, change the values, and then write it to Documents folder.

So, when you want to read the file, you need to first see if it exists in the Documents folder. If so, read it from there. If it doesn't exist (e.g. first time the app is run), then read it from the bundle.


Edit

The general idea is:

  • try to load the json file from the app's Documents folder
  • if that fails
    • create a "blank" user data object and show the Name Input view
  • if we successfully loaded the json file
    • use that data
  • after every change (name was entered, user answered a question), update the user data object and save (overwrite) the json file in the Documents folder

No need to have a .json file in your bundle, because you'll create a blank user data object if there is no .json file in the Documents folder.

Here is a complete example you can run and inspect:

struct UserData: Codable {
    var fullname: String
    var answered: Int
    var correct: Int
}

class FileTestVC: UIViewController {
    
    var userData: UserData!
    
    let nameTextField = UITextField()
    let nameLabel = UILabel()
    let questionLabel = UILabel()
    let resultNameLabel = UILabel()
    let resultLabel = UILabel()

    let userNameView = UIView()
    let questionView = UIView()
    let restartView = UIView()
    
    let numQuestions: Int = 10
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        setupNameView()
        setupQuestionView()
        setupRestartView()
        
        [userNameView, questionView, restartView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            v.layer.cornerRadius = 16
            v.layer.borderWidth = 2
            v.isHidden = true
            view.addSubview(v)
        }
        
        userNameView.layer.borderColor = UIColor.systemBlue.cgColor
        questionView.layer.borderColor = UIColor.systemRed.cgColor
        restartView.layer.borderColor = UIColor.systemGreen.cgColor

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([

            userNameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            userNameView.centerXAnchor.constraint(equalTo: g.centerXAnchor),

            questionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            questionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            restartView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            restartView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
        ])
        
        userData = UserData(fullname: "", answered: 0, correct: 0)
        
        if let jData = readLocalFile(forName: "data.json") {
            do {
                userData = try JSONDecoder().decode(UserData.self, from: jData)
            } catch {
                print(error)
            }
        }
        
        if userData.fullname.isEmpty {
            userNameView.isHidden = false
        } else {
            if userData.answered < numQuestions {
                questionView.isHidden = false
                nameLabel.text = userData.fullname
                showNextQuestion()
            } else {
                showResult()
            }
        }

    }
    
    func setupNameView() {
        
        // add a text field and "Save" button to userNameView
        nameTextField.borderStyle = .roundedRect
        
        let saveBtn = UIButton()
        saveBtn.setTitle("Save", for: [])
        saveBtn.setTitleColor(.white, for: .normal)
        saveBtn.setTitleColor(.lightGray, for: .highlighted)
        saveBtn.backgroundColor = .systemBlue
        saveBtn.layer.cornerRadius = 8
        
        [nameTextField, saveBtn].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            userNameView.addSubview(v)
        }
        
        let g = userNameView
        NSLayoutConstraint.activate([

            nameTextField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            nameTextField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            nameTextField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            nameTextField.widthAnchor.constraint(equalToConstant: 280.0),
            
            saveBtn.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20.0),
            saveBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            saveBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            saveBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
        ])
        
        saveBtn.addTarget(self, action: #selector(saveNameTapped(_:)), for: .touchUpInside)
    }

    func setupQuestionView() {
        
        // add labels + Right and Wrong buttons to questionView
        nameLabel.textAlignment = .center
        nameLabel.backgroundColor = .yellow
        nameLabel.text = "Name"
        
        questionLabel.textAlignment = .center
        questionLabel.backgroundColor = .green
        questionLabel.numberOfLines = 0
        questionLabel.text = "Question"
        
        let rightBtn = UIButton()
        rightBtn.setTitle("Right", for: [])
        rightBtn.setTitleColor(.white, for: .normal)
        rightBtn.setTitleColor(.lightGray, for: .highlighted)
        rightBtn.backgroundColor = .systemGreen
        rightBtn.layer.cornerRadius = 8
        
        let wrongBtn = UIButton()
        wrongBtn.setTitle("Wrong", for: [])
        wrongBtn.setTitleColor(.white, for: .normal)
        wrongBtn.setTitleColor(.lightGray, for: .highlighted)
        wrongBtn.backgroundColor = .systemRed
        wrongBtn.layer.cornerRadius = 8
        
        [nameLabel, questionLabel, rightBtn, wrongBtn].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            questionView.addSubview(v)
        }
        
        let g = questionView
        NSLayoutConstraint.activate([
            
            nameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            nameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            nameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            questionLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20.0),
            questionLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            questionLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            questionLabel.widthAnchor.constraint(equalToConstant: 280.0),
            
            rightBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
            rightBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            
            wrongBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
            wrongBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            wrongBtn.leadingAnchor.constraint(equalTo: rightBtn.trailingAnchor, constant: 20.0),
            wrongBtn.widthAnchor.constraint(equalTo: rightBtn.widthAnchor),
            
            wrongBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
        ])

        rightBtn.addTarget(self, action: #selector(rightAnswerTapped(_:)), for: .touchUpInside)
        wrongBtn.addTarget(self, action: #selector(wrongAnswerTapped(_:)), for: .touchUpInside)
    }

    func setupRestartView() {
        
        // add labels + button to restartView
        resultNameLabel.textAlignment = .center
        resultNameLabel.backgroundColor = .yellow
        resultNameLabel.text = "name"

        resultLabel.textAlignment = .center
        resultLabel.backgroundColor = .green
        resultLabel.numberOfLines = 0
        resultLabel.text = "You answered"

        let restartBtn = UIButton()
        restartBtn.setTitle("Restart", for: [])
        restartBtn.setTitleColor(.white, for: .normal)
        restartBtn.setTitleColor(.lightGray, for: .highlighted)
        restartBtn.backgroundColor = .systemBlue
        restartBtn.layer.cornerRadius = 8
        
        [resultNameLabel, resultLabel, restartBtn].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            restartView.addSubview(v)
        }
        
        let g = restartView
        NSLayoutConstraint.activate([
            
            resultNameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            resultNameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            resultNameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            resultLabel.topAnchor.constraint(equalTo: resultNameLabel.bottomAnchor, constant: 20.0),
            resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            resultLabel.widthAnchor.constraint(equalToConstant: 280.0),
            
            restartBtn.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20.0),
            restartBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            restartBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            restartBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),

        ])
        
        restartBtn.addTarget(self, action: #selector(restartTapped(_:)), for: .touchUpInside)
    }

    @objc func saveNameTapped(_ sender: Any?) {
        guard let s = nameTextField.text,
              !s.isEmpty
        else { return }
        view.endEditing(true)
        userNameView.isHidden = true
        questionView.isHidden = false
        userData.fullname = s
        nameLabel.text = userData.fullname
        saveJSON(userData)
        questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
    }
    
    @objc func restartTapped(_ sender: Any?) {
        // "empty" user data object
        userData = UserData(fullname: "", answered: 0, correct: 0)
        // save (overwrite) existing data
        saveJSON(userData)
        restartView.isHidden = true
        nameTextField.text = ""
        userNameView.isHidden = false
    }
    @objc func rightAnswerTapped(_ sender: Any?) {
        userData.correct += 1
        showNextQuestion()
    }
    @objc func wrongAnswerTapped(_ sender: Any?) {
        showNextQuestion()
    }

    func showNextQuestion() {
        userData.answered += 1
        saveJSON(userData)
        if userData.answered < numQuestions {
            questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
        } else {
            questionView.isHidden = true
            showResult()
        }
    }
    func showResult() {
        restartView.isHidden = false
        resultNameLabel.text = userData.fullname
        resultLabel.text = "You answered \(userData.correct) Right!"
    }
    
    private func readLocalFile(forName name: String) -> Data? {
        
        if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
            let pathWithFilename = documentDirectory.appendingPathComponent(name)
            do {
                let jd = try String(contentsOf: pathWithFilename)
                return jd.data(using: .utf8)
            } catch {
                print(error)
            }
            return nil
        }
        return nil
        
    }
    
    func saveJSON(_ d: UserData) {
        do {
            let jsonData = try JSONEncoder().encode(d)
            let jsonString = String(data: jsonData, encoding: .utf8)!
            
            if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
                let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
                do {
                    try jsonString.write(to: pathWithFilename,
                                         atomically: true,
                                         encoding: .utf8)
                } catch {
                    print(error)
                }
            }
        } catch {
            print(error)
        }
    }

}
Related