在 Swift 中使用 NSCoding 在分层 类 中进行数据持久化的最佳做法是什么?

What is best practice for data persistence in hierarchical classes using NSCoding in Swift?

问题:我已经使用 NSKeyedArchiver 在我的 iOS 应用程序中实现了数据持久化,它目前可以保存分层数据,但只能从顶层开始 class。我怎样才能从任何级别保存整个层次结构?

背景:我正在开发一个 iOS 应用程序,它使用分层 classes 的数据结构,例如学校,教室,学生。基本上,学校 class 包含一组教室(以及其他属性,如学区、名称、phone 编号等),教室 class 包含一组学生(以及其他属性)教师、房间号等属性)和学生 class 具有每个学生的属性(例如姓名、年级、课程等)。

该应用程序有三个视图控制器,一个用于层次结构的每个级别,允许在每个级别更改数据:DistrictTableViewController 有一个 School 对象数组,可以 add/delete 个数组元素,SchoolTableViewController 有一个Classroom 对象数组,并且可以 add/delete Classroom 对象数组中的元素,而 ClassroomViewController 允许用户 add/remove/edit 学生。

我已经使用 NSCoding 在所有三个 classes 中实现了数据持久性,它目前可以在层次结构中保存数据,但我只能保存来自顶级 DistrictTableVC(应用程序入口点)的数据. DistrictTableVC 有一个 saveSchools() 方法。相反,我希望能够保存来自三个 ViewController 中任何一个的更改,例如对 Student 属性 的更改将立即保存 Student 对象,以及教室中的学生数组和学校中的教室数组。

当前配置是 DistrictTableVC 将单个 School 对象传递给 SchoolTableVC,SchoolTableVC 将单个 Classroom 对象传递给 ClassroomVC。我认为我应该做的是:

  1. 创建一个名为 District 的新顶层 class,它包含学校数组并使用 NSCoding
  2. 在三个 VC 之间传递 District 对象而不是单一的较低级别对象
  3. 将 saveSchools() 方法从 DistrictTableVC 移至新 District class,允许我从三个 ViewController 中的任何一个调用它。

由于本人非专业,伸手看看:

  1. 我走在正确的轨道上吗?或
  2. 也许有人知道更好的方法?

感谢阅读!!

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

成功了!现在每个视图控制器都有一个区域对象,并且可以在数据模型修改时调用 district.saveDistrict() 。

class 地区: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)
}

}