Problem: I've implemented data persistence in my iOS app using NSKeyedArchiver and it currently functions to save hierarchical data, but only from the top level class. How can I make it possible to save the entire hierarchy from any level?
Background: I'm developing an iOS app that uses a data structure of hierarchical classes, e.g. School, Classroom, Student. Basically, the School class contains an array of Classrooms (along with other properties like district, name, phone number, etc.) the Classroom class contains an array of Students (along with other properties like teacher, room number, etc.) and the Student class has properties for each student (e.g. name, grade, courses, etc.).
The app has three view controllers, one for each level of the hierarchy that allows the data to be changed at each level: DistrictTableViewController has an array of School objects and can add/delete array elements, SchoolTableViewController has an array of Classroom objects and can add/delete elements from the array of Classroom objects, and ClassroomViewController allows the user to add/remove/edit Students.
I've implemented data persistence in all three classes using NSCoding and it currently functions to save data in the hierarchy, but I can only save the data from the top level DistrictTableVC (app entry point). DistrictTableVC has a saveSchools() method. Instead, I want to be able to save changes from any of the three ViewControllers, e.g. a change to a Student property would immediately save the Student object, as well as the array of Students in the Classroom and the array of Classrooms in the School.
The current configuration is such that the DistrictTableVC passes a single School object to the SchoolTableVC, SchoolTableVC passes a single Classroom object to ClassroomVC. I think what I should be doing instead is:
Since I am not a professional, I am reaching out to see:
Thanks for reading!!
class DistrictTableViewController: UITableViewController {
private let reuseIdentifier = "schoolCell"
var schoolsArray = [School]()
override func viewDidLoad() {
super.viewDidLoad()
self.navBarTitle.title = "Schools"
// Load saved Schools if they exist, otherwise load sample data
if let savedSchools = loadSchools() {
schoolsArray += savedSchools
print("Loading saved schools")
// Update all School stats
updateSchoolListStats()
} else {
// Load the sample data
loadSampleSchools()
print("Failed to load saved data. Loading sample data...")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: TableView datasource
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return schoolsArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! SchoolTableViewCell
// Configure the cell...
let school = schoolsArray[indexPath.row]
school.calcSchoolStats()
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
schoolsArray.remove(at: indexPath.row)
saveSchools()
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
// Deselect any selected cells
for (_, cell) in tableView.visibleCells.enumerated() {
cell.isSelected = false
}
// SchoolTableViewCell pressed: pass the selected school to SchoolsTableViewController
if (segue.identifier ?? "") == "showSchoolDetail" {
//guard let schoolsTableViewController = segue.destination as? SchoolsTableViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
guard let selectedSchoolCell = sender as? SchoolTableViewCell else {
fatalError("Unexpected sender: \(String(describing: sender))")
}
guard let indexPath = tableView.indexPath(for: selectedSchoolCell) else {
fatalError("The selected SchoolTableViewCell is not being displayed by the table")
}
schoolTableViewController.school = schoolsArray[indexPath.row]
}
// Add button pressed: show SchoolAttributesViewController
if addBarButtonItem == sender as? UIBarButtonItem {
guard segue.destination is SchoolAttributesViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
}
}
@IBAction func unwindToSessionsTableViewController(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? SchoolsTableViewController, let school = sourceViewController.school {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing session
schoolsArray.array[selectedIndexPath.row] = school
tableView.reloadRows(at: [selectedIndexPath], with: .none)
} else {
// Add a new school to the Table View
schoolsArray.insert(session, at: 0) // Update date source; add new school to the top of the table
let newIndexPath = IndexPath(row: 0, section: 0)
tableView.insertRows(at: [newIndexPath], with: .automatic)
tableView.cellForRow(at: newIndexPath)?.isSelected = true
tableView.cellForRow(at: newIndexPath)?.selectedBackgroundView = bgColorView
}
//updateSessionListStats()
//sessionsTableView.reloadData()
saveSchools()
}
}
//MARK: Actions
private func saveSchools() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(schoolsArray, toFile: School.ArchiveURL.path)
if isSuccessfulSave {
os_log("Schools successfully saved", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save schools...", log: OSLog.default, type: .error)
}
}
//MARK: Private Methods
private func updateSchoolListStats() {
for (_, school) in schoolsArray.array.enumerated() {
for (_, classroom) in school.classroomArray.enumerated() {
classroom.calcStats()
}
school.calcSchoolStats()
}
}
private func loadSchools() -> [School]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: School.ArchiveURL.path) as? [School]
}
class School: NSObject, NSCoding {
//MARK: Properties
var name: String
var district: String
var phoneNumber: Int
var classroomArray = [Classroom]()
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("schoolsArray")
init (name: String = "Default", district: String = "", phoneNumber: Int = -1, classroomArray = [Classroom]()) {
self.name = name
self.district = district
self.phoneNumber = phoneNumber
self.classroomArray = classroomArray
}
func calcSchoolStats() {
}
//MARK: NSCoding Protocol
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(district, forKey: "district")
aCoder.encode(phoneNumber, forKey: "phoneNumber")
aCoder.encode(classroomArray, forKey: "classroomArray")
}
required convenience init?(coder aDecoder: NSCoder) {
// The name is required. If we cannot decode a name string, the initializer should fail.
guard let name = aDecoder.decodeObject(forKey: "name") as? String else {
os_log("Unable to decode the name for a School object.", log: OSLog.default, type: .debug)
return nil
}
let district = aDecoder.decodeObject(forKey: "district") as! String
let phoneNumber = aDecoder.decodeInteger(forKey: "phoneNumber")
let classroomArray = aDecoder.decodeObject(forKey: "classroomArray") as! [Classroom]
// Must call designated initializer.
self.init(name: name, district: district, phoneNumber: phoneNumber, classroomArray: classroomArray)
}
}
class Classroom: NSObject, NSCoding {
//MARK: Properties
var teacher: String
var roomNumber: Int
var studentArray = [Student]()
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("classroomsArray")
init (teacher: String = "", building: Int = -1, studentArray = [Student]()) {
self.teacher = teacher
self.roomNumber = roomNumber
self.studentArray = studentArray
}
func calcStats() {
}
//MARK: NSCoding Protocol
func encode(with aCoder: NSCoder) {
aCoder.encode(teacher, forKey: "teacher")
aCoder.encode(roomNumber, forKey: "roomNumber")
aCoder.encode(studentArray, forKey: "studentArray")
}
required convenience init?(coder aDecoder: NSCoder) {
// The teacher is required. If we cannot decode a teacher string, the initializer should fail.
guard let teacher = aDecoder.decodeObject(forKey: "teacher") as? String else {
os_log("Unable to decode the teacher for a Classroom object.", log: OSLog.default, type: .debug)
return nil
}
let roomNumber = aDecoder.decodeInteger(forKey: "roomNumber")
let studentArray = aDecoder.decodeObject(forKey: "studentArray") as! [Student]
// Must call designated initializer.
self.init(teacher: teacher, roomNumber: roomNumber, studentArray: studentArray)
}
}
class Student: NSObject, NSCoding {
//MARK: Properties
var first: String
var last: String
var grade: Int
var courses: [String]
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("students")
init (first: String = "", last: String = "", grade: Int = -1, courses = [String]()) {
self.first = first
self.last = last
self.grade = grade
self.courses = courses
}
//MARK: NSCoding Protocol
func encode(with aCoder: NSCoder) {
aCoder.encode(first, forKey: "first")
aCoder.encode(last, forKey: "last")
aCoder.encode(grade, forKey: "grade")
aCoder.encode(courses, forKey: "courses")
}
required convenience init?(coder aDecoder: NSCoder) {
// The first name is required. If we cannot decode a first name string, the initializer should fail.
guard let first = aDecoder.decodeObject(forKey: "first") as? String else {
os_log("Unable to decode the first name for a Student object.", log: OSLog.default, type: .debug)
return nil
}
let last = aDecoder.decodeObject(forKey: "last") as! String
let grade = aDecoder.decodeInteger(forKey: "grade")
let courses = aDecoder.decodeObject(forKey: "courses") as! [String]
// Must call designated initializer.
self.init(first: first, last: last, grade: grade, courses: courses)
}
}
Got it working! Now each view controller has a district object and can call district.saveDistrict() whenever the data model is modified.
class District: NSObject, NSCoding {
//MARK: Properties
var array: [School]
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("District")
init (array: [School] = [School]()) {
self.array = array
}
//MARK: Actions
func saveDistrict() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(array, toFile: District.ArchiveURL.path)
if isSuccessfulSave {
os_log("Schools array successfully saved", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save schools array...", log: OSLog.default, type: .error)
}
}
func loadSavedDistrict() -> District? {
var savedDistrict = District()
if let districtConst = NSKeyedUnarchiver.unarchiveObject(withFile: District.ArchiveURL.path) as? [School] {
savedDistrict = District(array: districtConst)
}
return savedDistrict
}
//MARK: NSCoding Protocol
func encode(with aCoder: NSCoder) {
aCoder.encode(array, forKey: "array")
}
required convenience init?(coder aDecoder: NSCoder) {
// The array is required. If we cannot decode the array, the initializer should fail.
guard let array = aDecoder.decodeObject(forKey: "array") as? [School] else {
os_log("Unable to decode the Schools array object.", log: OSLog.default, type: .debug)
return nil
}
// Must call designated initializer.
self.init(array: array)
}
}