How do I save a global array to a table view so that after I close the app, when I reopen it, the data isn't lost?

How do I save a global array to a table view so that after I close the app, when I reopen it, the data isn't lost? In my app, I append to an array that is outside of the class. As long as I have the app open, I can switch views and return to the and the table view is still loaded with the array but after I close the app and return to the app the table view is empty.

Accepted Reply

So, in applicationWillTerminate, you should call

To save:

let encoder = JSONEncoder()
if let encoded = try? encoder.encode(myRoster!) {
  let defaults = UserDefaults.standard
  defaults.set(encoded, forKey: "Roster")
}
if let encoded = try? encoder.encode(draftOrder!) {
  let defaults = UserDefaults.standard
  defaults.set(encoded, forKey: "Order")
}



Then, when you reopen, in appDidfinishLaunching

if let savedRosters = defaults.object(forKey: "Roster") as? Data {
  let decoder = JSONDecoder()
  if let loadedRosters = try? decoder.decode([String].self, from: savedRosters) {
       myRoster = loadedRosters
  }
}

if let savedOrders = defaults.object(forKey: "Order") as? Data {
  let decoder = JSONDecoder()
  if let loadedOrders = try? decoder.decode([String].self, from: savedOrders) {
       draftOrder = loadedOrders
  }
}


Somewhere in code you did initilaize those arrays with initial values.

You should not overwrite what you have loaded from defaults.

So, add a test

if myRoster == [] {
     initialize with initial values
}
// otherwise you keep what you have read from userDefaults.


One more point.

You declare tour arrays as optional, but immediately give them a value

     var myRoster: [String]? = []


So, unless there are some places where you set them to nil, that's useless.

You could just declare

     var myRoster: [String] = []


The logic here is:

either you declare as optional, because you want to be able to test for nil somewhere

     var myRoster: [String]?

and initialize later


or you set a value

     var myRoster: [String] = []

Replies

As stated in another thread you opened, you can save data to a permanent storage:

- userDefaults (if small amount)

- a plist

- a file

- use CoreData


Then, fetch data from this storage when needed.


What is it exactly you have problem with ?

What do you mean exactly:

How do I save a global array to a table view

you do not save to tableView ! You may load into tableView, but not save.


I suppose your array is objectsArray. Exact ?

As it is Codable, that's straightforward. See:

h ttps://www.hackingwithswift.com/example-code/system/how-to-load-and-save-a-struct-in-userdefaults-using-codable


I assume you declared:

var objectsArray = [PlayerData]()     // [PlayerDaya] is also Codable

To save:

let encoder = JSONEncoder()
if let encoded = try? encoder.encode(objectsArray) {
  let defaults = UserDefaults.standard
  defaults.set(encoded, forKey: "SavedPlayers")
}

To load back:


if let SavedPlayers = defaults.object(forKey: "SavedPlayers") as? Data {
  let decoder = JSONDecoder()
  if let loadedPlayers = try? decoder.decode([PlayerData].self, from: SavedPlayers) {
       objectsArray = loadedPlayers
  }
}


Note: I did not test, hope there is no typo.

I have 2 arrays declared outside of a class called myRoster and draftOrder.


import Foundation
import UIKit

var myRoster: [String]? = []

var draftOrder: [String]? = []


In other classes such as Top_30_Quarterbacks, when a player is added, I append to myRoster array and draftOrder array. When a player is taken, I append to draftOrder array.


func addPlayer(index: IndexPath) {
        _ = top30QuarterbacksTable.cellForRow(at: index)
        let name: String = objectsArray[index.row].name
        let team: String = objectsArray[index.row].team
        let position: String = objectsArray[index.row].position
        
        objectsArray[index.row].strikeThrough = true
        objectsArray[index.row].accessory = true
        objectsArray[index.row].color = true
        
        if myRoster == [] {
            myRoster?.insert("\(name),  \(team) - \(position)", at: 0)
        } else {
            myRoster?.append("\(name),  \(team) - \(position)")
        }
        
        if draftOrder == [] {
            draftOrder?.insert("\(name),  \(team) - \(position)", at: 0)
        } else {
            draftOrder?.append("\(name),  \(team) - \(position)")
        }
        top30QuarterbacksTable.reloadData()
        Database.shared.changeMade()
    }
    
    func markAsTaken(index: IndexPath) {
        _ = top30QuarterbacksTable.cellForRow(at: index)
        let name: String = (objectsArray[index.row].name)
        let team: String = (objectsArray[index.row].team)
        let position: String = (objectsArray[index.row].position)
        
        objectsArray[index.row].strikeThrough = true
        objectsArray[index.row].color = true
        objectsArray[index.row].accessory = false
            
            if draftOrder == [] {
                draftOrder?.insert("\(name),  \(team) - \(position)", at: 0)
            } else {
                draftOrder?.append("\(name),  \(team) - \(position)")
            }
        top30QuarterbacksTable.reloadData()
        Database.shared.changeMade()
    }


