Core Data by tutorials 笔记(八)

最后更新于:2022-04-01 02:41:05

> 原文出处:http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-ba 今天来学习一下多个context的情况,特别是在多线程环境下。第十章也是本书的最后一章,如果你对core data的其他内容感兴趣,可以去翻看之前的笔记,或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials) ## **Chapter 10: Multiple Managed Object Contexts** 作者一开始介绍了几种使用多个context的情形,比如会阻塞UI的的任务,最好还是在后台线程单独使用一个context,和主线程context分开。还有处理临时编辑的数据时,使用一个child context也会很有帮助。 ### **一、Getting started** 本章提供了一个冲浪评分的APP作为Start Project,你可以添加冲浪地点的评价,还可以将所有记录导出为CSV文件。 与之前章节不同的是,这个APP的初始数据存放在**app bundle**中,我们看看在Core Data stack中如何获取: ~~~ // 1 找到并创建一个URL引用 let seededDatabaseURL = bundle .URLForResource("SurfJournalDatabase", withExtension: "sqlite") // 2 尝试拷贝seeded database文件到document目录,只会拷贝一次,存在就会失败。 var fileManagerError:NSError? = nil let didCopyDatabase = NSFileManager.defaultManager() .copyItemAtURL(seededDatabaseURL!, toURL: storeURL, error: &fileManagerError) // 3 只有拷贝成功才会运行下面方法 if didCopyDatabase { // 4 拷贝smh(shared memory file) fileManagerError = nil let seededSHMURL = bundle .URLForResource("SurfJournalDatabase", withExtension: "sqlite-shm") let shmURL = documentsURL.URLByAppendingPathComponent( "SurfJournalDatabase.sqlite-shm") let didCopySHM = NSFileManager.defaultManager() .copyItemAtURL(seededSHMURL!, toURL: shmURL, error: &fileManagerError) if !didCopySHM { println("Error seeding Core Data: \(fileManagerError)") abort() } // 5 拷贝wal(write-ahead logging file) fileManagerError = nil let walURL = documentsURL.URLByAppendingPathComponent( "SurfJournalDatabase.sqlite-wal") let seededWALURL = bundle .URLForResource("SurfJournalDatabase", withExtension: "sqlite-wal") let didCopyWAL = NSFileManager.defaultManager() .copyItemAtURL(seededWALURL!, toURL: walURL, error: &fileManagerError) if !didCopyWAL { println("Error seeding Core Data: \(fileManagerError)") abort() } println("Seeded Core Data") } // 6 指定store URL即可 var error: NSError? = nil let options = [NSInferMappingModelAutomaticallyOption:true, NSMigratePersistentStoresAutomaticallyOption:true] store = psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options, error: &error) // 7 if store == nil { println("Error adding persistent store: \(error)") abort() } ~~~ 上面的方法除了拷贝sqlite文件,还拷贝了SHM (shared memory file) 和WAL (write-ahead logging) files,这都是为了并行读写的需要。无论那个文件出错了都直接让程序终止abort。 ### **二、Doing work in the background** 当我们导出数据时,会发现这个过程会阻塞UI。传统的方法是使用GCD在后台执行export操作,但Core data managed object contexts并不是线程安全的,也就是说你不能简单的开启一个后台线程然后使用相同的core data stack。 解决方法也很简单:针对export操作创建一个新的context放到一个私有线程中去执行,而不是在主线程里。 将数据导出为csv,其实很多场景都能用到,具体来学习一下: * 先为实体JournalEntry子类添加一个csv string方法,将属性输出为字符串: ~~~ func csv() -> String { let coalescedHeight = height ?? "" let coalescedPeriod = period ?? "" let coalescedWind = wind ?? "" let coalescedLocation = location ?? "" var coalescedRating:String if let rating = rating?.intValue { coalescedRating = String(rating) } else { coalescedRating = "" } return "\(stringForDate()),\(coalescedHeight)," + "\(coalescedPeriod),\(coalescedWind)," + "\(coalescedLocation),\(coalescedRating)\n" } ~~~ * 通过fetch得到所有的jouranlEntry实体,用NSFileManager在临时文件夹下创建一个csv文件并返回这个URL ~~~ // 1 var fetchRequestError: NSError? = nil let results = coreDataStack.context.executeFetchRequest( self.surfJournalFetchRequest(), error: &fetchRequestError) if results == nil { println("ERROR: \(fetchRequestError)") } // 2 let exportFilePath = NSTemporaryDirectory() + "export.csv" let exportFileURL = NSURL(fileURLWithPath: exportFilePath)! NSFileManager.defaultManager().createFileAtPath( exportFilePath, contents: NSData(), attributes: nil) ~~~ * 用这个URL初始化一个NSFileHandle,用*for-in*遍历取出每一个journalEntry实体,执行csv()将自身属性处理成字符串,然后用UTF8-encoded编码转换为NSData类型的data,最后NSFileHandle将data写入URL ~~~ // 3 var fileHandleError: NSError? = nil let fileHandle = NSFileHandle(forWritingToURL: exportFileURL, error: &fileHandleError) if let fileHandle = fileHandle { // 4 for object in results! { let journalEntry = object as JournalEntry fileHandle.seekToEndOfFile() let csvData = journalEntry.csv().dataUsingEncoding( NSUTF8StringEncoding, allowLossyConversion: false) fileHandle.writeData(csvData!) } // 5 fileHandle.closeFile() ~~~ 学习完如何将数据导出为csv,我们来进入本章真正的主题,创建一个私有的后台线程,把export操作放在这个后台线程中去执行。 ~~~ // 1 创建一个使用私有线程的context,与main context共用一个persistentStoreCoordinator let privateContext = NSManagedObjectContext( concurrencyType: .PrivateQueueConcurrencyType) privateContext.persistentStoreCoordinator = coreDataStack.context.persistentStoreCoordinator // 2 performBlock这个方法会在context的线程上异步执行block里的内容 privateContext.performBlock { () -> Void in // 3 获取所有的JournalEntry entities var fetchRequestError:NSError? = nil let results = privateContext.executeFetchRequest( self.surfJournalFetchRequest(), error: &fetchRequestError) if results == nil { println("ERROR: \(fetchRequestError)") } ...... ~~~ 在后台执行performBlock的过程中,所有UI相关的操作还是要回到主线程中来执行。 ~~~ // 4 dispatch_async(dispatch_get_main_queue(), { () -> Void in self.navigationItem.leftBarButtonItem = self.exportBarButtonItem() println("Export Path: \(exportFilePath)") self.showExportFinishedAlertView(exportFilePath) }) } else { dispatch_async(dispatch_get_main_queue(), { () -> Void in self.navigationItem.leftBarButtonItem = self.exportBarButtonItem() println("ERROR: \(fileHandleError)") }) } } // 5 closing brace for performBlock() ~~~ 关于managed object context的**concurrency types**一共有三种类型: * ConfinementConcurrencyType 这种手动管理线程访问的基本不用 * PrivateQueueConcurrencyType 指定context将在后台线程中使用 * MainQueueConcurrencyType 指定context将在主线程中使用,任何UI相关的操作都要使用这一种,包括为table view创建一个fetched results controller。 ### **三、Editing on a scratchpad** 本节介绍了另外一种情形,类似于便笺本,你在上面涂写,到最后你可以选择保存也可以选择丢弃掉。作者使用了一种**child managed object contexts**的方式来模拟这个便签本,要么发送这些changes到parent context保存,要么直接丢弃掉。 具体的技术细节是:所有的managed object contexts都有一个叫做**parent store**(父母空间)的东西,用来检索和修改数据(具体数据都是*managed objects*形式)。进一步讲,the parent store其实就是一个**persistent store coordinator**,比如main context,他的parent store就是由CoreDataStack提供的*persistent store coordinator*。相对的,你可以将一个context设置为另一个context的**parent store**,其中一个context就是child context。而且当你保存这个child context时,这些changes只能到达parent context,不会再向更高的parent context传递(除非parent context save)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ecd7b6080.jpg) 关于这个冲浪APP还是有个小问题,当添加了一个新的journal entry后,就会创建新的**object1**添加到context中,如果这时候点击Cancel按钮,应用是不会保存到context,但这个**object1**会仍然存在,这个时候,再增加另一个**object2**然后保存到context,此时**object1**这个被取消的对象仍然会出现在table view中。 你可以在cancel的时候通过简单的删除操作来解决这个issue,但是如果操作更加复杂还是使用一个临时的child context更加简单。 ~~~ // 1 let childContext = NSManagedObjectContext( concurrencyType: .MainQueueConcurrencyType) childContext.parentContext = coreDataStack.context // 2 let childEntry = childContext.objectWithID( surfJournalEntry.objectID) as JournalEntry // 3 detailViewController.journalEntry = childEntry detailViewController.context = childContext detailViewController.delegate = self ~~~ 创建一个childContext,**parent store**设为main context。这里使用了**objectID**来获取*journal entry*。因为managed objects只特定于自己的context的,而**objectID**针对所有的context都是唯一的,所以childContext要使用**objectID**来获取mainContext中的*managed objects*。 最后一点要注意的是注释3,这里同时为detailViewController传递了**managed object**(childEntry)和**managed object context**(childContext),为什么不只传递**managed object**呢,他可以通过属性*managed object context*来得到context呀,原因就在于**managed object**对于**context**仅仅是**弱引用**,如果不传递context,ARC就有可能将其移除,产生不可控结果。 历时一周终于写完了,通过对Core Data的系统学习还是收获不小的:)
';

