Ios 在Swift中使用NSCoding在分层类中实现数据持久化的最佳实践是什么?
问题:我已经在iOS应用程序中使用NSKeyedArchiver实现了数据持久化,目前它的功能是保存分层数据,但只保存顶级类的数据。如何从任何级别保存整个层次结构 背景:我正在开发一个iOS应用程序,它使用分层类的数据结构,例如学校、教室、学生。基本上,学校类包含教室数组(以及其他属性,如学区、姓名、电话号码等)。教室类包含学生数组(以及其他属性,如教师、房间号等),学生类具有每个学生的属性(例如姓名、年级、课程等) 该应用程序有三个视图控制器,层次结构的每一层各有一个,允许在每一层更改数据:DistrictTableViewController有一个学校对象数组,可以添加/删除数组元素;SchoolTableViewController有一个教室对象数组,可以从教室对象数组中添加/删除元素,ClassroomViewController允许用户添加/删除/编辑学生 我已经使用NSCoding在所有三个类中实现了数据持久化,目前它的功能是在层次结构中保存数据,但我只能从顶级DistrictTableVC(应用程序入口点)保存数据。DistrictTableVC有一个saveSchools()方法。相反,我希望能够保存来自三个ViewController中任何一个的更改,例如,对学生属性的更改将立即保存学生对象,以及教室中的学生数组和学校中的教室数组 当前的配置是DistrictTableVC将单个学校对象传递给SchoolTableVC,SchoolTableVC将单个教室对象传递给ClassroomVC。我想我应该做的是:Ios 在Swift中使用NSCoding在分层类中实现数据持久化的最佳实践是什么?,ios,swift,hierarchical-data,nscoding,data-persistence,Ios,Swift,Hierarchical Data,Nscoding,Data Persistence,问题:我已经在iOS应用程序中使用NSKeyedArchiver实现了数据持久化,目前它的功能是保存分层数据,但只保存顶级类的数据。如何从任何级别保存整个层次结构 背景:我正在开发一个iOS应用程序,它使用分层类的数据结构,例如学校、教室、学生。基本上,学校类包含教室数组(以及其他属性,如学区、姓名、电话号码等)。教室类包含学生数组(以及其他属性,如教师、房间号等),学生类具有每个学生的属性(例如姓名、年级、课程等) 该应用程序有三个视图控制器,层次结构的每一层各有一个,允许在每一层更改数据:D
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对象,可以在修改数据模型时调用district.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)
}
}让它工作起来了!现在,每个视图控制器都有一个district对象,可以在修改数据模型时调用district.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)
}
}/P>如果你想在本地设备上坚持,你可以考虑使用CordaDATA来保存你的对象。您可以采取的另一种策略是利用Firebase远程存储数据。如果您序列化(编码)顶级对象(区域数组),则无法单独加载/保存嵌套的学校/教室/学生数组。始终必须加载/保存完整的顶级阵列。对于您的问题,另一种方法可能更可取@Adrian提到了CoreData或Firebase。问问自己:应用程序的一个用户(地区/学校/教室/学生)总共有多少个实体(不同类型)。如果总数超过几百个,您肯定应该使用CoreData或第三方数据库解决方案。感谢您的回复!我可能应该在我的原始帖子中提到,我正在努力避免使用CoreData。再说一次,我不是专业人士,但从我在研究()中看到的情况来看,CoreData需要进行更多的设置工作。不过我会调查Firebase。。。我想远程数据存储在未来的版本中会很好。为了回答卢茨的问题,它非常小,可能不到100。再次感谢!如果你想坚持本地设备,你可以考虑使用CordaDATA来保存你的对象。您可以采取的另一种策略是利用Firebase远程存储数据。如果您序列化(编码)顶级对象(区域数组),则无法单独加载/保存嵌套的学校/教室/学生数组。始终必须加载/保存完整的顶级阵列。对于您的问题,另一种方法可能更可取@Adrian提到了CoreData或Firebase。问问自己:应用程序的一个用户(地区/学校/教室/学生)总共有多少个实体(不同类型)。如果总数超过几百个,您肯定应该使用CoreData或第三方数据库解决方案。感谢您的回复!我可能应该在我的原始帖子中提到,我正在努力避免使用CoreData。再说一次,我不是专业人士,但从我在研究()中看到的情况来看,CoreData需要进行更多的设置工作。不过我会调查Firebase。。。我想远程数据存储在未来的版本中会很好。为了回答卢茨的问题,它非常小,可能不到100。再次感谢!