When I switch views, the arrays stay populated with the data but when I close the app and reopen it, the data is lost. I tried UserDefaults but it doesn't work.

So, in applicationWillTerminate, you should call

To save:

let encoder = JSONEncoder()
if let encoded = try? encoder.encode(myRoster!) {
  let defaults = UserDefaults.standard
  defaults.set(encoded, forKey: "Roster")
}
if let encoded = try? encoder.encode(draftOrder!) {
  let defaults = UserDefaults.standard
  defaults.set(encoded, forKey: "Order")
}



Then, when you reopen, in appDidfinishLaunching

if let savedRosters = defaults.object(forKey: "Roster") as? Data {
  let decoder = JSONDecoder()
  if let loadedRosters = try? decoder.decode([String].self, from: savedRosters) {
       myRoster = loadedRosters
  }
}

if let savedOrders = defaults.object(forKey: "Order") as? Data {
  let decoder = JSONDecoder()
  if let loadedOrders = try? decoder.decode([String].self, from: savedOrders) {
       draftOrder = loadedOrders
  }
}


Somewhere in code you did initilaize those arrays with initial values.

You should not overwrite what you have loaded from defaults.

So, add a test

if myRoster == [] {
     initialize with initial values
}
// otherwise you keep what you have read from userDefaults.


One more point.

You declare tour arrays as optional, but immediately give them a value

     var myRoster: [String]? = []


So, unless there are some places where you set them to nil, that's useless.

You could just declare

     var myRoster: [String] = []


The logic here is:

either you declare as optional, because you want to be able to test for nil somewhere

     var myRoster: [String]?

and initialize later


or you set a value

     var myRoster: [String] = []

Ok. What class do I add applicationWillTerminate and appDidFinishLaunching to? Thank you

hi,


just getting caught up on this ... took a few days off ...


japio: it's clear that when you closed out the last thread, you were satisfied with the Database singleton and that you wanted to persist the list of players. it's no surprise now to find out that things don't work because you never asked to persist myRoster and draftOrder, nor did the Database ever handle these.


Claude31's suggestions should work fine, and if you're satisfied with them, that's cool with me. and whether you persist data to a file in the Documents directory or as data in UserDefaults, it probably does not matter for the size of this app.


however, with all due respect to my friend Claude31, i think you really want to move to a file-based model managed by the Database singleton, and eventually begin to use a UIDocument model to allow auto-saving.


so i would suggest all of what appears below. i'm sorry for the length, but the specificity really is necessary, and i'd encourage you to read through all of it and, if it makes sense, then consider using it in the future.


(1) let the Database singleton manage all data centrally, rather than rely on hooks in the Application Delegate and adding portions of the data management throughout your code to save other lists.


(2) instead of keeping three separate lists, one of type PlayerData and two of type String, simplify by making all lists of type PlayerData (and let whatever view controllers access those lists decide how they want to display them, using the name, the team, and the position).


(3) keep all the internals of how the lists are managed within the Database singleton; and to accomplish this, i'd propose extending the definition of PlayerData to include two Int values, one perhaps named "rosterPosition" to indicate the position at which a player appears in the myRoster list, and one perhaps named "draftPosition" to indicate the position at which a player appears in the draftOrder list. use -1 as a value to indicate that a player does not appear in a list.


in short, there will be only one list of players ⚠ all the draft and roster information will be incorporated into the PlayerData records directly in this single list.


(4) expand the Database singleton to have methods to provide myRoster and draftOrder as needed in your program with


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