Core Data by tutorials 笔记(七)

最后更新于:2022-04-01 02:41:02

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-qi 本章主要介绍了一些影响Core Data的性能问题,以及优化的方法。如果你对CoreData的其他方面感兴趣请查看我之前的笔记或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials) ## **Chapter 9:Measuring and Boosting Performance** ### **一、Getting started** 性能其实是一个需要在内存用量与速度之间的平衡问题,访问内存中的数据比磁盘中的数据要快很多,但是往内存中存入大量数据又会引起触发low memory warnings,你的程序又很快会被系统干掉。所以这都要靠开发者自己去平衡。 本章提供了一个关于“雇员名录”的Start Project,基于tab-bar。第一次启动时间会非常的长,这也是作者故意这么做的,方便我们接下来优化。 ### **二、Measure,change,verify** 关于性能优化,作者主要列举了三个步,通过对这三步的反复执行形成一个个闭环,从而达到性能最优的目标。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eca9c38c9.jpg) ①. 使用Gauges,Instruments和XCTest framework等工具衡量性能。 ②. 针对第1步结果,改写code提高性能。 ③. 验证新加code对性能的提升。 反复执行这三步,达到性能最优的目的。 1. **Measuring the problem** 首先运行这个Start APP,切换到Memory Report查看具体的内存用量。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eca9e275d.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ecaa1ccc5.jpg) ~~~ 上图2稳定下来的内存用量基本上就是整个程序最终的内存占用。可以看到整个内存占用还是相当可观的。接下来我们结合代码来分析一下。 ~~~ 通过对seed.json以及载入seed的方法的review,发现引起内存占用的主要是载入了大量的图片,一个解决办法就是将图片从整个数据结构中剥离出来,真正需要时再载入内存。 2. **Making changes to improve performance** 书中的做法是在数据model中新添加了一个EmployeePicture实体,用来专门存放图片相关属性(这里为其添加了一个picture的属性)。这里的picture属性设置为**Binary Data**并勾选了**Allows External Storage**,意味着由Core Data自己决定将这些二进制数据单独保存到磁盘中还是保留在sqlite数据库中。 将原Employee实体的**picture**属性重名为**pictureThumbnail**,表明在Employee中只保存缩略图,从而缩减内存用量。 在model中创建Employee与EmployeePicture之间的relationship。接下来就是对代码进行一些修改。如果是从网络API获取的图片,要看是否提供了缩略图的版本,因为这里将图片转换成缩略图的*绘图方法*也是会消耗一些性能的。 3. **Verify the changes** 再次使用Memory Report来验证内存用量,发现已经从当初的392MB缩减为41MB。 ### **三、Fetching and performance** 对Fetch进行优化同样是要平衡内存的占用率,为了提高启动速率,我们来削减不必要的fetch。Core Data提供了一个**fetchBatchSize**属性来避免Fetch多余数据,该属性的默认值为0(没有启用)。 接着我们使用**Instruments**工具来分析一下(要选择Instruments Core Data template),默认的Core Data template包括下面三个工具: * Core Data Fetches Instrument >Captures fetch count and duration of fetch operations. This will help you balance the number of fetch requests versus the size of each request. * Core Data Cache Misses Instrument >Captures information about fault events that result in cache misses. This can help diagnose performance in low-memory situations. * Core Data Saves Instrument >Captures information on managed object context save events. Writing data out to disk can be a performance and battery hit, so this instrument can help you determine whether you should batch things into one big save rather than many small ones. 我们点击Record按钮,等待20秒后停止,选择Core Data Fetches工具,下面的窗口会展示详细的信息,包括**fetch entity,fetch count,fetch duration** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ecaa3c02f.jpg) 通过上图可以看出一次imports了50个employees,而且完成整个fetch花了2000微秒,并不十分高效。 接着就来修改代码,书里将fetchRequest.fetchBatchSize = 10,至于fetchBatchSize这个具体的数字应为屏幕显示条目数的2倍,iPhone一次大概能显示5行,因此这里设为10比较合适。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ecaa65e32.jpg) 最后来验证我们的修改效果,同样是运行Core Data Fetches tool 20秒,这次可以看到多个fetches,排第一fetch因为只fetch count而不是所有的objects,所以速度是最快的,随后,我们滚动scrollView,fetch counts每次为10,表明每fetch一次最多取回10个objcet,也符合之前fetchBatchSize的设置。 更进一步的做法是使用**predicates**条件来限制返回的data,仅仅fetch需要的数据即可。如果存在多个*predicate*,那么将更具限制性的条件放在前面性能会更好一些。更多细节见官方[Guide](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html) > "(active == YES) AND (name CONTAINS[cd] %@)"就比"(name CONTAINS[cd] %@) AND (active == YES)"更加高效。 最后我们换一种工具来衡量性能,这次使用的是**XCTest framework**,这也是Xcode 6新增加的特性,可以用来进行性能方面的测试,关于单元测试可以看我[Core Data by tutorials 笔记(六)](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-liu) 打开**DepartmentListViewControllerTests.swift**,添加一个测试方法: ~~~ func testTotalEmployeesPerDepartment(){ measureMetrics([XCTPerformanceMetric_WallClockTime], automaticallyStartMeasuring: false) { let departmentList = DepartmentListViewController() departmentList.coreDataStack = CoreDataStack() //标记开始measuring self.startMeasuring() let items = departmentList.totalEmployeesPerDepartment() self.stopMeasuring() } } ~~~ 这个方法使用**measureMetrics**来衡量具体code的执行时间。CMD+U执行测试方法,会显示具体消耗的时间(不同设备需要的时间也不相同)。 我们来修改代码只获取employee的数目**count**而不是实际的对象,书中这里用到了[NSExpressionDescription](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-er),修改完成后继续验证,果然又快了很多。 接着我们来获取sales的数目,同样是先验证,然后再修改代码,再次验证。这次我们在设置完predicate后,直接使用[countForFetchRequest](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-er)来获取couts数目。 获取sales还有没有更快的方法呢,答案是肯定的,因为employee有一个属性就是sales,所以我们可以简单地使用relationship来获取: ~~~ public func salesCountForEmployeeSimple(employee:Employee) -> String { return "\(employee.sales.count)" } ~~~
';

Core Data by tutorials 笔记(六)

