本文继续上文末尾的内容,上文地址
本文主要讲解如何在在swift中和coredata进行交互,包括:
- 提取数据(query)
- 添加数据
具体到业务逻辑上来说:
- 添加一个新的account
- 在tableview中展示account列表
本文涉及到数据库中一个基础的知识点:
- One to many field
本文主要涉及到以下两个基础coredata的类,
- NSManagedObjectContext: 一种暂存存储器,存储着父对象的所有数据。本文中默认的moc(做managed object context之意)的父对象是Persistant Store,也就是用户能接触到的coredata的最底层的永久存储。moc初始化之后可以对其进行添加,删除,query等操作,避免了对底层数据的频繁读写。
注1:对moc进行添加、删除或修改操作,并不会同时的更改其父对象的内容,需要调用save方法将操作写入
注2:moc的父对象也可以是另一个moc - NSEntityDescription:直观的理解就是数据库中的一张表
了解系统自动生成的coredata文件
上篇文章提到,使用代码生成工具一共为我们生成了6个源文件,每个entity都生成了两个,都是以
- XXX+CoreDataClass
- XXX+CoreDataProperties
的命名格式来命名的,其中第一个源文件非常简单,只是提供了一个类的定义,第二个源文件至关重要,起到了翻译我们在xcdatamodeld文件的作用
我们本章主要关注Account+CoreDataProperties这个文件
首先我们可以看到一个类方法
@nonobjc public class func fetchRequest() -> NSFetchRequest<Account> {
return NSFetchRequest<Account>(entityName: "Account")
}
这个类方法为我们提供了最简单的从数据库获取query的方式,我们先按下不表,只需知道这个函数是swift替我们生成的的一个方便函数
然后是值的定义
@NSManaged public var currency: String?
@NSManaged public var name: String?
@NSManaged public var costs: NSSet? //使用set来表示to many field
currency和name都没有问题,关键在于costs,我们在模型定义的时候把costs定义为一个to many field的关系,对应的是Cost类,相应的在Cost类中,我们有一个叫做account的to one field关系。
直观的来说即是,一个Cost只能有一个Account,我们假设一笔流水只能从一个账户扣款,那么相应的,一个账户可以在不同时间有着不同的流水,那么在swift中的体现即是一个set类型,存储了他所有的流水信息
Swift也向我们提供了管理这个one to many field的相应的函数,具体的使用的方式我们等会儿具体使用的时候来细看
extension Account {
@objc(addCostsObject:)
@NSManaged public func addToCosts(_ value: Cost)
@objc(removeCostsObject:)
@NSManaged public func removeFromCosts(_ value: Cost)
@objc(addCosts:)
@NSManaged public func addToCosts(_ values: NSSet)
@objc(removeCosts:)
@NSManaged public func removeFromCosts(_ values: NSSet)
}
StoryBoard的准备工作
基于上篇文章给出的那张界面布局图,我们今天主要在AccountListViewController和AddAccountListViewController上进行工作
首先把AccountListView中的cell类型变为subtitle
然后回到AddAccountListViewContoller,以下的截图我标注了在我代码中出现的所有的控件的名称,以方便读者阅读
控件以及其对应的outlet的名称
我们首先忽视那个initValue的文本框,首先考虑在点击button之后,我们可以往coredata写入一个有相应名称和币种的account
配置currencyPicker
我们希望currencyPicker可以显示三个币种,cny,eur和usd,为此我们首先要将class符合其对应的protocol,分别需要满足Delegate和DataSource
class AddAccountViewController: UIViewController, UIPickerViewDelegate,UIPickerViewDataSource {}
定义一个类成员来存储三个币种
var currencyArray = ["CNY","EUR","USD"]
然后配置好所有的delegate
// MARK: Picker view delgate
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return currencyArray[row] //返回某一列某一行对应的字符串,这里我们只有一列,所以直接按下标取数组值
}
// MARK: Picker view data source
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1 //我们只希望picker显示一列
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return currencyArray.count //这里返回的是pick中某一列的元素的个数,因为我们只有一列,所以可以直接返回这一列的元素的个数
}
配置好这些运行程序,应该可以看到
显示币种的currencyPicker
配置Button
按住control把button拉入class,然后添加一个action,我们希望点击button的时候
- 新建一个Account
- 填上这个account所对应的的名称和币种
- 存进数据库
获取存储“路径”
我们并不能直接和底层的数据库进行通信,我们使用NSManagedObjectContext类作为一个中间件,直观的说他存储了一份副本,使得我们可以直接在中间件上操作而避免频繁和数据库进行读写操作
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let moc = appDelegate.persistentContainer.viewContext
直接使用在appdelegate中附送的persistentContainer来初始化我们的moc(对于moc更深层次的讨论请移步官方文档)
添加Account
let account = NSEntityDescription.insertNewObject(forEntityName: "Account", into: moc) as! Account
我们使用NSEntityDescription类的类方法在moc中创建了一个空白的Account对象,注意,此时我们只更改了这个moc,而没有更改其对应的数据库
account.setValue(accountNameTextField.text!, forKey: "name") //我们忽视名称的合法性检查
account.setValue(currencyArray[currencyPicker.selectedRow(inComponent: 0)], forKey: "currency")
然后我们使用setValue方法来填入对应的数据,此处我们忽略了所有该做的检查
存储
我们需要使用moc的save方法来把moc中的改变写入他所对应的persistentstore (此处我们不讨论moc的parent store的其他情况)
do {
try moc.save()
} catch {
fatalError("Faillure to save context: \(error)")
}
至此我们已经完成了一个account的添加,我们需要对AccountListViewController做一定配置使得我们可以看到刚才添加的account
配置Table View
本段主要讲述了如何从coredata中获取数据
首先我们在AccountListViewController声明了一个lazy的变量moc,作为之后我们要用的moc的变量
lazy var moc : NSManagedObjectContext = {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}()
同时也声明一个account的数组,作为table view的数据的载体
var accounts = [Account].init()
最后配置viewWillAppear方法
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//此处调用了Account的类方法,返回了一个我们需要用到的fetchRequest
let fetchRequest:NSFetchRequest<Account> = Account.fetchRequest()
do{
let fetchResults = try moc.fetch(fetchRequest)
self.accounts = fetchResults
tableView.reloadData()
} catch {
print("Fetch failed \(error)")
}
}
最后配置一下tableview的delegate
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return accounts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell"){
cell.textLabel?.text = accounts[indexPath.row].name
return cell
}
let cell = UITableViewCell.init(style: .subtitle, reuseIdentifier: "accountCell")
cell.textLabel?.text = accounts[indexPath.row].name
return cell
}
运行程序,一切正常的话就可以在tableview中看到我们刚才添加的Account了,如下图所示
table view的预览
给Account初始值
重新回到AddAccountViewController的页面,这次我们希望可以在创建account的时候为account给定一个初始的值
准备工作
首先我们需要新建一个swift文件,命名为Account+CustomExtension,这个文件中我们会自定义一些Account类中额外需要的方法和数值变量
extension Account{
var net : Double {
var net = 0.0
if let costs = self.costs as! Set<Cost>?{
for cost in costs{
net += cost.amount
}
}
return net
}}
注:这里获得account剩余资金的方法采用了一个较为直观但是对性能开销比较大的方法,每次希望获取net的时候,都会遍历account旗下的所有流水,求和之后返回,这种方法在流水数目大的时候会有很大的性能开销,如有机会,在之后的文章中会考虑解决这个问题,这里我们希望尽可能的简化所有模型。
基于这个获取剩余资金的方式,我们在创建一个账户的时候,考虑同时生成一笔流水,流水的数额就是在当前页面填入的金额,我们希望在category中把这笔流水分类为initValue,这就要求在程序启动时,数据库中就已经有initValue作为category的一条数据,我们在appdelegate中写入如下程序:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//检查是否是第一次启动,如果是的话,往数据库里写一些默认数据
if !UserDefaults.standard.bool(forKey: "firstlaunch") {
let moc = persistentContainer.viewContext
let initValueCategory = NSEntityDescription.insertNewObject(forEntityName: "Category", into: moc) as! Category
//我们不希望这个类别在之后添加流水的时候出现,所以我们额外设置了一个bool来告诉那时候的controller不让他出现
initValueCategory.setValue(true, forKey: "isHidden")
initValueCategory.setValue("initValue", forKey: "name")
do{
try moc.save()
} catch{
fatalError("Init db error due to \(error)")
}
UserDefaults.standard.set(true, forKey: "firstlaunch")
}
return true
}
上述代码会在程序启动时往数据库里插入一个名称为initValue的Category的信息,并且通过检查UserDefault来保证这段代码不会被执行多次
添加初值
回到我们AddAccountViewController中的submitButtonPressed函数中,现在完整的代码应该如下:
@IBAction func submitButtonPressed(_ sender: Any) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let moc = appDelegate.persistentContainer.viewContext
//插入一个新的Account对象
let account = NSEntityDescription.insertNewObject(forEntityName: "Account", into: moc) as! Account
account.setValue(accountNameTextField.text!, forKey: "name")
account.setValue(currencyArray[currencyPicker.selectedRow(inComponent: 0)], forKey: "currency")
//插入一个新的Cost对象
let cost = NSEntityDescription.insertNewObject(forEntityName: "Cost", into: moc) as! Cost
cost.setValue(Double(initValueTextField.text!), forKey: "amount")
cost.setValue(NSDate.init(), forKey: "createDate")
//使用addToCosts方法建立account和cost之间的联系
account.addToCosts(cost)
//使用Nspredicate类来充当filter,获取到名称为initValue的类
let predicate = NSPredicate.init(format: "name like 'initValue'");
let fetchRequest : NSFetchRequest<Category> = Category.fetchRequest()
fetchRequest.predicate = predicate
do {
let fetchResult = try moc.fetch(fetchRequest)
fetchResult.first?.addToCosts(cost)
} catch{
fatalError("Failure to init value due to \(error)")
}
//In this case, fetchResult should only have one result
do {
try moc.save()
} catch {
fatalError("Faillure to save context: \(error)")
}
self.navigationController?.popViewController(animated: true)
}
上述代码中使用addToCosts方法来建立one to many field的联系,实际使用中切勿直接更改cost中的category和account的值,不然会造成不可预料的后果。请一直使用addToCosts或者removeFromCosts来处理两者之间的联系
注1:在coredata中使用了nspredicate类来充当了sql的查询语句。具体的使用方法请参考官方文档 Predicate Programing Guide
注2:在xcdatamodeld中可以设置这些field的delete rule,默认是nullify,也就是说,如果一个cost对应的category被删除,那么这个cost的category会被重置为nil,如果希望这个cost同时被删除,请选择cascade
更新AccountListView的代码
这一块很简单,只需要加上一行代码
cell.detailTextLabel?.text = "\(accounts[indexPath.row].net) \(accounts[indexPath.row].currency ?? "CNY")"
就可以在对应的subtitle的位置显示出余额了,最后的效果应该如下图所示
更改后的table view界面
下期展望
account的事情做完,下篇文章将会着眼于如何添加一个流水,不过有了这篇文章的基础,添加流水无非就是照葫芦画瓢而已
2018.08.15
水母程序员
网友评论