How do I save and load an array into a table view using singleton database option?

I used the delegate option using UserDefaults to store and load arrays into a table view and it works good but I am trying to use the singleton database option now.


I have everything in my app working right except when I add a player to myRoster or draftOrder it doesn't add the player.


My data:


import Foundation
import UIKit

class PlayerData: Codable {
    var num: Int = 0
    var name: String = ""
    var team: String = ""
    var position: String = ""
    var strikeThrough: Bool = false
    var color: Bool = false
    var accessory: Bool = false
    var rosterPosition: Int = -1
    var draftPosition: Int = -1
    
    init(num: Int, name: String, team: String, position: String) {
      self.num = num
      self.name = name
      self.team = team
      self.position = position
    }
}

var objectsArray = [PlayerData]()

var myRoster: [PlayerData] = []

var draftOrder: [PlayerData] = []

class Database {
    
    static let shared = Database()
    
    fileprivate var allPlayers: [PlayerData]
    
    fileprivate func playerDataURL() -> URL {
        let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask).last!
        return documentDirectoryURL.appendingPathComponent("playerData.json")
    }
    
    fileprivate func playersReadFromDisk() -> [PlayerData]? {
        let filePlayerURL = playerDataURL()
        guard FileManager.default.fileExists(atPath: filePlayerURL.path) else {
            return nil
        }
        do {
            let fileContents = try Data(contentsOf: filePlayerURL)
            let list = try JSONDecoder().decode([PlayerData].self, from: fileContents)
            return list
        } catch let error as NSError {
            NSLog("Error reading file: \(error.localizedDescription)")
        }
        return nil
    }
    
    fileprivate func playersWriteToDisk() {
        do {
            let data = try JSONEncoder().encode(allPlayers)
            try data.write(to: playerDataURL())
        } catch let error as NSError {
            NSLog("Error reading file: \(error.localizedDescription)")
        }
    }
    
    func changeMade() {
        playersWriteToDisk()
    }  
    
    func playerList(position: String) -> [PlayerData] {
      return allPlayers.filter({ $0.position == position })
    }
    
    func rosterList() -> [PlayerData] {
        let rosteredPlayers = allPlayers.filter { ($0.rosterPosition >= 0)}
        return rosteredPlayers.sorted(by: { $0.rosterPosition < $1.rosterPosition })
    }
    
    func draftList() -> [PlayerData] {
      let draftedPlayers = allPlayers.filter({ $0.draftPosition >= 0 })
      return draftedPlayers.sorted(by: { $0.draftPosition < $1.draftPosition })
    }
    
    func addToRosterList(player: PlayerData) {
      let maxRosteredIndex = allPlayers.map({ $0.rosterPosition }).max()!
      player.rosterPosition = maxRosteredIndex + 1
    }
    
    func addToDraftList(player: PlayerData) {
      let maxDraftedIndex = allPlayers.map({ $0.draftPosition }).max()!
      player.draftPosition = maxDraftedIndex + 1
    }
    
    func removeFromRosterList(player: PlayerData) {
        let currentRosterPosition = player.rosterPosition
        player.rosterPosition = -1 // No longer on the rosterList. Gets all players on roster at a higher index.
        let higherNumberedRosterPlayers = allPlayers.filter({ $0.rosterPosition > currentRosterPosition}) // Their rosterPositions have now gone down by one.
        
        for otherPlayer in higherNumberedRosterPlayers {
            otherPlayer.rosterPosition -= 1 // Decrease position by 1.
        }
    }
  
    init() {
        allPlayers = [PlayerData]()
        
        if let playerList = playersReadFromDisk() {
            allPlayers = playerList
        } else {
            allPlayers = [
                // Quarterbacks
                PlayerData(num: 1, name: "Patrick Mahomes", team: "KC", position: "QB"),
                PlayerData(num: 2, name: "Deshaun Watson", team: "HOU", position: "QB"),
                PlayerData(num: 3, name: "Aaron Rodgers", team: "GB", position: "QB"),
                PlayerData(num: 4, name: "Matt Ryan", team: "ATL", position: "QB"),
                PlayerData(num: 5, name: "Baker Mayfield", team: "CLE", position: "QB"),
                PlayerData(num: 6, name: "Carson Wentz", team: "PHI", position: "QB"),
                PlayerData(num: 7, name: "Jared Goff", team: "LAR", position: "QB"),
                PlayerData(num: 8, name: "Cam Newton", team: "CAR", position: "QB"),
                PlayerData(num: 9, name: "Andrew Luck", team: "IND", position: "QB"),
                PlayerData(num: 10, name: "Drew Brees", team: "NO", position: "QB"),
                PlayerData(num: 11, name: "Ben Roethlisberger", team: "PIT", position: "QB"),
                PlayerData(num: 12, name: "Dak Prescott", team: "DAL", position: "QB"),
                PlayerData(num: 13, name: "Russell Wilson", team: "SEA", position: "QB"),
                PlayerData(num: 14, name: "Kyler Murray", team: "ARI", position: "QB"),
                PlayerData(num: 15, name: "Tom Brady", team: "NE", position: "QB"),
                PlayerData(num: 16, name: "Lamar Jackson", team: "BAL", position: "QB"),
                PlayerData(num: 17, name: "Mitchell Trubisky", team: "CHI", position: "QB"),
                PlayerData(num: 18, name: "Jameis Winston", team: "TB", position: "QB"),
                PlayerData(num: 19, name: "Philip Rivers", team: "LAC", position: "QB"),
                PlayerData(num: 20, name: "Kirk Cousins", team: "MIN", position: "QB")
                ]
            }
        }
    }

