iOS开发实战-时光记账Demo 本地数据库版

liyy3867 7年前
   <p>现在记账APP也是用途比较广泛</p>    <p>自己写了个简单的demo 欢迎指正</p>    <h2>效果</h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a3dd7f67e7c360651ce5e7c87fa6088e.gif"></p>    <p>效果.gif</p>    <h2>分析</h2>    <h2>1.思维推导</h2>    <p>首先简单的做了下思维推导</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3119739567b9013d06a3d8edde7cb60b.png"></p>    <p>思维推导</p>    <h2>2.文件结构</h2>    <p>大致框架想好后就可以着手开始准备了</p>    <ul>     <li>数据库管理:coreData</li>     <li>视图管理:navigationcontroller<br> 暂时没有使用cocoapods导入第三方的数据库管理框架<br> 简单的coreData完全可以胜任<br> 说白了就两个页面 主界面 和 记账界面</li>    </ul>    <p>这是完成时的文件结构</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/de0951334c830f4900e5fbfa9b5730fd.jpg"></p>    <p>文件结构</p>    <h2>3.数据库设计</h2>    <ul>     <li>Tally 账单表</li>    </ul>    <ul>     <li>identity :String 唯一标识</li>     <li>expenses :double 支出</li>     <li>income :double 收入</li>     <li>timestamp :date 时间戳</li>     <li>关系      <ul>       <li>与TallyDate 日期表:1V1</li>       <li>与TallyType 类型表:1V1</li>      </ul> </li>    </ul>    <p style="text-align:center"><br> <img src="https://simg.open-open.com/show/b854ae4c1dda95cf9e8be7fb9772484c.png"></p>    <p style="text-align:left">账单表</p>    <ul>     <li>TallyDate 日期表</li>    </ul>    <ul>     <li>date :string 日期</li>     <li>关系<br> -与Tally 账单表:1VN</li>    </ul>    <p style="text-align:center"><br> <img src="https://simg.open-open.com/show/7b2dbec86791b9f75842a1d949991a8a.png"></p>    <p style="text-align:left">日期表</p>    <ul>     <li>TallyType 类型表</li>    </ul>    <ul>     <li>typename :string 类型名</li>     <li>typeicon :string 类型图片标</li>     <li>关系<br> -与Tally 账单表:1VN</li>    </ul>    <p style="text-align:center"><br> <img src="https://simg.open-open.com/show/be36c7601e5694f0c16bc5520f97f858.png"></p>    <p>类型表</p>    <h2>4.页面编写</h2>    <h3>增加账单页面</h3>    <p>由于主页只是一个展示的时光轴界面,UIScrollView加几个按钮就能完成,需要读取数据库内容,所以我们先把内页-增加账单 完成。</p>    <ul>     <li>view      <ul>       <li>UICollectionView展示账单类型</li>       <li>自定义View计算器界面计算存储结果</li>      </ul> </li>     <li>model      <ul>       <li>UICollectionViewCell模型 使用了plist和KVC转字典</li>      </ul> </li>     <li>controller      <ul>       <li>负责添加 两个view 及处理两个view的代理</li>      </ul> </li>    </ul>    <h3>增加账单部分代码</h3>    <ul>     <li>model</li>    </ul>    <pre>  <code class="language-objectivec">#import <Foundation/Foundation.h>    @interface TallyListCellModel : NSObject  @property (nonatomic,copy)NSString *tallyCellImage;  @property (nonatomic,copy)NSString *tallyCellName;    - (instancetype)initWithDict:(NSDictionary*)dict;  + (instancetype)tallyListCellModelWithDict:(NSDictionary*)dict;    @end</code></pre>    <pre>  <code class="language-objectivec">#import "TallyListCellModel.h"  @implementation TallyListCellModel  - (instancetype)initWithDict:(NSDictionary*)dict {      self = [super init];      if (self) {          [self setValuesForKeysWithDictionary:dict];      }      return self;  }  + (instancetype)tallyListCellModelWithDict:(NSDictionary*)dict {      return [[TallyListCellModel alloc] initWithDict:dict];  }  @end</code></pre>    <ul>     <li>计算界面</li>    </ul>    <pre>  <code class="language-objectivec">#import <UIKit/UIKit.h>  #import "AppDelegate.h"  #import <CoreData/CoreData.h>  #import "TimeTallyDemo+CoreDataModel.h"  typedef void(^PositionInViewBlock)(CGPoint point);      @protocol CalculatorViewDelegate <NSObject>    //保存成功  - (void)tallySaveCompleted;    //保存失败  - (void)tallySaveFailed;    //回到原来的位置  - (void)backPositionWithAnimation;    @end    @interface CalculatorView : UIView  //类型图片的size  @property(nonatomic,assign)CGSize imageViewSize;  //类型图  @property(nonatomic,strong)UIImage *image;  //类型名  @property(nonatomic,strong)NSString *typeName;  //账单是否存在 判断 是修改还是新增  @property(nonatomic,assign)BOOL isTallyExist;  @property(nonatomic,strong)id<CalculatorViewDelegate> delegate;  //回调image在整个view中的位置  @property(nonatomic,copy)PositionInViewBlock positionInViewBlock;  //修改账单界面进入时传入参数  - (void)modifyTallyWithIdentity:(NSString *)identity;  @end</code></pre>    <pre>  <code class="language-objectivec">#import "CalculatorView.h"    @interface CalculatorView()  @property (nonatomic,assign)CGFloat btnWidth;               //btn宽  @property (nonatomic,assign)CGFloat btnHeight;              //btn高  @property (nonatomic,copy)NSString *nValue;                 //当前输入值  @property (nonatomic,copy)NSString *resutlStr;              //结果值  @property (nonatomic,strong)UILabel *resultLab;             //结果栏  @property (nonatomic,strong)UIButton *addBtn;               //+  @property (nonatomic,strong)UIColor *btnColor;              //按钮颜色  @property (nonatomic,assign)CGColorRef boardLineColor;      //边框线条颜色  @property (nonatomic,strong)UIImageView *imageView;         //tally类型图  @property(nonatomic,strong)UILabel *typeLab;  @property(nonatomic,copy)NSString *tallyIdentity;  @end  static CGFloat const kBoardWidth = 1;  static CGFloat const kMaxCalCount = 9;  @implementation CalculatorView    - (instancetype)initWithFrame:(CGRect)frame {      self = [super initWithFrame:frame];      if (self) {          self.backgroundColor = [UIColor whiteColor];          self.btnWidth = (self.frame.size.width-kBoardWidth/2)/4;          self.btnHeight = self.frame.size.height/5;          self.nValue = @"";          self.resutlStr = @"";          self.typeLab.text = @"";          self.btnColor = [UIColor grayColor];          self.boardLineColor = [UIColor lightGrayColor].CGColor;          self.layer.borderColor = self.boardLineColor;          self.layer.borderWidth = kBoardWidth;          self.imageView = [[UIImageView alloc] init];          [self addSubview:self.imageView];          [self loadBtn];      }      return self;  }    - (void)setImage:(UIImage *)image {      _image = image;        [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {          _imageView.image = image;      }];    }    - (void)setTypeName:(NSString *)typeName {      _typeName = typeName;      self.typeLab.text = typeName;  }    - (void)setImageViewSize:(CGSize)imageViewSize {      _imageViewSize = imageViewSize;      _imageView.frame = CGRectMake(10, (self.btnHeight-imageViewSize.height)/2, imageViewSize.width, imageViewSize.height);      _imageView.backgroundColor = [UIColor clearColor];      //回调实际位置      if (_positionInViewBlock) {          CGPoint point = CGPointMake(10+imageViewSize.width/2, self.frame.origin.y + _imageView.frame.origin.y + imageViewSize.height/2) ;          _positionInViewBlock(point);      }      self.typeLab.frame = CGRectMake(self.imageView.frame.origin.x + self.imageView.frame.size.width + 10, self.imageView.frame.origin.y, imageViewSize.width * 2, imageViewSize.height);  }    //类型名称lab  - (UILabel *)typeLab {      if (!_typeLab) {          _typeLab = [[UILabel alloc] init];          _typeLab.font = [UIFont systemFontOfSize:14];          [self addSubview:_typeLab];      }      return _typeLab;  }    //结果lab  - (UILabel *)resultLab {      if (!_resultLab) {          _resultLab = [[UILabel alloc] initWithFrame:CGRectMake(self.frame.size.width * 0.3, 0, self.frame.size.width * 0.7-10, self.btnHeight)];          _resultLab.text = @"¥ 0.00";          _resultLab.textAlignment = NSTextAlignmentRight;          _resultLab.adjustsFontSizeToFitWidth = YES;          _resultLab.font = [UIFont systemFontOfSize:25];          _resultLab.userInteractionEnabled = YES;          UITapGestureRecognizer *tag = [[UITapGestureRecognizer alloc] init];          tag.numberOfTapsRequired = 1;          [tag addTarget:self action:@selector(clickResultLab)];          [_resultLab addGestureRecognizer:tag];      }      return _resultLab;  }    - (void)clickResultLab {      if ([self.delegate respondsToSelector:@selector(backPositionWithAnimation)]) {          [self.delegate backPositionWithAnimation];      }  }  //加号  - (UIButton *)addBtn {      if (!_addBtn) {          ...          [_addBtn addTarget:self action:@selector(clickAdd) forControlEvents:UIControlEventTouchUpInside];        }      return _addBtn;  }    //普通数字btn  - (void)loadBtn {      // 1 - 9 btn      ...      [btn addTarget:self action:@selector(clickNumber:) forControlEvents:UIControlEventTouchUpInside];        //0 btn      ...      [zeroBtn addTarget:self action:@selector(clickNumber:) forControlEvents:UIControlEventTouchUpInside];        //小数点 btn      ...      pointBtn.tag = 99;      [pointBtn addTarget:self action:@selector(clickNumber:)         //重置 btn      ...      [resetBtn addTarget:self action:@selector(resetZero) forControlEvents:UIControlEventTouchUpInside];          //DEL btn      ...      [delBtn addTarget:self action:@selector(clickDel) forControlEvents:UIControlEventTouchUpInside];        //ok btn      ...      [okBtn addTarget:self action:@selector(clickOk) forControlEvents:UIControlEventTouchUpInside];        [self addSubview:self.addBtn];      [self addSubview:self.resultLab];    }    //点击数字按键  - (void)clickNumber:(UIButton*)btn {        if(self.addBtn.isSelected){          self.nValue = @"";      }      NSString *currentValue = @"";      if (btn.tag == 99) {          //有 . 就不加 .          if ([self.nValue rangeOfString:@"."].location == NSNotFound) {              currentValue = @".";          }      }else {          currentValue = [NSString stringWithFormat:@"%ld",(long)btn.tag];      }        self.nValue = [self.nValue stringByAppendingString:currentValue];          //保留小数点后两位      NSRange pointRange = [self.nValue rangeOfString:@"."];      if (pointRange.location != NSNotFound) {          if ([self.nValue substringFromIndex:pointRange.location+1].length > 2) {              self.nValue = [self.nValue substringWithRange:NSMakeRange(0, pointRange.location + 3)];          }            //总位数不超过9 处理小数部分          if ([self.nValue substringToIndex:pointRange.location].length > kMaxCalCount) {              self.nValue = [NSString stringWithFormat:@"%0.2f",[self.nValue doubleValue]];              self.nValue = [self.nValue substringToIndex:kMaxCalCount+3];          }        } else {          //总位数不超过9 整数部分          if (self.nValue.length > kMaxCalCount) {              self.nValue = [NSString stringWithFormat:@"%0.2f",[self.nValue doubleValue]];              self.nValue = [self.nValue substringToIndex:kMaxCalCount];          }      }        //显示数字      self.resultLab.text = [NSString stringWithFormat:@"¥ %.2f",[self.nValue doubleValue]];      self.addBtn.selected = NO;        NSLog(@"new = %@",self.nValue);  }    //单击加号  - (void)clickAdd {      //显示结果  点击后nValue清零      if (!self.addBtn.isSelected) {          self.addBtn.selected = YES;          double result = [self.resutlStr doubleValue] + [self.nValue doubleValue] ;          self.resutlStr = [NSString stringWithFormat:@"%.2f",result];          self.resultLab.text = [NSString stringWithFormat:@"¥ %.2f",[self.resutlStr doubleValue]];          NSLog(@"resutl = %@",self.resutlStr);      }    }    //重置0  - (void)resetZero {      self.resutlStr = @"";      self.nValue = @"";      self.resultLab.text = @"¥ 0.00";  }    //退格  - (void)clickDel {      if (self.nValue.length > 0) {          self.nValue = [self.nValue substringWithRange:NSMakeRange(0, self.nValue.length-1)];      }      NSLog(@"-----%@",self.nValue);      self.resultLab.text = [NSString stringWithFormat:@"¥ %.2f",[self.nValue doubleValue]];  }  //完成  - (void)clickOk {      //存在 使用修改保存方式 不存在 使用新增保存方式      if (self.isTallyExist) {          [self modifyTallySavedWithIdentity:self.tallyIdentity];      } else {          [self addTallySave];      }  }    //增加账单保存  - (void)addTallySave {      if ( ![self.typeLab.text isEqualToString:@""] && [self.nValue doubleValue] != 0) {          [self clickAdd];          //存数据          NSManagedObjectContext *managedObjectContext = ((AppDelegate *)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;            NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];          [dateFormatter setDateFormat:@"yyyy-MM-dd"];          NSString *dateString = [dateFormatter stringFromDate:[NSDate date]];          //查询有无对应的date 有则使用无则创建          NSFetchRequest *fdate = [TallyDate fetchRequest];          NSArray<NSSortDescriptor *> *sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"date" ascending:YES]];          fdate.sortDescriptors = sortDescriptors;          NSPredicate *p = [NSPredicate predicateWithFormat:@"date = %@",dateString];          fdate.predicate = p;          NSArray<TallyDate *> *ss = [managedObjectContext executeFetchRequest:fdate error:nil];          TallyDate *date;          if (ss.count > 0) {              date = ss[0];          } else {              date = [[TallyDate alloc] initWithContext:managedObjectContext];              date.date = dateString;          }          //配置数据          Tally *model = [[Tally alloc] initWithContext:managedObjectContext];          NSFetchRequest *ftype = [TallyType fetchRequest];          NSPredicate *ptype = [NSPredicate predicateWithFormat:@"typename = %@",self.typeLab.text];          ftype.predicate = ptype;          NSArray<TallyType *> *sstype = [managedObjectContext executeFetchRequest:ftype error:nil];          TallyType *type = [sstype firstObject];          //给关系赋值          model.typeship = type;          model.dateship = date;          model.identity = [NSString stringWithFormat:@"%@", [model objectID]];          model.timestamp = [NSDate date];          if ([self.typeLab.text isEqualToString:@"工资"]) {              model.income = [self.resutlStr doubleValue];              model.expenses = 0;          } else {              model.expenses = [self.resutlStr doubleValue];              model.income = 0;          }          //存          [managedObjectContext save:nil];          if ([self.delegate respondsToSelector:@selector(tallySaveCompleted)]) {              [self.delegate tallySaveCompleted];          }      } else {          if ([self.delegate respondsToSelector:@selector(tallySaveFailed)]) {              [self.delegate tallySaveFailed];          }          NSLog(@"不存");      }  }    //修改账单保存  - (void)modifyTallySavedWithIdentity:(NSString *)identity {      [self clickAdd];         if ([self.resutlStr doubleValue] == 0) {          if ([self.delegate respondsToSelector:@selector(tallySaveFailed)]) {              [self.delegate tallySaveFailed];          }          NSLog(@"不存");          return;      }      self.addBtn.selected = NO;      NSManagedObjectContext *managedObjectContext = ((AppDelegate *)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;      //设置账单类型      NSFetchRequest *ftype = [TallyType fetchRequest];      NSPredicate *ptype = [NSPredicate predicateWithFormat:@"typename = %@",self.typeLab.text];      ftype.predicate = ptype;      NSArray<TallyType *> *sstype = [managedObjectContext executeFetchRequest:ftype error:nil];      TallyType *type = [sstype firstObject];      //找出当前账单      NSFetchRequest *fetchRequest = [Tally fetchRequest];      NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identity = %@", identity];      [fetchRequest setPredicate:predicate];      NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:nil];      //配置当前账单      Tally *tally = [fetchedObjects firstObject];      tally.typeship = type;      if ([self.typeLab.text isEqualToString:@"工资"]) {          tally.income = [self.resutlStr doubleValue];          tally.expenses = 0;      } else {          tally.expenses = [self.resutlStr doubleValue];          tally.income = 0;      }      [managedObjectContext save:nil];      if ([self.delegate respondsToSelector:@selector(tallySaveCompleted)]) {          [self.delegate tallySaveCompleted];      }  }      //修改界面传值  - (void)modifyTallyWithIdentity:(NSString *)identity {      self.tallyIdentity = identity;      NSManagedObjectContext *managedObjectContext = ((AppDelegate *)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;      NSFetchRequest *fetchRequest = [Tally fetchRequest];      NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identity = %@", identity];      [fetchRequest setPredicate:predicate];      NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:nil];      Tally *tally = [fetchedObjects firstObject];      self.imageView.image = [UIImage imageNamed:tally.typeship.typeicon];      self.typeLab.text = tally.typeship.typename;      self.nValue = tally.income > 0?[NSString stringWithFormat:@"%@",@(tally.income)]:[NSString stringWithFormat:@"%@",@(tally.expenses)];      self.resultLab.text = [NSString stringWithFormat:@"¥ %.2f",[self.nValue doubleValue]];      self.isTallyExist = YES;  }    @end</code></pre>    <ul>     <li>类型选择界面 自定义cell就不贴出来了 就一个图片和label</li>    </ul>    <pre>  <code class="language-objectivec">#import <UIKit/UIKit.h>  #import "TallyListCell.h"  #import <CoreData/CoreData.h>  #import "TimeTallyDemo+CoreDataModel.h"  #import "AppDelegate.h"    @protocol TallyListViewDelegate <NSObject>  //选择对应cell 传递 image title cell的实际位置  - (void)didSelectItem:(UIImage*)cellImage andTitle:(NSString*)title withRectInCollection:(CGRect)itemRect;  //滚动到底  - (void)listScrollToBottom;  @end    @interface TallyListView : UICollectionView  @property(nonatomic,strong)id<TallyListViewDelegate> customDelegate;  @end</code></pre>    <pre>  <code class="language-objectivec">#import "TallyListView.h"  @interface TallyListView()<UICollectionViewDelegate,UICollectionViewDataSource>    @property (nonatomic, strong) NSArray *tallyListArray;  @property (nonatomic, assign) CGFloat offsety;    @end  static NSString *cellId = @"tallyListCellID";  @implementation TallyListView    //读取plist数据  - (NSArray *)tallyListArray {      if (!_tallyListArray) {          NSMutableArray *res = [NSMutableArray array];          NSString *path = [[NSBundle mainBundle] pathForResource:@"TallyList" ofType:@"plist"];          NSArray *list = [NSArray arrayWithContentsOfFile:path];          for (NSDictionary *dict in list) {              TallyListCellModel *model = [TallyListCellModel tallyListCellModelWithDict:dict];              [res addObject:model];          }          _tallyListArray = [NSArray arrayWithArray:res];          [self writeToSqlite];      }      return  _tallyListArray;  }    - (void)writeToSqlite {        //将类型名字和图片信息写入数据库      NSManagedObjectContext *managedObjectContext = ((AppDelegate*)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;      for (TallyListCellModel *model in self.tallyListArray) {            //查询有无对应的type 有则使用无则创建          NSFetchRequest *fdate = [TallyType fetchRequest];          NSPredicate *p = [NSPredicate predicateWithFormat:@"typename = %@",model.tallyCellName];          fdate.predicate = p;          NSArray<TallyType *> *ss = [managedObjectContext executeFetchRequest:fdate error:nil];          if (ss.count == 0) {              TallyType *type = [[TallyType alloc] initWithContext:managedObjectContext];              type.typename = model.tallyCellName;              type.typeicon = model.tallyCellImage;              [managedObjectContext save:nil];          }        }  }    //初始化  - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {      self = [super initWithFrame:frame collectionViewLayout:layout];      if (self) {          self.delegate = self;          self.dataSource = self;          self.backgroundColor = [UIColor clearColor];          [self registerNib:[UINib nibWithNibName:@"TallyListCell" bundle:nil] forCellWithReuseIdentifier:cellId];           }      return self;  }      #pragma mark - UICollectionViewDelegate & UICollectionViewDataSource  - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {      return self.tallyListArray.count;  }    - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{      TallyListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellId forIndexPath:indexPath];      cell.cellModel = self.tallyListArray[indexPath.item];      return cell;  }      - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {      TallyListCell *cell = (TallyListCell*)[collectionView cellForItemAtIndexPath:indexPath];      //cell在collectionView的位置      CGRect cellRect = [collectionView convertRect:cell.frame fromView:collectionView];      //image在cell中的位置      CGRect imgInCellRect = cell.imageView.frame;      CGFloat x = cellRect.origin.x + imgInCellRect.origin.x;      CGFloat y = cellRect.origin.y + imgInCellRect.origin.y + 64 - self.offsety;      //图片在collectionView的位置      CGRect imgRect = CGRectMake(x, y, imgInCellRect.size.width, imgInCellRect.size.height);      //回调      if ([self.customDelegate respondsToSelector:@selector(didSelectItem:andTitle:withRectInCollection:)]){          [self.customDelegate didSelectItem:cell.imageView.image andTitle:cell.imageLab.text withRectInCollection:imgRect];      }    }    //外部调用 用于修改账单传值  - (void)scrollViewDidScroll:(UIScrollView *)scrollView {      self.offsety = scrollView.contentOffset.y;  }    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{      CGFloat bottomY = self.contentSize.height - self.frame.size.height;      if (scrollView.contentOffset.y >= bottomY) {          NSLog(@"计算器下去");          if ([self.customDelegate respondsToSelector:@selector(listScrollToBottom)]) {              [self.customDelegate listScrollToBottom];          }      }  }  @end</code></pre>    <h3>主页面界面</h3>    <ul>     <li>model      <ul>       <li>用于传递给账单界面的数据模型</li>      </ul> </li>     <li>view      <ul>       <li>时间线绘图</li>      </ul> </li>     <li>controller      <ul>       <li>处理时间线视图的删改查</li>      </ul> </li>    </ul>    <h3>主界面部分代码</h3>    <ul>     <li>model</li>    </ul>    <pre>  <code class="language-objectivec">#import <Foundation/Foundation.h>    typedef enum : NSUInteger {      TallyMoneyTypeIn = 0,      TallyMoneyTypeOut,    } TallyMoneyType;    @interface TimeLineModel : NSObject  @property(nonatomic,copy)NSString *tallyIconName;  @property(nonatomic,assign)double tallyMoney;  @property(nonatomic,assign)TallyMoneyType tallyMoneyType;  @property(nonatomic,copy)NSString *tallyDate;  @property(nonatomic,copy)NSString *tallyType;  @property(nonatomic,copy)NSString *identity;  @property(nonatomic,assign)double income;  @property(nonatomic,assign)double expense;    @end</code></pre>    <ul>     <li> <p>时间线视图</p> <p>这里用runtime方法为uibutton分类给时间线上的btn添加了两个属性</p> <p>keyWithBtn :用于存储日期</p> <p>panelBtnType :用于存储按钮是修改还是删除</p> </li>    </ul>    <pre>  <code class="language-objectivec">typedef enum : NSUInteger {      PanelViewBtnTypeLeft = 0,      PanelViewBtnTypeRight,  } PanelViewBtnType;  //使用runtime 给uibutton扩展 属性  @interface UIButton (BtnWithKey)  @property(nonatomic,copy)NSString *keyWithBtn;  @property(nonatomic,assign)PanelViewBtnType panelBtnType;  @end  static const void *kKeyWithBtn = @"keyWithBtn";  static const void *kPanelBtnType = @"panelBtnType";  @implementation UIButton (BtnWithKey)    - (NSString *)keyWithBtn {      return objc_getAssociatedObject(self, kKeyWithBtn);    }  - (void)setKeyWithBtn:(NSString *)keyWithBtn {      objc_setAssociatedObject(self, kKeyWithBtn, keyWithBtn, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  }    - (PanelViewBtnType)panelBtnType {      return [objc_getAssociatedObject(self, kPanelBtnType) intValue];  }    - (void)setPanelBtnType:(PanelViewBtnType)panelBtnType {      objc_setAssociatedObject(self, kPanelBtnType, [NSNumber numberWithUnsignedInteger:panelBtnType], OBJC_ASSOCIATION_RETAIN_NONATOMIC);  }  @end</code></pre>    <pre>  <code class="language-objectivec">#import "TimeLineView.h"    @interface TimeLineView()  @property(nonatomic,strong)UIView *panelView;       //删除、修改 面板  @end      @implementation TimeLineView    - (instancetype)initWithFrame:(CGRect)frame {      self = [super initWithFrame:frame];      if (self) {      }      return self;  }      - (void)drawRect:(CGRect)rect {        CGContextRef context = UIGraphicsGetCurrentContext();      int keyIndex = 0;      CGFloat aDateAllLine = 0;      for (NSString *key  in self.timeLineModelsDict.allKeys) {          //读取字典对应key的数组          NSArray<TimeLineModel*> *modelArray = self.timeLineModelsDict[key];          //画线画日期          CGRect dateRect = CGRectMake(self.center.x-kDateWidth/2, aDateAllLine, kDateWidth, kDateWidth);          CGContextAddEllipseInRect(context, dateRect);          CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);          CGContextFillPath(context);          CGContextStrokePath(context);          CGRect dateLabRect = CGRectMake(0, aDateAllLine, self.frame.size.width/2-kBtnWidth, kBtnWidth/2);          //日期lab          UILabel *dateLab = [[UILabel alloc] initWithFrame:dateLabRect];          dateLab.textAlignment = NSTextAlignmentRight;          dateLab.text = key;          [dateLab setTextColor:[UIColor blueColor]];          [self addSubview:dateLab];            for (int i = 0 ; i < modelArray.count; i++) {              //画竖线              CGFloat start = aDateAllLine + kDateWidth + i * (kLineHeight+kBtnWidth);              CGFloat end = aDateAllLine + kDateWidth+kLineHeight + i * (kLineHeight+kBtnWidth);              CGContextMoveToPoint(context, self.center.x, start);              CGContextAddLineToPoint(context, self.center.x, end);              CGContextSetLineWidth(context, kLineWidth);              CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);              CGContextStrokePath(context);                //收支类型btn              CGRect btnRect = CGRectMake(self.center.x-kBtnWidth/2, end, kBtnWidth, kBtnWidth);              UIButton *btn = [[UIButton alloc] initWithFrame:btnRect];              btn.tag = i;              btn.keyWithBtn = key;              [btn setImage:[UIImage imageNamed:modelArray[i].tallyIconName] forState:UIControlStateNormal];              btn.layer.masksToBounds = YES;              [btn addTarget:self action:@selector(clickTallyTypeBtn:) forControlEvents:UIControlEventTouchUpInside];              [self addSubview:btn];                //收支情况              CGFloat labX = modelArray[i].tallyMoneyType == TallyMoneyTypeIn ? 0:self.center.x + kBtnWidth;              CGFloat labY = btnRect.origin.y;              CGFloat labWidth = self.frame.size.width/2 - kBtnWidth ;              CGFloat labHeight = btnRect.size.height;              UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(labX, labY, labWidth, labHeight)];              label.textAlignment = modelArray[i].tallyMoneyType == TallyMoneyTypeIn ? NSTextAlignmentRight:NSTextAlignmentLeft;              label.text = [NSString stringWithFormat:@"%@ %0.2f",modelArray[i].tallyType,modelArray[i].tallyMoney];              [self addSubview:label];                //最后一条线 最后一条账单不画此线              if (keyIndex < self.timeLineModelsDict.allKeys.count) {                  CGFloat lastStart = aDateAllLine;                  CGFloat lastEnd = kLineHeight;                  CGContextMoveToPoint(context, self.center.x, lastStart);                  CGContextAddLineToPoint(context, self.center.x, lastEnd);                  CGContextSetLineWidth(context, kLineWidth);                  CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);                  CGContextStrokePath(context);              }          }            //当前时间线总长          aDateAllLine = aDateAllLine + kDateWidth + (kBtnWidth+kLineHeight)*modelArray.count + kLineHeight;          keyIndex++;        }      }    //刷新view时清空UI  - (void)setNeedsDisplay {      [super setNeedsDisplay];      for (UIView *view in self.subviews) {          [view removeFromSuperview];      }  }    //点击账单类型按钮  - (void)clickTallyTypeBtn:(UIButton *)btn {      NSLog(@"%@",btn.keyWithBtn);      //清空控制板      if (self.panelView) {          [self.panelView removeFromSuperview];      }      //控制板出现      self.panelView = [[UIView alloc] initWithFrame:CGRectMake(0, btn.frame.origin.y, self.frame.size.width, btn.frame.size.height)];      self.panelView.backgroundColor = [UIColor whiteColor];      self.panelView.userInteractionEnabled = YES;      [self addSubview:self.panelView];      //左按钮 删除      ...      leftBtn.panelBtnType = PanelViewBtnTypeLeft;      leftBtn.tag = btn.tag;      leftBtn.keyWithBtn = btn.keyWithBtn;      [leftBtn addTarget:self action:@selector(deleteCurrentTally:) forControlEvents:UIControlEventTouchUpInside];        //右按钮 修改      ...      rightBtn.panelBtnType = PanelViewBtnTypeRight;      rightBtn.tag = btn.tag;      rightBtn.keyWithBtn = btn.keyWithBtn;      [rightBtn addTarget:self action:@selector(modifyCurrentTally:) forControlEvents:UIControlEventTouchUpInside];      //中间按钮 收回panelview     ...      [middleBtn addTarget:self action:@selector(clickMiddleBtn) forControlEvents:UIControlEventTouchUpInside];      }    //btn出现时动画  - (void)btnShowAnimation:(UIButton*)btn{        //layer动画 左右分开      }    //点击中间按钮 控制板收回  - (void)clickMiddleBtn{      for (UIButton *btn in self.panelView.subviews) {          [self btnDismissAnimation:btn];      }      [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:NO block:^(NSTimer * _Nonnull timer) {          [self.panelView removeFromSuperview];      }];    }    //btn消失时动画  - (void)btnDismissAnimation:(UIButton*)btn{      //两边收回  }    //删除当前账单  回调给controller处理  - (void)deleteCurrentTally:(UIButton*)btn {      if ([self.delegate respondsToSelector:@selector(willChangeValueForKey:)]) {          NSArray<TimeLineModel*>*array = self.timeLineModelsDict[btn.keyWithBtn];          [self.delegate willDeleteCurrentTallyWithIdentity:array[btn.tag].identity];      }  }    //修改当前账单  回调给controller处理  - (void)modifyCurrentTally:(UIButton*)btn {      if ([self.delegate respondsToSelector:@selector(willModifyCurrentTallyWithIdentity:)]) {          NSArray<TimeLineModel*>*array = self.timeLineModelsDict[btn.keyWithBtn];          [self.delegate willModifyCurrentTallyWithIdentity:array[btn.tag].identity];      }  }  @end</code></pre>    <ul>     <li>controller</li>    </ul>    <pre>  <code class="language-objectivec">#import "ViewController.h"  #import "AddTallyViewController.h"  #import "TimeLineView.h"  @interface ViewController ()<TimeLineViewDelegate>  @property(nonatomic,strong)UIScrollView *scrollView;  @property(nonatomic,strong)NSDictionary *timeLineModelsDict;  @property(nonatomic,assign)CGFloat allDateAllLine;  @property (weak, nonatomic) IBOutlet UILabel *incomLab;  @property (weak, nonatomic) IBOutlet UILabel *expenseLab;    @end  @implementation ViewController        - (UIScrollView *)scrollView {      if (!_scrollView) {           _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 64+80, self.view.frame.size.width, self.view.frame.size.height-64-80)];          _scrollView.backgroundColor = [UIColor whiteColor];      }      return _scrollView;  }    - (void)viewDidLoad {      [super viewDidLoad];      NSLog(@"%@",NSHomeDirectory());      self.title = @"我的账本";      [self.view addSubview:self.scrollView];  }    - (void)didReceiveMemoryWarning {      [super didReceiveMemoryWarning];      // Dispose of any resources that can be recreated.  }    //增加一条账单  - (IBAction)clickAddTally:(id)sender {      AddTallyViewController *addVC = [[AddTallyViewController alloc] init];      [self.navigationController pushViewController:addVC animated:YES];  }    //出现时 刷新整个时间线  - (void)viewWillAppear:(BOOL)animated {      NSLog(@"will");      [self showTimeLineView];    }    - (void)showTimeLineView {      //先读取数据库中内容 封装成字典      [self readSqliteData];        //移除上一个timelineView      for (UIView *view in self.scrollView.subviews) {          if (view.tag == 1990) {              [view removeFromSuperview];          }      }        //计算总收入 和 总支出      double incomeTotal = 0;      double expenseTotal = 0;      self.allDateAllLine = 0;      //计算时间线长度      for (NSString *key  in self.timeLineModelsDict.allKeys) {          NSArray<TimeLineModel*> *modelArray = self.timeLineModelsDict[key];          //一天的时间线总长          self.allDateAllLine = self.allDateAllLine + kDateWidth + (kBtnWidth+kLineHeight)*modelArray.count + kLineHeight;          for (TimeLineModel *model in modelArray) {              incomeTotal = incomeTotal + model.income;              expenseTotal = expenseTotal + model.expense;          }      }        self.incomLab.text = [NSString stringWithFormat:@"%.2f",incomeTotal];      self.expenseLab.text = [NSString stringWithFormat:@"%.2f",expenseTotal];        //设置时间线视图timelineview      CGRect rect  = CGRectMake(0, 0, self.view.frame.size.width, self.allDateAllLine);      TimeLineView *view = [[TimeLineView alloc] initWithFrame:rect];      view.tag = 1990;      view.delegate = self;      view.backgroundColor = [UIColor whiteColor];      view.timeLineModelsDict = self.timeLineModelsDict;      [self.scrollView addSubview:view];      self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width, self.allDateAllLine);      //滚动到顶端      [self.scrollView setContentOffset:CGPointZero animated:YES];        }      //读取数据库中的数据  以字典的形式 key:@"日期" object:[账单信息]  - (void)readSqliteData{      NSManagedObjectContext *managedObjectContext = ((AppDelegate*)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;      self.timeLineModelsDict = nil;        //先查询日期 遍历日期表      NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];      NSEntityDescription *entity = [NSEntityDescription entityForName:@"TallyDate" inManagedObjectContext:managedObjectContext];      [fetchRequest setEntity:entity];      NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO];      [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];      NSError *error = nil;      NSArray<TallyDate*> *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];      NSMutableDictionary *dict = [NSMutableDictionary dictionary];      //再查询该日期下的tally表      for (TallyDate *date in fetchedObjects) {          NSString *key = date.date;          NSFetchRequest *fetchRequest2 = [[NSFetchRequest alloc] init];          NSEntityDescription *entity2 = [NSEntityDescription entityForName:@"Tally" inManagedObjectContext:managedObjectContext];          [fetchRequest2 setEntity:entity2];          //在tally表中 筛选 为该日期的tally 并逆序排列          NSPredicate *predicate = [NSPredicate predicateWithFormat:@"dateship.date = %@",key];          [fetchRequest2 setPredicate:predicate];          NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:NO];          [fetchRequest2 setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor2, nil]];          NSError *error = nil;          NSArray<Tally*> *fetchedObjects2 = [managedObjectContext executeFetchRequest:fetchRequest2 error:&error];          NSMutableArray *array = [NSMutableArray array];          //遍历 tally表 将表中的每个结果保存下来          for (Tally *tally in fetchedObjects2) {              TimeLineModel *model = [[TimeLineModel alloc] init];              model.tallyDate = tally.dateship.date;              model.tallyIconName = tally.typeship.typeicon;              model.tallyMoney = tally.income > 0 ? tally.income:tally.expenses;              model.tallyMoneyType = tally.income > 0 ? TallyMoneyTypeIn:TallyMoneyTypeOut;              model.tallyType = tally.typeship.typename;              model.identity = tally.identity;              model.income = tally.income;              model.expense = tally.expenses;              [array addObject:model];          }          [dict setObject:array forKey:key];      }      self.timeLineModelsDict = dict;  }    //删除前的确认  - (void)willDeleteCurrentTallyWithIdentity:(NSString*)identity {        UIAlertController *alertVC =[UIAlertController alertControllerWithTitle:@"提示" message:@"确认删除" preferredStyle:UIAlertControllerStyleAlert ];      [alertVC addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {          //从数据库中删除 Tally表中对应identity字段行          NSManagedObjectContext *managedObjectContext = ((AppDelegate*)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;          NSFetchRequest *fetchRequest = [Tally fetchRequest];          NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identity = %@", identity];          [fetchRequest setPredicate:predicate];          NSError *error = nil;          NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];          [managedObjectContext deleteObject:[fetchedObjects firstObject]];          [managedObjectContext save:&error];          //删除完成后 刷新视图          [self showTimeLineView];        }]];      [alertVC addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];      [self presentViewController:alertVC animated:YES completion:nil];    }    //TimeLineViewDelegate  delegate 修改前的准备  - (void)willModifyCurrentTallyWithIdentity:(NSString*)identity {      AddTallyViewController *addVC = [[AddTallyViewController alloc] init];      [addVC selectTallyWithIdentity:identity];      [self.navigationController pushViewController:addVC animated:YES];    }  @end</code></pre>    <h2>5.结束</h2>    <p>由于coredata增删改查时的代码量实在是太大,我们可以优化一下,将数据库操作全部放到一个类中,这样代码逻辑会更清晰一点,可读性更强。</p>    <p>也是也到这里才想到数据库封装。所以刚刚去改了下。</p>    <p>所以上面的代码都包括冗长的coreData操作</p>    <p>创建一个 数据库操作的单例</p>    <pre>  <code class="language-objectivec">#import <Foundation/Foundation.h>  #import <CoreData/CoreData.h>  #import "TimeTallyDemo+CoreDataModel.h"  #import "AppDelegate.h"  #import "TimeLineModel.h"  @interface CoreDataOperations : NSObject  @property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;  //单例  + (instancetype)sharedInstance;  //从数据库中删除 Tally表中对应identity字段行  - (void)deleteTally:(Tally*)object;    //保存  - (void)saveTally;    //读取对应字段  - (Tally*)getTallyWithIdentity:(NSString *)identity;    //获取对应类型  - (TallyType*)getTallyTypeWithTypeName:(NSString*)typeName;    //读取数据库中的数据  以字典的形式 key:@"日期" object:[账单信息]  - (NSDictionary*)getAllDataWithDict;  @end</code></pre>    <pre>  <code class="language-objectivec">#import "CoreDataOperations.h"  @interface CoreDataOperations()  @end    @implementation CoreDataOperations    static CoreDataOperations *instance = nil;    + (instancetype)sharedInstance  {      return [[CoreDataOperations alloc] init];  }    + (instancetype)allocWithZone:(struct _NSZone *)zone  {      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          instance = [super allocWithZone:zone];      });      return instance;  }    - (instancetype)init  {      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          instance = [super init];          if (instance) {              instance.managedObjectContext = ((AppDelegate*)[UIApplication sharedApplication].delegate).persistentContainer.viewContext;          }      });      return instance;  }    //从数据库中删除 Tally表中某一数据  - (void)deleteTally:(Tally*)object {      [self.managedObjectContext deleteObject:object];  }    //保存  - (void)saveTally {      [self.managedObjectContext save:nil];  }    //读取对应字段  - (Tally*)getTallyWithIdentity:(NSString *)identity {      //返回对应账单  }    //获取对应类型  - (TallyType*)getTallyTypeWithTypeName:(NSString*)typeName {      //返回对应账单类型  }    //读取数据库中的数据  以字典的形式 key:@"日期" object:[账单信息]  - (NSDictionary*)getAllDataWithDict{          //遍历查询      return dict;  }    @end</code></pre>    <h2> </h2>    <p> </p>