Какова наилучшая практика сохранения данных в иерархических классах с использованием NSCoding в Swift?

Проблема: я реализовал сохраняемость данных в своем приложении для iOS с помощью NSKeyedArchiver, и в настоящее время оно сохраняет иерархические данные, но только из класса верхнего уровня. Как я могу сделать возможным сохранение всей иерархии с любого уровня?

Предыстория. Я разрабатываю приложение для iOS, в котором используется структура данных иерархических классов, например. Школа, Класс, Студент. По сути, класс School содержит массив Classrooms (вместе с другими свойствами, такими как район, имя, номер телефона и т. д.), класс Classroom содержит массив Student (вместе с другими свойствами, такими как учитель, номер комнаты и т. д.) и Студенческий класс имеет свойства для каждого учащегося (например, имя, класс, курсы и т. д.).

Приложение имеет три контроллера представления, по одному на каждый уровень иерархии, что позволяет изменять данные на каждом уровне: DistrictTableViewController имеет массив объектов School и может добавлять/удалять элементы массива, SchoolTableViewController имеет массив объектов Classroom и может добавлять /delete элементы из массива объектов Classroom, а ClassroomViewController позволяет пользователю добавлять/удалять/редактировать учащихся.

Я реализовал сохраняемость данных во всех трех классах с помощью NSCoding, и в настоящее время он работает для сохранения данных в иерархии, но я могу сохранить данные только из DistrictTableVC верхнего уровня (точка входа в приложение). DistrictTableVC имеет метод saveSchools(). Вместо этого я хочу иметь возможность сохранять изменения из любого из трех ViewControllers, например. изменение свойства Student немедленно сохранит объект Student, а также массив Student в Classroom и массив Classrooms в School.

Текущая конфигурация такова, что DistrictTableVC передает один объект School в SchoolTableVC, SchoolTableVC передает один объект Classroom в ClassroomVC. Я думаю, что мне следует делать вместо этого:

  1. создайте новый класс верхнего уровня под названием District, который содержит массив школ, а также использует NSCoding
  2. передавать объект District между тремя VC вместо отдельных объектов более низкого уровня
  3. переместите метод saveSchools() из DistrictTableVC в новый класс District, что позволит мне вызывать его из любого из трех 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)
    }
}

person benlydmb    schedule 08.09.2019    source источник
comment
Если вы хотите сохранить данные на локальном устройстве, вы можете рассмотреть возможность использования CoreData для сохранения ваших объектов. Еще один способ, который вы можете использовать, — использовать Firebase для удаленного хранения данных.   -  person Adrian    schedule 09.09.2019
comment
Если вы сериализуете (кодируете) свой объект верхнего уровня (массив районов), вы не можете загружать/сохранять вложенные массивы школы/класса/учащегося отдельно. Вам всегда нужно загружать/сохранять полный массив верхнего уровня. Для вашей проблемы может быть более желательным другой подход. @Adrian упомянул CoreData или Firebase. Спросите себя: сколько сущностей (разных типов) приходится на одного пользователя приложения (районы/школы/классы/учащиеся). Если общее число превышает пару сотен, вам определенно следует использовать CoreData или стороннее решение для базы данных.   -  person Lutz    schedule 09.09.2019
comment
Спасибо за ответы! Я, вероятно, должен был упомянуть в своем исходном посте, что я пытаюсь избежать использования CoreData. Опять же, я не профессионал, но из того, что я видел в своих исследованиях (nshipster.com/nscoding) (raywenderlich. com/), настройка CoreData требует дополнительных усилий. Однако я посмотрю на Firebase ... Я подумал, что удаленное хранилище данных было бы неплохо иметь в будущей версии. Чтобы ответить на вопрос Лутца, это очень мало, наверное, меньше 100. Еще раз спасибо!   -  person benlydmb    schedule 10.09.2019


Ответы (1)


Получил работу! Теперь каждый контроллер представления имеет объект района и может вызывать район.saveDistrict() при каждом изменении модели данных.

район класса: 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)
}

}

person benlydmb    schedule 11.09.2019