myRoster view controller

import UIKit

class MyTeam_2019_2020: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        myRoster = Database.shared.rosterList()
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myRoster.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let player = myRoster[indexPath.row]
        let str = "\(player.name),  \(player.team) - \(player.position)"
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = "\(indexPath.row+1).  \([str])"
        cell.textLabel?.adjustsFontSizeToFitWidth = true
        cell.textLabel?.font = UIFont.systemFont(ofSize: 22)
        return cell
    }
    
    func save() {
        let defaults = UserDefaults.standard
        defaults.set(myRoster, forKey: "saveMyRoster")
        defaults.set(draftOrder, forKey: "saveDraftOrder")
    }
    
    @IBOutlet weak var myTeam_20192020: UILabel!
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}


Top_30_Quartebacks view controller

import UIKit

@available(iOS 11.0, *)

class Top_30_Quarterbacks: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return objectsArray.count
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        objectsArray = Database.shared.playerList(position: "QB")
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let num = objectsArray[indexPath.row].num
        let name = objectsArray[indexPath.row].name
        let team = objectsArray[indexPath.row].team
        cell.textLabel?.text = "\(num).  \(name),  \(team)"
        cell.textLabel?.adjustsFontSizeToFitWidth = true
        cell.textLabel?.font = UIFont.systemFont(ofSize: 22)
        
        if objectsArray[indexPath.row].strikeThrough == false && objectsArray[indexPath.row].accessory == false && objectsArray[indexPath.row].color == false {
            cell.textLabel?.text = "\(num).  \(name),  \(team)"
            cell.textLabel?.attributedText = noStrikeThroughText("\(num).  \(name),  \(team)")
            cell.accessoryType = UITableViewCell.AccessoryType.none
            cell.backgroundColor = .none
        }
        else if objectsArray[indexPath.row].strikeThrough == true && objectsArray[indexPath.row].accessory == true && objectsArray[indexPath.row].color == true {
            cell.textLabel?.text = "\(num).  \(name),  \(team)"
            cell.textLabel?.attributedText = strikeThroughText("\(num).  \(name),  \(team)")
            cell.accessoryType = UITableViewCell.AccessoryType.checkmark
            cell.backgroundColor = .systemGray3
        } else {
            cell.textLabel?.text = "\(num).  \(name),  \(team)"
            cell.textLabel?.attributedText = strikeThroughText("\(num).  \(name),  \(team)")
            cell.accessoryType = UITableViewCell.AccessoryType.none
            cell.backgroundColor = .systemGray2
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let cell = self.top30QuarterbacksTable.cellForRow(at: indexPath)
        let str: String = (cell?.textLabel!.text)!
        let player = objectsArray[indexPath.row].name
        
        if cell?.textLabel?.attributedText == strikeThroughText(str) {
            strikeThroughTextBool = true
        } else {
            strikeThroughTextBool = false
        }
        if strikeThroughTextBool == false {
            let add = UIContextualAction(style: .normal, title: "Add") { (contextualAction, view, actionPerformed: @escaping (Bool) -> Void) in
                let alert = UIAlertController(title: "Add Player", message: "Are you sure you want to add '\(player)' to your roster?", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { (alertAction) in
                    actionPerformed(false)
                }))
                alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: { (alertAction) in
                    self.addPlayer(index: indexPath)
                    let okayAlert = UIAlertController(title: "Player Added!", message: "You added '\(player)' to your roster?", preferredStyle: .alert)
                    okayAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (alertAction) in
                        actionPerformed(true)
                    }))
                    self.present(okayAlert, animated: true)
                }))
                self.present(alert, animated: true)
            }
            add.backgroundColor = .systemGreen
            let taken = UIContextualAction(style: .normal, title: "Taken") { (contextualAction, view, actionPerformed: @escaping (Bool) -> Void) in
                let alert = UIAlertController(title: "Player Taken", message: "Are you sure you want to mark '\(player)' as taken and not available?", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { (alertAction) in
                    actionPerformed(false)
                }))
                alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: { (alertAction) in
                    self.markAsTaken(index: indexPath)
                    let okayAlert = UIAlertController(title: "Taken Player", message: "You marked '\(player)' as taken and not available!", preferredStyle: .alert)
                    okayAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (alertAction) in
                        actionPerformed(true)
                    }))
                    self.present(okayAlert, animated: true)
                }))
                self.present(alert, animated: true)
            }
            taken.backgroundColor = .systemRed
            let config = UISwipeActionsConfiguration(actions: [taken, add])
            config.performsFirstActionWithFullSwipe = false
            return config
        } else {
            let undo = UIContextualAction(style: .normal, title: "Undo") { (contextualAction, view, actionPerformed: @escaping (Bool) -> Void) in
                let alert = UIAlertController(title: "Undo", message: "Are you sure you want to undo the action for '\(player)'?", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { (alertAction) in
                    actionPerformed(false)
                }))
                alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: { (alertAction) in
                    self.removePlayer(index: indexPath)
                    let okayAlert = UIAlertController(title: "Action Undone", message: "The previous action for '\(player)' has been undone!", preferredStyle: .alert)
                    okayAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (alertAction) in
                        actionPerformed(true)
                    }))
                    self.present(okayAlert, animated: true)
                }))
                self.present(alert, animated: true)
            }
            undo.backgroundColor = .systemBlue
            let config = UISwipeActionsConfiguration(actions: [undo])
            config.performsFirstActionWithFullSwipe = false
            return config
        }
    }
    
    func addPlayer(index: IndexPath) {
        let player = objectsArray[index.row]
        
        player.strikeThrough = true
        player.accessory = true
        player.color = true
        
        Database.shared.addToRosterList(player: player)
        Database.shared.addToDraftList(player: player)
        
        top30QuarterbacksTable.reloadData()
        Database.shared.changeMade()
    }
    
    func markAsTaken(index: IndexPath) {
        let player = objectsArray[index.row]
        
        player.strikeThrough = true
        player.color = true
        player.accessory = false
        
        top30QuarterbacksTable.reloadData()
        Database.shared.changeMade()
    }
    
    func removePlayer(index: IndexPath) {
        let player = objectsArray[index.row]
        
        player.strikeThrough = false
        player.color = false
        player.accessory = false
        
        top30QuarterbacksTable.reloadData()
        Database.shared.changeMade()
    }
        
    @IBOutlet weak var top30QuarterbacksTable: UITableView!
    @IBOutlet var view1: UIView!
    @IBOutlet weak var view2: UIView!
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Accepted Reply