最后更新于:2022-04-01 02:41:00

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-liu 今天我们来关注一下CoreData的单元测试,其实在写程序之前,先写测试,将问题一点点分解,也是TDD所倡导的做法,这也是我今年所期望达成的一个目标,新开项目按TDD的流程来做,以后也会整理些这方面的东西。如果你对CoreData的其他方面感兴趣请查看我之前的笔记或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials) ## **Chapter 8: Unit Testing** 作者列举了一系列单元测试的好处:帮助你在一开始就组织好项目的结构,可以不用操心UI去测试核心功能。单元测试还可以方便重构。还可以更好地拆分UI进行测试。 这章主要焦距在XCTest这个框架来测试Core Data程序,多数情况下Core Data的测试要依赖于真实的Core Data stack,但又不想将单元测试的test data与你手动添加的接口测试弄混,本章也提供了解决方案。 ### **一、Getting started** 本章要测试的是一个关于野营管理的APP,主要管理营地、预订(包括时间表和付款情况)。作者将整个业务流程分解为三块: 1. campsites(野营地) 2. campers(野营者) 3. reservations(预订) 由于swift内部的访问控制,app和其test分别属于不同的targets和不同的modules,因此你并不能普通地从tests中访问app中的classes,这里有两个解决办法: 1. 把App中的classes和methods标记为**public**,是其对tests可见。 2. 直接在test target里添加所需要的classes。 作者提供的实例中,已经将要测试的类和方法标记为*public*的了,现在就可以对Core Data部分进行测试了,作者在测试开始前给了一些建议: > Good unit tests follow the acronym **FIRST**: > > • **F**ast: If your tests take too long to run, you won’t bother running them. > > • **I**solated: Any test should function properly when run on its own or before or after any other test. > > • **R**epeatable: You should get the same results every time you run the test against the same codebase. > > • **S**elf-verifying: The test itself should report success or failure; you shouldn’t have to check the contents of a file or a console log. > > • **T**imely: There’s some benefit to writing the tests after you’ve already written the code, particularly if you’re writing a new test to cover a new bug. Ideally, though, the tests come first to act as a specification for the functionality you’re developing. 为了达到上面提到“**FIRST**”目标,我们需要修改Core Data stack使用**in-memory store**而不是SQLite-backed store。具体的做法是为**test target**创建一个CoreDataStack的子类来修改*store type*。 ~~~ class TestCoreDataStack: CoreDataStack { override init() { super.init() self.persistentStoreCoordinator = { var psc: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) var error: NSError? = nil var ps = psc!.addPersistentStoreWithType( NSInMemoryStoreType, configuration: nil, URL: nil, options: nil, error: &error) if (ps == nil) { abort() } return psc }() } } ~~~ ### **二、Your first test** 单元测试需要将APP的逻辑拆分出来,我们创建一个类来封装这些逻辑。作者这里创建的第一个测试类为**CamperServiceTests**是**XCTestCase**的子类,用来测试APP**CamperService**类中的逻辑 ~~~ import UIKit import XCTest import CoreData import CampgroundManager // class CamperServiceTests: XCTestCase { var coreDataStack: CoreDataStack! var camperService: CamperService! override func setUp() { super.setUp() coreDataStack = TestCoreDataStack() camperService = CamperService(managedObjectContext: coreDataStack.mainContext!, coreDataStack: coreDataStack) } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() coreDataStack = nil camperService = nil } func testAddCamper() { let camper = camperService.addCamper("Bacon Lover", phoneNumber: "910-543-9000") XCTAssertNotNil(camper, "Camper should not nil") XCTAssertTrue(camper?.fullName == "Bacon Lover") XCTAssertTrue(camper?.phoneNumber == "910-543-9000") } ~~~ **setUp**会在每次测试前被调用,这里可以创建一些测试需要用到东西,而且因为使用的是in-memory store,每次在setUp中创建的context都是全新的。**tearDown**相对于*setUp*,是在每次test结束后调用,用来清除一些属性。上面的例子主要测试了addCamper()方法。 ~~~ 这里注意的就是该测试创建的对象和属性都不会保存在任何store中的。 ~~~ ### **三、Asynchronous tests** 关于异步测试,这里用到了两个context,一个root context运行在后台线程中,另外一个main context是root context的子类,让context分别在正确的线程中执行其实也很简单,主要采用下面两种方法: 1. performBlockAndWait() 将等待block里的内容执行完后才继续 2. performBlock() 执行到此方法立即返回 测试第二种performBlock()方法时可能会需要些技巧,因为数据可能不会立即得到,还好XCTestCase提供了一个叫**expectations**的新特性。下面展示了使用**expectation**来完成对异步方法的测试: ~~~ let expectation = self.expectationWithDescription("Done!"); someService.callMethodWithCompletionHandler() { expectation.fulfill() } self.waitForExpectationsWithTimeout(2.0, handler: nil) ~~~ 该特性的关键是要么是*expectation.fulfill()*被执行,要么触发超时产生一个异常expectation,这样test才能继续。 我们现在来为CamperServiceTests继续增加一个新的方法来测试root context的保存: ~~~ func testRootContextIsSavedAfterAddingCamper() { //1 创建了一个针对异步测试的方法,主要是通过观察save方法触发的通知,触发通知后具体的handle返回一个true。 let expectRoot = self.expectationForNotification( NSManagedObjectContextDidSaveNotification, object: coreDataStack.rootContext) { notification in return true } //2 增加一个camper let camper = camperService.addCamper("Bacon Lover", phoneNumber: "910-543-9000") //3 等待2秒,如果第1步没有return true,那么就触发error self.waitForExpectationsWithTimeout(2.0) { error in XCTAssertNil(error, "Save did not occur") } } ~~~ ### **四、Tests first** 这一节新建了一个**CampSiteServiceTests** Class 对CampSiteService进行测试,具体code形式与上一节类似,添加了测试**testAddCampSite()**和**testRootContextIsSavedAfterAddingCampsite()**,作者在这里主要展示了**TDD**的概念。 > Test-Driven Development (TDD) is a way of developing an application by writing a test first, then incrementally implementing the feature until the test passes. The code is then refactored for the next feature or improvement. 根据需求又写了一个**testGetCampSiteWithMatchingSiteNumber()**方法用来测试**getCampSite()**,因为campSiteService.addCampSite()方法在之前的测试方法中已经通过测试了,所以这里可以放心去用,这就是TDD的一个精髓吧。 ~~~ func testGetCampSiteWithMatchingSiteNumber(){ campSiteService.addCampSite(1, electricity: true, water: true) let campSite = campSiteService.getCampSite(1) XCTAssertNotNil(campSite, "A campsite should be returned") } func testGetCampSiteNoMatchingSiteNumber(){ campSiteService.addCampSite(1, electricity: true, water: true) let campSite = campSiteService.getCampSite(2) XCTAssertNil(campSite, "No campsite should be returned") } ~~~ 写完测试方法运行一下CMD+U,当然通不过啦,我们还没有实现他。现在为CampSiteService类添加一个getCampSite()方法: ~~~ public func getCampSite(siteNumber: NSNumber) -> CampSite? { let fetchRequest = NSFetchRequest(entityName: "CampSite") fetchRequest.predicate = NSPredicate( format: "siteNumber == %@", argumentArray: [siteNumber]) var error: NSError? let results = self.managedObjectContext.executeFetchRequest( fetchRequest, error: &error) if error != nil || results == nil { return nil } return results!.first as CampSite? } ~~~ 现在重新CMD+U一下,就通过了。 ### **五、Validation and refactoring** 最后一节主要针对APP中的**ReservationService**类进行测试,同样的是创建一个**ReservationServiceTests**测试类,这个test类的setUP和tearDown与第三节类似。只不过多了campSiteService与camperService的设置。在*testReserveCampSitePositiveNumberOfDays()*方法中对ReservationService类里的**reserveCampSite()**进行测试后,发现没有对numberOfNights有效性进行判断,随后进行了修改,这也算是展示了单元测试的另一种能力。作者是这么解释的:不管你对这些要测试的code有何了解,你尽肯能地针对这些API写一些测试,如果OK,那么皆大欢喜,如果出问题了,那意味着要么改进code要么改进测试代码。
';

Core Data by tutorials 笔记(五)