(5) expand the Database singleton to allow moving a player onto one of these lists (which, from your code, always seems to be an "appending"). this requires a quick computation to figure out where in the list to place an addition:


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
}


(6) your addPlayer() function then simplifies:


func addPlayer(index: IndexPath) {
  // remove this line: _ = top30QuarterbacksTable.cellForRow(at: index)
  let player = objectsArray[index.row]
  // remove this line: let name: String = objectsArray[index.row].name
  // remove this line: let team: String = objectsArray[index.row].team
  // remove this line: let position: String = objectsArray[index.row].position

  player.strikeThrough = true
  player.accessory = true
  player.color = true

  Database.shared.addToRosterList(player: player)
  // remove this line: if myRoster == [] {
  // remove this line: myRoster?.insert("\(name),  \(team) - \(position)", at: 0)
  // remove this line: } else {
  // remove this line: myRoster?.append("\(name),  \(team) - \(position)")
  // remove this line: }

  Database.shared.addToDraftList(player: player)
  // remove this line: if draftOrder == [] {
  // remove this line: draftOrder?.insert("\(name),  \(team) - \(position)", at: 0)
  // remove this line: } else {
  // remove this line: draftOrder?.append("\(name),  \(team) - \(position)")
  // remove this line: }
  top30QuarterbacksTable.reloadData()
  Database.shared.changeMade()
}


that does it in principle, except that you'll need to understand the implications:


(a) (EDIT: you'll need to update the PlayerData class definition)


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 // not on the roster
  var draftPosition: Int = -1  // not drafted

  init(num: Int, name: String, team: String, position: String) {
  self.num = num
  self.name = name
  self.team = team
  self.position = position
  }
}


(b) in any view controller where you display the roster and draft order, you'll start with


override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated: animated)
  myRoster = Database.shared.rosterList()
  draftOrder = Database.shared.draftList()
}


(c) in such a viewController, you would need to build out whatever string you want to appear in cellForRow(at:) separately, because myRoster and draftOrder will now be lists of PlayerData and not lists of String. for example, it's no longer


let str = myRoster[index]


but instead


let player = myRoster[index]
let str = "\(player.name),  \(player.team) - \(player.position)"


(d) finally, if you make all these changes and adopt this program, you should expect to run into errors when you run your app, because we have changed the definition of PlayerData. it's now incompatible with what is stored on disk and/or in UserDefaults. simply delete the app from the simulator or iOS device, then run again from XCode with a fresh copy of the app and no data yet persisted. you'll be back at the beginning.



that's a lot to digest. i hope it makes some sense to you, and it will greatly simplify a lot of your code.



hope that helps,

DMG

That's in AppDelegate.

You don't have to add. They should be there already.


You should have it with the following comment

// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.

I added a log to see it is called.


    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        print("I leave")
    }

hi,


in the previous message, i forgot that you occasionally delete a player from one of your lists. with the model i have suggested, you can do this for a given player by adding this method to Database to remove a player from the roster:


func removeFromRosterList(player: PlayerData) {
  let currentRosterPosition = player.rosterPosition
  player.rosterPosition = -1 // denotes no longer on the rosterList
  // get 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
  }
  }


you'd add a similar method to remove a player from your draft list.


hope that helps,

DMG

Sure, database is more future proof and powerful. More complex as well. You were courageaous enough to completely lay this out, congrats !


It's always the same trade off between user defaults, plain json file, CoreData or database.


Now Japio has all options in hands, he will choose what fits best. But whatever selected, I would advise him (or her ?) to keep a copy of thoses threads? That may prove useful for the future.

thanks for the comment, and I agree: it's up to japio to go from here.


Happy New Year, all!

DMG

I am going to try both ways. I would like to implement the database method but it is really complex. I will give it a shot.

May be you can start simple (userDefaults) which does not require a lot of changes.


When it works, save a copy of the full project folder. And close this thread.


Then start modifying code for database option.

You will probably open a new tyhread dedicated to this, to avoid extending this one too much.


You will learn a lot, but that will be hard. Hence, important to keep a project code that is OK)

Yes, the way you proposed in appDelegate does work. I am going to open up a new thread using database option.


Thank you for your help.