hi,


the problem now is that there are too many themes in your code, some from your initial efforts, some from Claude31, and several from me, that just don't knit too well together.


rather than giving you a long reply here in this forum (i really do hate code boxes and i prefer that posts not get too long), i've mocked up an app that contains all the elements you need, it manages everything that needs to be persisted in a Database singleton (essentailly a global variable), it unifies and simplifies some of the code that's been patched together these last few weeks, and it does exactly what you want (as best i can tell).


you can find the project at:


bitbucket.org/delawaremathguy/japio


hope that helps,

DMG

Replies

I do not see where you update allPlayers when you addPlayer. You update objectsArray, but this does not seem to propagate to allPlayers.


So, you could pass objectsArray as parapeter to

    func changeMade(for forPlayers: [PlayerData]) {
       allPlayers = forPlayers
        playersWriteToDisk()
    }

And of course call:

        Database.shared.changeMade(for: objectsArray)


If DMG has a look at it, he will certainly provide very useful advices.

hi,


the problem now is that there are too many themes in your code, some from your initial efforts, some from Claude31, and several from me, that just don't knit too well together.


rather than giving you a long reply here in this forum (i really do hate code boxes and i prefer that posts not get too long), i've mocked up an app that contains all the elements you need, it manages everything that needs to be persisted in a Database singleton (essentailly a global variable), it unifies and simplifies some of the code that's been patched together these last few weeks, and it does exactly what you want (as best i can tell).