最后更新于:2022-04-01 02:40:58

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-wu 我们继续来看[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)这本书的第七章 Syncing with iCloud,本章所讨论的是iOS 8和Yosemite最新释出的iCloud Drive,至于iCloud Drive与iCloud的区别可以看[这里](http://www.v2ex.com/t/139804),调试本章code需要一个开发者帐号:) ## **Chapter 7: Syncing with iCloud** ### **一、CloudNotes** iCloud是基于Core Data的,之前几个版本确实做的比较烂,不过苹果也在不断地改进。本章的代码其实就是在第六章代码的基础上改为Cloud版本。 ### **二、Enabling iCloud** iCloud对core data store同步其实使用的是**ubiquity containers**(无处不在的容器),这个*无处不在的容器*存在你应用的sandbox中,并由iOS系统替你管理。如同向NSFileManager请求应用的 Documents directory,对于iCloud来说,请求的是一个**ubiquity container URL**。 Core Data通过这个ubiquity container URL保存应用程序的数据,他和通过URL保存到Documents没什么区别。唯一不同的就是iCloud后台进程监视着*ubiquity container*里面文件的变化,时刻准备上传到云端,而这一切都是系统自动完成的。 这里需要注意的是,iCloud的运行机制有点类似与**Git**,每次同步的其实是transaction logs,而不是data store本身。而与Git不同的是,commit的时候,Git经常需要处理冲突。但iCloud的commit change是“atomic”的,意味着,要么数据全部有效,要么全部丢弃。 作者这里用伦敦桥搬家举了一个很形象的例子,把桥拆成一砖一砖,然后搬到目的地再按照log记录的正确顺序重组。iCloud的工作机制也差不多。 在Xcode中将Capabilities中的iCloud启用。这里可以使用默认的ubiquity container,也可以自定义。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eb3414c7d.jpg) ### **三、The cloud stack** 为**.addPersistentStoreWithType方法的参数***option数组*增加一个成员**NSPersistentStoreUbiquitousContentNameKey**,同样地在stack的初始化中设置。 ~~~ lazy var stack : CoreDataStack = { let options = [NSPersistentStoreUbiquitousContentNameKey: "CloudNotes", NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true] let stack = CoreDataStack(modelName: "CloudNotesDataModel", storeName: "CloudNotes", options: options) return stack }() ~~~ 使用NSPersistentStoreUbiquitousContentNameKey的值*CloudNotes*,在ubiquity container中来唯一标识该应用的persistent store。 到此为止,已经为该APP完全开启了iCloud同步,很简单吧。但Core Data在背后还是做了一些工作的,其中一项就是设置了一个**fallback store**,用于在iCloud离线时保存数据。不过开发者完全不用担心这些东西。 ### **四、Testing iCloud** 至于测试则需要拥有一个开发者帐号,登录[itunesconnect](https://itunesconnect.apple.com/),依次选择**Users and Roles** >> **Sandbox Testers** 新建一个测试用户,这里注意的是,新建完的测试帐号需要在真机设备上激活一下。 具体的测试也很简单,现在模拟器上运行起来,可以看到目前所有添加的数据,然后切换target device(并不按下停止键)选择真机设备Run一下,此时模拟器和真机会同时运行。此时在模拟器上创建新的记录,并选择Debug\Trigger iCloud Sync来触发同步,不久应该就能看到新添加的记录在真机上出现。作者还展示了**iCloud gauge**的方式来查看具体的同步记录。 现在可以来总结一下设置iCloud的基本步骤了,主要有三步: 1. 启用iCloud并且设置ubiquity containers。 2. 通过设置persistent store的options来启用 iCloud-Core Data同步。 3. 当app运行时,设置app响应新的变化。 前两步已经说过了,现在来看第3点。 ### **五、Responding to iCloud changes** 该实例程序使用的是fetched results controller,而fetched results controller主要又依赖的是NSManagedObjectContext。但是iCloud更新是直接在persistent store级别的,不会经过Context。因此也不会触发fetched results controller的代理方法来更新UI。 既然如此,我们需要知道iCloud何时更新可以通过监听广播的方法来实现,通过监听**NSPersistentStoreDidImportUbiquitousContentChangesNotification**广播,来刷新context(通过mergeChangesFromContextDidSaveNotification(notification)) ### **六、Switching iCloud accounts** 当前账户登出的话,Core Data会删除当前数据(都安全保存在云端,会在用户重新登录时同步回来)。帐号切换时,Core Data会发送如下两个通知: * NSPersistentStoreCoordinatorStoresWillChangeNotification * NSPersistentStoreCoordinatorStoresDidChangeNotification **the notification**会包含具体要被*adding/added*或*removing/removed*的**NSPersistentStore objects** 1. 当Core Data发出NSPersistentStoreCoordinatorStoresWillChangeNotification时,Core Data stack会保存当前所有数据到当前store中,这样用户帐号登出了不会丢失任何数据。接着重置managed object context。 2. 当Core data发出“did change” notification时, the notes list controller需要重为新登录的帐号的或本地存储的数据源刷新View。 先处理 “will change” notification: ~~~ func persistentStoreCoordinatorWillChangeStores( notification: NSNotification){ var error : NSErrorPointer = nil if context.hasChanges { if context.save(error) == false { print("Error saving \(error)") } } context.reset() } ~~~ 再处理“did change” notification ~~~ func persistentStoreCoordinatorDidChangeStores(notification:NSNotification){ notes.performFetch(nil) tableView.reload() } ~~~
';

Core Data by tutorials 笔记(四)

最后更新于:2022-04-01 02:40:55

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-si Raywenderlich家[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)这本书到此为止已经回顾过半,今天来学习一下第六章“版本迁移”。第六章也是本书篇幅最多的。根据数据模型的每一次的调整程度,数据迁移都有可能会变得更加复杂。最后,迁移数据所花的成本甚至超过了所要实现的功能。那么前期完善对Model的设计将会变得十分重要,这一切都需要开发者去权衡。 ## **Chapter 6: Versioning and Migration** 本章提供了一个记事本APP,未来数据结构要变更,迁移(migration)过程就是:在旧data model的基础上将数据迁移到新的data model中来。 ### **一、When to migrate** 如果仅仅是把Core data当做是离线缓存用,那么下次update的时候,丢弃掉就OK了。但是,如果是需要保存用户的数据,在下个版本仍然能用,那么就需要迁移数据了,具体操作是创建一个**新版本的data model**,然后提供一个*迁移路径(migration path)*。 ### **二、The migration process** 在创建Core Data stack的时候,系统会在添加store到persistent store coordinator之前分析这个store的model版本,接着与coordinator中的data model相比较,如果不匹配,那么Core Data就会执行迁移。当然,你要启用*允许迁移*的选项,否则会报错。 具体的迁移需要源data model和目的model,根据这两个版本的model创建mapping model,mapping model可以看做是迁移所需要的地图。 迁移主要分三步: 1. Core Data拷贝所有的对象从一个data store到另一个。 2. Core Data根据**relationship mapping**重建所有对象的关系 3. 在destination model开启数据有效性验证,在此之前的copy过程中是被disable了。 这里不用担心出错,Core Data只有迁移成功,才会删除原始的data store数据。 作者根据日常经验将迁移划分为四种: * Lightweight migrations * Manual migrations * Manual migrations * Fully manual migrations ~~~ 第一种是苹果的方式,你几乎不用做什么操作,打开选项迁移就会自动执行。第二种需要设置一个mapping model类似与data model,也是全GUI操作没什么难度。第三种,就需要你在第二种的基础上自定义迁移策略(NSEntityMigrationPolicy)供mapping model选择。最后一种考虑的是如何在多个model版本中跨版本迁移,你要提供相应的判定代码。 ~~~ ### **三、A lightweight migration** 所谓轻量级的迁移就是给Note实体增加了一个image的属性。要做的步骤也很简单: 1. 在上一model基础上创建UnCloudNotesDataModel v2,然后添加image属性。 2. 启用Core Data自动迁移选项,这个选项在**.addPersistentStoreWithType方法**中开启 > 作者的做法是在CoreDataStack初始化的时候传入这个options数组参数,然后再传递给.addPersistentStoreWithType方法。 ~~~ init(modelName: String, storeName: String, options: NSDictionary? = nil) { self.modelName = modelName self.storeName = storeName self.options = options } store = coordinator.addPersistentStoreWithType( NSSQLiteStoreType, configuration: nil, URL: storeURL, options: self.options, error: nil) lazy var stack : CoreDataStack = CoreDataStack( modelName:"UnCloudNotesDataModel", storeName:"UnCloudNotes", options:[NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]) ~~~ **NSMigratePersistentStoresAutomaticallyOption**是自动迁移选项,而**NSInferMappingModelAutomaticallyOption**是mapping model自动推断。所有的迁移都需要mapping model,作者也把mapping model比作是向导。紧接着列出了可以应用自动推断的一些模式,基本上都是对实体、属性的增、删、改以及关系的修改。 > 1. Deleting entities, attributes or relationships; > 2. Renaming entities, attributes or relationships using the renamingIdentifier; > 3. Adding a new, optional attribute; > 4. Adding a new, required attribute with a default value; > 5. Changing an optional attribute to non-optional and specifying a default value; > 6. Changing a non-optional attribute to optional; > 7. Changing the entity hierarchy; > 8. Adding a new parent entity and moving attributes up or down the hierarchy; > 9. Changing a relationship from to-one to to-many; > 10. Changing a relationship from non-ordered to-many to ordered to-many (and vice versa). 所以正确的做法就是任何数据迁移都应先从自动迁移开始,如果搞不定才需要手动迁移。 ### **四、A manual migration** 1. 与lightweight migration相同,首先要创建一个UnCloudNotesDataModel v3,这次需要添加一个新Entity,命名为Attachment,并给该Entity添加两个属性dateCreated、image。将Note和Attachment的关系设为一对多,即一个note会有多个attachment。 2. 创建一个**mapping model**,命名为UnCloudNotesMappingModel_v2_to_v3 3. 修改mapping model,分为**Attribute Mappings**和**Relationship Mappings** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eafb36e8f.jpg) 上图是实体**Note**的**mapping model**,这里的source指的是源数据模型(data model)里的Note实体,创建新加实体**Attachment**的*mapping model*也很简单,在Entity Mapping inspector里将**source entity**改为**Note**,接着实体Attachment的属性dateCreated、image就来自于上一版data model里的Note实体。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eb010a342.jpg) 在Mapping model中可以添加过滤条件,比如设置NoteToAttachment的**Filter Predicate**为image != nil,也就是说Attachment的迁移只有在image存在的情况下发生。 4. Relationship mapping,这里要注意的一点就是实体Note与Attachment的关系是在UnCloudNotesDataModel v3这一版本中添加的,所以我们需要的*destination relationship*其实就是UnCloudNotesDataModel v3中的relationship。于是我们这样获得这段关系 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eb0123eca.jpg) 作者这里展示了这个表达式函数: ~~~ FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "NoteToNote", $source) ~~~ 5. 最后需要更改之前CoreData的*options*设置 ~~~ options:[NSMigratePersistentStoresAutomaticallyOption:true, NSInferMappingModelAutomaticallyOption:false] ~~~ 将自动推断mapping model关掉,因为我们已经自定义了mapping model。 ### **五、A complex mapping model** 1. 创建一个UnCloudNotesDataModel v4的版本,在v3的版本上增加一个Entity,命名为**ImageAttachment**,设为*Attachment*的子类。接着为这个新的ImageAttachment添加caption、width、height三个属性,移除Attachment中的image。这样就为今后支持videos、audio做好了扩展准备。 2. 添加UnCloudNotesMappingModel_v3_to_v4,和上一节类似,**NoteToNote mapping**和**AttachmentToAttachment mapping**Xcode已经为我们设置OK了,我们只需关注**AttachmentToImageAttachment**,修改他的$source为**Attachment** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6eb0158727.jpg) 除了从父类*Attachment*继承而来的属性,新添加的三个属性都没有mapping,我们用代码来实现吧。 3. 除了mapping model中的*FUNCTION expressions*,我们还可以自定义**migration policies**。增加一个NSEntityMigrationPolicy类的swift文件命名为AttachmentToImageAttachmentMigrationPolicyV3toV4,覆盖NSEntityMigrationPolicy初始化方法: ~~~ class AttachmentToImageAttachmentMigrationPolicyV3toV4: NSEntityMigrationPolicy { override func createDestinationInstancesForSourceInstance( sInstance: NSManagedObject, entityMapping mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool { // 1 创建一个新destination object let newAttachment = NSEntityDescription.insertNewObjectForEntityForName("ImageAttachment", inManagedObjectContext: manager.destinationContext) as NSManagedObject // 2 在执行手动migration之前,先执行mapping model里定义的expressions for propertyMapping in mapping.attributeMappings as [NSPropertyMapping]! { let destinationName = propertyMapping.name! if let valueExpression = propertyMapping.valueExpression { let context: NSMutableDictionary = ["source": sInstance] let destinationValue: AnyObject = valueExpression.expressionValueWithObject(sInstance, context: context) newAttachment.setValue(destinationValue, forKey: destinationName) } } // 3 从这里开始才是custom migration,从源object得到image的size if let image = sInstance.valueForKey("image") as? UIImage { newAttachment.setValue(image.size.width, forKey: "width") newAttachment.setValue(image.size.height, forKey: "height") } // 4 得到caption let body = sInstance.valueForKeyPath("note.body") as NSString newAttachment.setValue(body.substringToIndex(80), forKey: "caption") // 5 manager作为迁移管家需要知道source、destination与mapping manager.associateSourceInstance(sInstance, withDestinationInstance: newAttachment, forEntityMapping: mapping) // 6 成功了别忘了返回一个bool值 return true } } ~~~ 这样就定义了一个自定义迁移policy,最后别忘了在AttachmentToImageAttachment的**Entity Mapping Inspector**里*Custom Policy*那一栏填入我们上面创建的这个*UnCloudNotes.AttachmentToImageAttachmentMigrationPolicyV3toV4*。 ## **六、Migrating non-sequential versions** 如果存在多个版本非线性迁移,也就是可能从V1直接到V3或V4...这又该怎么办呢,这节代码比较多,说下思路,就不全帖出来了。 1. 创建一个DataMigrationManager,这个类有一个stack属性,由他来负责提供合适的migrated Core Data stack。为了分清各个版本,这个manager初始化需要传入store name和model name两个参数。 2. 扩展NSManagedObjectModel,创建两个类方法: ~~~ class func modelVersionsForName(name: String) -> [NSManagedObjectModel] class func uncloudNotesModelNamed(name: String) -> NSManagedObjectModel ~~~ 前者根据model名称返回所有版本的model,后者返回一个指定的Model实例。 > When Xcode compiles your app into its app bundle, it will also compile your data models. The app bundle will have at its root a .momd folder that contains .mom files. MOM or Managed Object Model files are the compiled versions of .xcdatamodel files. You’ll have a .mom for each data model version. 3. 根据上面扩展的方法,继续对NSManagedObjectModel进行扩展,创建几个比较版本的handle method,例如: ~~~ class func version2() -> NSManagedObjectModel { return uncloudNotesModelNamed("UnCloudNotesDataModel v2") } func isVersion2() -> Bool { return self == self.dynamicType.version2() } ~~~ 直接使用“==”比较当然是不行的,这里继续对“==”改写一下,有同样的entities就判定相等: ~~~ func ==(firstModel:NSManagedObjectModel, otherModel:NSManagedObjectModel) -> Bool { let myEntities = firstModel.entitiesByName as NSDictionary let otherEntities = otherModel.entitiesByName as NSDictionary return myEntities.isEqualToDictionary(otherEntities) } ~~~ 4. 增加store和model是否匹配的判断方法,这里主要用NSPersistentStoreCoordinator的**metadataForPersistentStoreOfType方法**返回一个**metadata**,然后再用model的**isConfiguration方法**对这个*metadata*进行判断,来决定model和persistent store是否匹配。 5. 添加两个计算属性,**storeURL**和**storeModel**,storeModel遍历所有的model,通过第4步的判断方法找出相匹配的storeModel。 6. 修改stack的定义:先判断,store与model不相容,就先执行迁移。 ~~~ var stack: CoreDataStack { if !storeIsCompatibleWith(Model: currentModel) { performMigration() } return CoreDataStack(modelName: modelName, storeName: storeName, options: options) } ~~~ 7. 自定义一个迁移方法,将store URL、source model、destination model和可选的mapping model作为参数,这就是完全手动实现迁移的方法。如果做轻量级的迁移,将最后一个mapping model设为nil,那么使用本方法和系统实现没有差别。 ~~~ func migrateStoreAt(URL storeURL:NSURL, fromModel from:NSManagedObjectModel, toModel to:NSManagedObjectModel, mappingModel:NSMappingModel? = nil) { //...... } ~~~ 8. 最后我们来实现第6步提到的**performMigration**方法,现在最新的版本是v4,开始之前先做个判断,当前model的最新版本为v4,才执行这个performMigration方法下面的内容: ~~~ if !currentModel.isVersion4() { fatalError("Can only handle migrations to version 4!") } ~~~ 这样就变成了从v1 -> v4,v2 -> v4,v3 -> v4的迁移,接下来的方法也很简单,分别判断storeModle的版本号,执行第7步的*migrateStoreAt:*方法,并且通过对*performMigration*方法的**递归调用**来最终迁移到v4版本。 作者最后还给了两条建议: * 尽量可能采取最简单的迁移方式,因为迁移很难测试。 * 每个版本都尽量保存一点数据以便将来迁移时可以测试。
';

Core Data by tutorials 笔记(三)

最后更新于:2022-04-01 02:40:53

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-san 今天继续来学习Raywenderlich家[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)的第五章,本章将会聚焦在**NSFetchedResultsController** * * * ## **Chapter 5: NSFetchedResultsController** 作者在开篇就提到了NSFetchedResultsController虽然是一个controller,但是他并不是一个view controller,因为他没有view。 按本章的目录梳理一下 ### **一、Introducing the World Cup app** 本章要完成一个World Cup App,作者提供了一个基本的Start Project,快速浏览一下,原始数据保存在seed.json文件中。 ### **二、It all begins with a fetch request...** **NSFetchedResultsController**大概是可以看做对“NSFetchRequest获取结果”这一过程的一种封装。话不多说,直接上代码: ~~~ //1 let fetchRequest = NSFetchRequest(entityName: "Team") //2 fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil) //3 var error: NSError? = nil if (!fetchedResultsController.performFetch(&error)) { println("Error: \(error?.localizedDescription)") } ~~~ > 前面介绍过,NSFetchRequest是可以高度定制化的,包括sort descriptors、predicates等 注意一下NSFetchedResultsController初始化需要的两个必要参数fetchRequest和context,第3步由之前的context来performFetch改为NSFetchedResultsController来performFetch,可以看做是NSFetchedResultsController接管了context所做的工作,当然NSFetchedResultsController不仅仅是封装performFetch,他更重要的使命是负责协调Core Data和Table View显示之间的同步。这样一来,你所需要做到工作就只剩下了提供各种定制好的NSFetchRequest给NSFetchedResultsController就好了。 除了封装了fetch request之外,NSFetchedResultsController内部有容器存储了fetched回来的结果,可以使用fetchedObjects属性或objectAtIndexPath方法来获取到。下面是一些提供的Data source方法: ~~~ func numberOfSectionsInTableView(tableView: UITableView) -> Int { return fetchedResultsController.sections!.count } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let sectionInfo = fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo return sectionInfo.numberOfObjects } ~~~ sections 数组包含的对象实现了NSFetchedResultsSectionInfo代理,由他来提供title和count信息。接着来看configureCell ~~~ func configureCell(cell: TeamCell, indexPath: NSIndexPath) { let team = fetchedResultsController.objectAtIndexPath(indexPath) as Team cell.flagImageView.image = UIImage(named: team.imageName) cell.teamLabel.text = team.teamName cell.scoreLabel.text = "Wins: \(team.wins)" } ~~~ 这里没有专门的数组来保存数据,数据都存在fetched results controller中,并通过objectAtIndexPath来获取。 这里还要注意的一点就是**NSFetchedResultsController至少需要设置一个sort descriptor**,标准的fetch request是不需要的,但NSFetchedResultsController涉及到table view的操作,需要知道列表的排列顺序。这样就可以了,按名称排序: ~~~ let sortDescriptor = NSSortDescriptor(key: "teamName", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] ~~~ ### **三、Grouping results into sections** 参加世界杯的有亚、非、欧、大洋洲、南美,中北美及加勒比海等六大洲,球队需要按归属地(qualifyingZone)分类。qualifyingZone是Team实体的一个属性。用NSFetchedResultsController实现起来相当简单: ~~~ fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: "qualifyingZone", cacheName: nil) ~~~ 按sectionNameKeyPath分组,使用起来还是相当灵活的 > sectionNameKeyPath takes a keyPath string. It can take the form of an attribute name such as “qualifyingZone” or “teamName”, or it can drill deep into a Core Data relationship, such as “employee.address.street”. 这里还有一点要特别注意:上面我们将NSSortDescriptor只设为teamName排序,而当使用sectionNameKeyPath为qualifyingZone就会报错,正确的方法是在刚才设置NSSortDescriptor的地方添加key为qualifyingZone的NSSortDescriptor实例: ~~~ let zoneSort = NSSortDescriptor(key: "qualifyingZone", ascending: true) let scoreSort = NSSortDescriptor(key: "wins", ascending: false) let nameSort = NSSortDescriptor(key: "teamName", ascending: true) fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort] ~~~ > If you want to separate fetched results using a section keyPath, the first sort descriptor’s attribute must match the key path’s attribute. > 如果要按section keyPath分组,必须创建一个key为key path的NSSortDescriptor实例,并放在**第一位** 运行一下程序,发现每个分组内的球队先是按分数排序,然后才会按姓名,这是因为数组内对象的先后顺序和排序的优先级是相关的。 ~~~ fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort] ~~~ ### **四、“Cache” the ball** 将球队分组的操作开销有时候并不小,32支球队或许不算什么,但上百万的数据呢,或许你会想到丢掉后台去操作,这样确实不会阻塞主线程UI,但分组在后台还是会花上很长时间,你还是要loading好久才能有结果,如果每次fetch都这样,的确是个头疼的问题。好的解决办法就是,只付出一次代价,之后每次可以重用这个结果。NSFetchedResultsController的作者已经想到这个问题了,为我们提供了cahing来解决,打开它就好了。 ~~~ fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: "qualifyingZone", cacheName: "worldCup") ~~~ 要特别记住的就是这里的**section cache** 与Core Data中的*persistent store*是完全独立的 > NSFetchedResultsController’s section cache is very sensitive to changes in its fetch request. As you can imagine, any changes—such as a different entity description or different sort descriptors—would give you a completely different set of fetched objects, invalidating the cache completely. If you make changes like this, you must delete the existing cache using deleteCacheWithName: or use a different cache name. > section cache其实相当易变,要时刻注意 ### **五、Monitoring changes** 最后一个特性,十分强大但容易被滥用,也被作者称为是双刃剑。首先NSFetchedResultsController可以监听result set中变化,并且通知他的delegate。你只需要使用他的delegate方法来刷新tableView就ok了。 > A fetched results controller can only monitor changes made via the managed object context specified in its initializer. If you create a separate NSManagedObjectContext somewhere else in your app and start making changes there, your delegate method won’t run until those changes have been saved and merged with the fetched results controller’s context. > 说白了就是只能监听NSFetchedResultsController初始化传进来的context中的changes,其他不相关的context是监听不到的,除非合并到这个context中,这个在多线程中会用到。 下面是一个通常的用法 ~~~ func controllerWillChangeContent(controller: NSFetchedResultsController!) { tableView.beginUpdates() } func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) { switch type { case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic) case .Delete: tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) case .Update: let cell = tableView.cellForRowAtIndexPath(indexPath) as TeamCell configureCell(cell, indexPath: indexPath) case .Move: tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic) default: break } } func controllerDidChangeContent(controller: NSFetchedResultsController!) { tableView.endUpdates() } ~~~ 这个代理方法会被反复调用,无论data怎么变,tableView始终与persistent store保持一致。 ### **六、Inserting an underdog** 最后作者开了个小玩笑,摇动手机可以走后门加一支球队进来(比如中国队?)。其实完全是为了展示Monitoring changes的强大~
';

Core Data by tutorials 笔记(二)

最后更新于:2022-04-01 02:40:51

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-er 今天继续来重温Raywenderlich家的[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)。学完前三章就掌握了CoreData的基本知识,现在我们来深入来研究下Fetching? * * * ## **Chapter 4: Intermediate Fetching** 这一章主要深入地探究Fetching,学完这一章,你将会加满如下技能点: * fetch only what you need to; * refine your fetched results using predicates; * fetch in the background to avoid blocking the UI; * avoid unnecessary fetching by updating objects directly in the persistent store. 还是按作者的主要目录来梳理一下吧: ### **一、NSFetchRequest: the star of the show** 明星登场,同样是创建NSFetchRequest的实例,并对其进行configure且执行。总而言之,繁重的工作都替你干了,任劳任怨的程度和NSManagedObjectContext有一拼。接着展示了四种创建NSFetchRequest实例的方式: ~~~ //1 let fetchRequest1 = NSFetchRequest() let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext!) let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext!) //2 let fetchRequest2 = NSFetchRequest(entityName: "Person") //3 let fetchRequest3 = managedObjectModel.fetchRequestTemplateForName("peopleFR") //4 let fetchRequest4 = managedObjectModel.fetchRequestFromTemplateWithName("peopleFR", substitutionVariables: ["NAME" : "Ray"]) ~~~ ### **二、Introducing the Bubble Tea app** 本章要实现一个泡泡?茶的APP,作者提供了一个Start project,快速浏览一下,主界面是一个TableView,奶茶店等原始数据都保存seed.json之中。接下来要考虑如何将数据从seed中读取到Core Data的对象中去。 ### **三、Stored fetch requests** 这里可以在data model中设置一个Fetch Request,并选择Fetch哪些对象,设置一些条件;紧接着你就可以使用模板中fetchRequest了: ~~~ fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest") ~~~ 这里要使用coreData,并注意FetchRequest名称要和model中的匹配。 ### **四、Fetching different result types** 作者把NSFetchRequest比喻成了Core Data framework中的瑞士军刀,看来还是相当强大的。这里有四种fetch request的result type: ~~~ ①.NSManagedObjectResultType: Returns managed objects (default value). ②.NSCountResultType: Returns the count of the objects that match the fetch request. ③.NSDictionaryResultType: This is a catch-all return type for returning the results of different calculations. ④.NSManagedObjectIDResultType: Returns unique identifiers instead of full- fledged managed objects. ~~~ 将**fetchRequest.resultType**设为**NSCountResultType**在查询大量对象数量时可以极大的优化性能。 另一种获取count的方式是context直接调用**countForFetchRequest**方法 ~~~ let count = coreDataStack.context.countForFetchRequest(fetchRequest, error: &error) ~~~ Core Data提供了多种函数支持来支持计算,如average, sum, min and max. 下面是一个求和的例子: ~~~ func populateDealsCountLabel() { //1 指定result类型 let fetchRequest = NSFetchRequest(entityName: "Venue") fetchRequest.resultType = .DictionaryResultType //2 创建表达式描述,指定个名称,以便将来在fetch的结果(字典类型)中找到 let sumExpressionDesc = NSExpressionDescription() sumExpressionDesc.name = "sumDeals" //3 创建具体的表达式 sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments:[NSExpression(forKeyPath: "specialCount")]) sumExpressionDesc.expressionResultType = .Integer32AttributeType //4 设置fetch request 将要fetch sum fetchRequest.propertiesToFetch = [sumExpressionDesc] //5 执行,最后通过之前设置的表达式name得到最终结果 var error: NSError? let result = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [NSDictionary]? if let resultArray = result { let resultDict = resultArray[0] let numDeals: AnyObject? = resultDict["sumDeals"] numDealsLabel.text = "\(numDeals!) total deals" } else { println("Could not fetch \(error), \(error!.userInfo)") } } ~~~ 现在还剩一种.ManagedObjectIDResultType类型,他返回一个包含NSManagedObjectID对象的数组,NSManagedObjectID 可以看成是managed object通用唯一标识,除了多线程的某些场景下,否则很少用到它。 关于性能优化CoreData通过以下几种方式来削减income data: ~~~ ①.fetch batches,可以使用fetchBatchSize, fetchLimit 和 fetchOffset这些属性来削减income data。 ②.支持**faulting**这样的*占位符*。 ③.使用predicates ~~~ 最后要注意一下如果想要在运行时改变predicate,就不能再用model来初始化fetchRequest了 ~~~ override func viewDidLoad() { super.viewDidLoad() // fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest") fetchRequest = NSFetchRequest(entityName: "Venue") fetchAndReload() } ~~~ 至于predicates的用法请查看[官方文档](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html) ### **五、Sorting fetched results** 排序主要用到了NSSortDescriptor这个类,值得注意的是该排序是真正发生在**SQLite层面**的,而不是在内存中的,因此该排序十分高效。 NSSortDescriptor实例初始化需要三个参数:①.你要排序属性的key path ②.指明升序 or 降序 ③. 一个可选的selector 这里要注意一点就是CoreData不支持block-based API定义NSSortDescriptor,以及基于block-based方式定义NSPredicate。 ~~~ //下面两个初始化方法不能用于CoreData中 + (instancetype)sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending comparator:(NSComparator)cmptr + (NSPredicate *)predicateWithBlock:(BOOL (^)(id evaluatedObject, NSDictionary *bindings))block ~~~ 下面创建了一个按名字排序**filter** ~~~ lazy var nameSortDescriptor: NSSortDescriptor = { var sd = NSSortDescriptor(key: "name", ascending: true, selector: "localizedStandardCompare:") return sd }() ~~~ 这里注意一下第三个参数*localizedStandardCompare*,传入这个参数就会按本地语言排序,这也是苹果官方推荐的一种做法。 得到一个反转排序的sortDescriptor也很简单,这样就可以了 ~~~ selectedSortDescriptor = nameSortDescriptor.reversedSortDescriptor ~~~ ### **六、Asynchronous fetching** iOS 8苹果推出全新的API来处理异步fetch问题,**NSAsynchronousFetchRequest**,这里不要被他的名字迷惑了,和NSFetchRequest没什么直接关系,他其实是**NSPersistentStoreRequest**的子类。下面是该类的定义及初始化方法: ~~~ @availability(iOS, introduced=8.0) class NSAsynchronousFetchRequest : NSPersistentStoreRequest { var fetchRequest: NSFetchRequest { get } var completionBlock: NSPersistentStoreAsynchronousFetchResultCompletionBlock? { get } var estimatedResultCount: Int init(fetchRequest request: NSFetchRequest, completionBlock blk: NSPersistentStoreAsynchronousFetchResultCompletionBlock?) } ~~~ 从该类的初始化方法可以将*异步fetchRequest*看做是对fetchRequest的一种包装,第一个参数是标准的NSFetchRequest 对象,而第二个参数是一个**a completion handler**,不过仅仅有completion handler是不够的,最后还是需要*executeRequest*,下面是一个完整的例子: ~~~ //1 fetchRequest = NSFetchRequest(entityName: "Venue") //2 asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { [unowned self] (result: NSAsynchronousFetchResult! ) -> Void in self.venues = result.finalResult as [Venue] self.tableView.reloadData() } //3 var error: NSError? let results = coreDataStack.context.executeRequest(asyncFetchRequest, error: &error) if let persistentStoreResults = results { //Returns immediately, cancel here if you want } else { println("Could not fetch \(error), \(error!.userInfo)") } ~~~ 这段代码注意executeRequest一旦执行立即返回一个*NSAsynchronousFetchResult*类型的结果,在这里你不用操心fetch到的数据和UI匹配,这些工作都在第二步handle那里处理了。另外NSAsynchronousFetchResult是有cancel()方法的。 仅仅有新API还不够,还要修改context ~~~ context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) ~~~ > 另外一种*.PrivateQueueConcurrencyType*将在后面多线程介绍 因为是异步fetch,所以可能tableview初始化完毕后,fetch的结果才到,这里给数据源可以设置一个空数组来fix。 ### **七、Batch updates: no fetching required** 有的时候你需要一次更新很多属性,全部fetch到内存显然是不高效的,iOS 8推出了全新的批量更新(batch updates)来解决这一痛点。这个类就是**NSBatchUpdateRequest**,和上面提到的*NSAsynchronousFetchRequest*都是**NSPersistentStoreRequest**的子类,下面是这个类的定义: ~~~ @availability(iOS, introduced=8.0) class NSBatchUpdateRequest : NSPersistentStoreRequest { init(entityName: String) init(entity: NSEntityDescription) var entityName: String { get } var entity: NSEntityDescription { get } var predicate: NSPredicate? // Should the update include subentities? Defaults to YES. var includesSubentities: Bool // The type of result that should be returned from this request. Defaults to NSStatusOnlyResultType var resultType: NSBatchUpdateRequestResultType // Dictionary of NSPropertyDescription|property name string -> constantValue/NSExpression pairs describing the desired updates. // The expressions can be any NSExpression that evaluates to a scalar value. var propertiesToUpdate: [NSObject : AnyObject]? } ~~~ 具体使用起来也很简单: ~~~ let batchUpdate = NSBatchUpdateRequest(entityName: "Venue") batchUpdate.propertiesToUpdate = ["favorite" : NSNumber(bool: true)] batchUpdate.affectedStores = coreDataStack.psc.persistentStores batchUpdate.resultType = .UpdatedObjectsCountResultType var batchError: NSError? let batchResult = coreDataStack.context.executeRequest(batchUpdate, error: &batchError) as NSBatchUpdateResult? if let result = batchResult { println("Records updated \(result.result!)") } else { println("Could not update \(batchError), \(batchError!.userInfo)") } ~~~ 最后注意一点,就是批量更新跳过了NSManagedObjectContext,直接对persistentStore进行更新,没有经过有效性验证,这个就要靠你自己确保更新的数据合法了。
';

Core Data by tutorials 笔记(一)

最后更新于:2022-04-01 02:40:49

> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji 最近花了半个月读完了Raywenderlich家的[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials),接下来几天就做个回顾,笔记并不是对原书的简单翻译,算是对整个知识脉络的一个整理的过程吧:) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6e71b40a4f.jpg) **Update:** 上周去搞**Sketch**了,工具算是基本入门了,抄了几个图标,画了几个图,以后会再聊聊。好了,进入正题吧,今天打算介绍前三章,也是CoreData的最基础的部分。*** ## **Chapter 1:Your First Core Data App** 第一章比较简单,主要是带你熟悉CoreData的用法,并用CoreData创建了一个简单的List APP,学完这章你能加满如下技能点: * model data you want to store in Core Data using Xcode’s model editor; * add new records to Core Data; * fetch a set of records from Core Data; * display the fetched results to the user in a table view. 第一章按顺序大致如下: ### **一、Getting started** 作者提供了一个Start Project,打开其实就是一个UITableViewController,你需要做的就是增加一个**addName**方法,这里使用了*UIAlertController*这个iOS 8新添加的方法来做数据的添加逻辑,使用起来也很优雅。 ### **二、Modeling your data** 这个主要是根据你的数据模型创建对应的managed object model,主要操作也在*.xcdatamodeld*中进行,全GUI操作,没什么难度。 > You can think of a Core Data entity as a class “definition” and the managed object as an instance of that class. > 这里作者用了面向对象的思想做了类比,虽然不是很准确,但也方便初学者理解。 ### **三、Saving to Core Data** 这里要注意到NSManagedObject的“对象”其实是遵循**“KVC”**的,NSManagedObject对象的值改变后,在ManagedContext中进行save操作。 ### **四、Fetching from Core Data** 创建一个NSFetchRequest对象,设置好各种条件,依旧是交给ManagedContext对象去执行。下面是作者描述了两种*fetch*失败的情况 > If there are no objects that match the fetch request’s criteria, the method returns an optional value containing an empty array. > If an error occurred during the fetch, the method returns an optional value that contains nil. If this happens, you can inspect the NSError and respond appropriately. **第一章到此结束:)** * * * ## **Chapter 2:NSManagedObject Subclasses** NSManagedObject上一章我们提到过,要存取属性,只能用KVC,使用起来既不安全也不符合面向对象的原则,所以我们这章要生成他的子类,来创建我们自己的属性,这样就又能愉快地使用**"."**语法啦。 第二章的大致顺序如下: ### **一、Getting started** 作者提供了一个**“挑选领结”**的Start Project,领结数据存放在plist文件中,同样是一个很简单的App。 ### **二、Modeling your data** 打开xcdatamodeld文件进行编辑,添加属性。这里主要看一下**Binary Data**与**Transformable**两种类型: * **Binary Data**这里将image对象保存成了二进制文件,当然任何可以序列化的对象其实都可以保存成二进制。这里作者强调了性能问题,如果将一个很大的二进制保存到SQLite数据库中很可能会产生卡顿等问题。当然,幸运的是Core Data提供了一种叫**Allows External Storage**的解决方式,他只对binary data的属性类型有效,你只需要简单的开启他就好了。 > When you enable Allows External Storage, Core Data heuristically decides on a per-value basis if it should save the data directly in the database or store a URI that points to a separate file. * **Transformable**除了一些基本属性,如果一个对象遵循**NSCoding Protocol**,那么这个对象是可以选择使用transformable类型的,作者这里使用的是*UIColor*类型,遵循*NSSecureCoding*协议,而该协议又继承自*NSCoding*,所以设为Transformable是OK的。自定义的类想要设置为*Transformable*,那么你首先要实现**NSCoding protocol** ### **三、Managed object subclasses** 在Xcode中选择**Editor\Create NSManagedObject Subclass**创建managedObject对象的子类,这里注意下Map Model中的attribute type与实际子类对象中的属性的对应关系就好了 * String maps to String * Integer 16/32/64, Float, Double and Boolean map to NSNumber * Decimal maps to NSDecimalNumber * Date maps to NSDate * Binary data maps to NSData * Transformable maps to AnyObject 当然如果你想保留Map Model中的原始基本类型,那么在创建**NSManagedObject Subclass**时要勾选*Use scalar properties for primitive data types* > Similar to @dynamic in Objective-C, the @NSManaged attribute informs the Swift compiler that the backing store and implementation of a property will be provided at runtime instead of at compile time. > 这里注意下两种语言的一点区别 创建完子类还要记得一点就是在Model中的**Attributes inspector**将*class name*设为你新生成的子类路径,完成这一步主要是为了在runtime时将managed object subclass与Model中的entity链接起来。(有点类似与在SB中创建一个VC,并设置他的custom class)。 ### **四、Propagating a managed context** 因为Xcode默认的CoreData模板会将context在AppDelegate中创建,所以,就会有这种获取context的做法: ~~~ let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate ~~~ 但这种做法还是不推荐使用,较好的方法是能够在对象的的初始化中将context作为参数进行传递。 接下来,作者从plist中将数据通过子类对象导入到core data中,再对界面做了些操作,这都比较简单。 ### **五、Data validation in Core Data** 数据校验这里可以在data model inspector 中设置最大最小值,如果出错会在context save的时候将错误信息传给error参数 **第二章到此结束:)** * * * ## **Chapter 3:The Core Data Stack** Core Data要正常运行,需要几个组件共同协作来完成,这些组件包括: * NSManagedObjectModel * NSPersistentStore * NSPersistentStoreCoordinator * NSManagedObjectContext 这些组件共同构成了Core Data Stack,组件间的关系我觉得用苹果官方提供的一张图来展示最好不过了 > ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6e71b5f9a9.jpg) > > * An external persistent store that contains saved records. > * A persistent object store that maps between records in the store and objects in your application. > * A persistent store coordinator that aggregates all the stores. > * A managed object model that describes the entities in the stores. > * A managed object context that provides a scratch pad for managed objects. 基本Core Data整个操作都是围绕这张图来做的,本章作者给出的例子是一个记录**狗狗?散步**的APP,按照时间顺序添加散步记录。 第三章的大致顺序如下: ### **一、The managed object model** The NSManagedObjectModel 反映了APP中所有的对象类型在data model中所拥有的属性,以及对象之间的关系。作者还提到了我们用Xcode提供的visual editor创建/编辑了一个*xcdatamodel file*,然而真正在幕后的是一个叫*momc*的编译器(compiler),把model file编译后的结果放到**momd**文件夹下。Core Data可以很高效地在运行时使用*momd文件夹里编译过的内容*,来初始化一个**NSManagedObjectModel**实例。 ### **二、The persistent store** Core Data提供了四种开箱即用的NSPersistentStore存储类型,三种原子型*atomic*的,一种非原子型*non-atomic*的 > An atomic persistent store needs to be completely deserialized and loaded into memory before you can make any read or write operations. In contrast, a non- atomic persistent store can load chunks of itself onto memory as needed. * NSQLiteStoreType 依托SQLite数据库,也是唯一的非原子型的*non-atomic* * NSXMLStoreType 依托于XML文件,是原子型的*atomic* * NSBinaryStoreType 依托于二进制文件,是原子型的*atomic* * NSInMemoryStoreType 其实是存在于**内存中**的*persistent store type*,不算严格意义上的持久化存储,通常用来做单元测试和缓存。 除了上述介绍过的存储类型,作者说了,只要你的数据类型是基于JSON和CSV的格式,你还可以通过创建**NSIncrementalStore**的子类来创建自己的persistent store类型。 > [Incremental Store Programming Guide](https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/IncrementalStorePG/Introduction/Introduction.html) ### **三、The persistent store coordinator** NSPersistentStoreCoordinator可以看成是一座连接managed object model与persistent store的桥梁,他负责理解model,更好地来处理信息的存取。特别是有多个persistent stores时,persistent store coordinator相对managed context提供唯一的接口,保证了context与特定的persistent store交互。 ### **四、The managed object context** 之前的章节提到过context可以看做是内存中的scratchpad,来记录所有的managed object的行为,当然managed object所做的任何改变,在context没有save()之前,都是不会在数据库中生效的。 作者又提到了关于context的五个比较重要的特性: ~~~ ①. The context 管理着对象们的生命周期,不管这些对象是create还是fetch到的,这种对生命周期的管理在faulting、inverse、relationship handling 和 validation时很有用。 ②. A managed object不能脱离context而存在,他们是紧紧地绑在一起的。 ③. contexts都很有领土意识?,一旦一个managed object被归到某个context中去了,那么这个managed object在他整个生命周期内属于这个context了。 ④. 一个应用可以使用多个context,大多非凡的Core Data应用都这么搞。 ⑤. A context是非线程安全的,你最好不要跨线程去使用context,Apple提供了多种方式来在多线程中使用context,后面会讲到。 ~~~ ### **五、Creating your stack object** 创建自己的core data stack,其实只要记住本章开始那张图,记住要创建四个对象以及他们之间的关系,创建起来还是比较简单的。 ~~~ class CoreDataStack { let context:NSManagedObjectContext let psc:NSPersistentStoreCoordinator let model:NSManagedObjectModel let store:NSPersistentStore? } ~~~ 你要做到工作就是创建这些对象以及他们之间的关系,具体代码见苹果官方提供的[Snippet](https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/CoreDataSnippets/Articles/stack.html) 创建完毕,使用起来也相当简单,这里注意使用**Lazy**来懒加载 ~~~ lazy var coreDataStack = CoreDataStack() ~~~ ### **六、Modeling your data** 接下来就是在Xcode里创建model,这里也没什么难度。增加两个Entity,设置他们的关系为一对多,并在一对多的关系下面的Arrangement那里勾选Ordered ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6e71bc3146.jpg) ### **七、Adding managed object subclasses** 这里通过Xcode生成的子类要注意一下,刚才一对多的关系因为上图勾选了Ordered,所以这里生成了一个NSOrderedSet类型的对象。*一开始我“以为这个真是一个有序集合,CoreData会为这个集合里的子对象们排序”*,但经过和[@Kyrrr](http://www.weibo.com/u/2626996387)同学讨论试验,发现并不是这么一回事。真正的原因是**集合里的子对象们都是无序的,本来应该用数组。但数组保证不了子对象的唯一性,所以才用了集合。而使用有序集合只是为了将来使用起来索引方便,而并不是排序。**作者下面也有提到: > NSSet seems like an odd choice, doesn’t it? Unlike arrays, sets don’t allow accessing their members by index. In fact, there’s no ordering at all! Core Data uses NSSet because a set forces uniqueness in its members. The same object can’t feature more than once in a to-many relationship. 这里同样要注意的一点,还是创建完子类记得去model对应的Entity那里,Data Model inspector下填写相应的Class路径。 ### **八、A walk down persistence lane** 接着就去ViewController做一些工作来使用CoreData,完成狗狗散步的App,这一步也比较简单。 ### **九、Deleting objects from Core Data** 删除一条记录,先删除context中的相应object,保存后,从UI中删去相关条目。
';