使用swift+coredata构建简易记账软件(2) 与coredata交互

本文继续上文末尾的内容,上文地址
本文主要讲解如何在在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
水母程序员

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容