you can find the project at:


bitbucket.org/delawaremathguy/japio


hope that helps,

DMG

hi Claude31,


just a quick comment on enjoying your help on this app with japio. we've suggested many tools for japio to use, but japio is still having trouble putting them all together.


i've posted a working project as a solution, hope you'll take a look.


and to your point above, changeMade() should work fine without a parameter, since the Database owns the playerData (everyone else just gets references, so changes made elsewhere are changes to the real data already owned in the Database).


regards,

DMG

What an effort !


I played with it also, but got the error:

2020-01-01 18:19:44.256392+0100 Japio[41689:35910114] [Storyboard] Unknown class _TtC5Japio14ViewController in Interface Builder file.


Seems that the first view in navigation is referenced as ViewController, which does not exist.

I removed it to get a plain UIViewController and everything worked OK.

thanks for the pickup! (EDIT for Claude31: i also hate GIT, and it's always the last edit that doesn't get pushed right, and then you have source tree conflicts. i have deleted the old repository and replaced it by one that should run out of the box as of 1:57 PM EDT on Jan 1, 2020. sorry for any confusion.)


DMG

Hello, The problem actually was that I wasn't calling the data from the database.


I have 2 buttons in my main view, one is for loading a table with myRoster and one is for loading a table with draftOrder.


I didn't ever show the code with these two buttons but I just changed the code to pull data from the database:


@IBAction func draftOrder_2019_2020(_ sender: UIButton) {
        
        if Database.shared.draftList().isEmpty {
            performSegue(withIdentifier: "no draft", sender: self)
        } else {
            performSegue(withIdentifier: "draft order 2019/2020", sender: self)
        }
    }


It works perfect now. The only thing I need to do is to slow down the animation and make it smooth when reloading the tables after I swipe and tap the add button in Top_30_Quarterbacks view controller.


Thank you for your help.

Hello,


I don't have a first view in navigation referenced as ViewController.


I did find the problem though.


The problem actually was that I wasn't calling the data from the database.


I have 2 buttons in my main view, one is for loading a table with myRoster and one is for loading a table with draftOrder.


I didn't ever show the code with these two buttons but I just changed the code to pull data from the database:


The problem actually was that I wasn't calling the data from the database.

I have 2 buttons in my main view, one is for loading a table with myRoster and one is for loading a table with draftOrder.

I didn't ever show the code with these two buttons but I just changed the code to pull data from the database:


Thank you for your help.