iOS开发实战-时光记账Demo 本地数据库版
liyy3867
8年前
<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>