iOS 小而精的Demo(2) 备忘录

备忘录.gif

前面对着别人的代码学习做了第一个iOS的Demo-通讯录,这次完全靠自己设计编码实现了另一个简单的Demo-备忘录。没错,就是仿iPhone上的备忘录。虽然demo很简单,但是我完全自己做的第一个demo,涵盖了我所学到的大部分知识,比如委托和协议、UITableViewController、UINavigationController等等,自认为对初学者有点帮助。
本文将详细讲解我的设计思路和源码分析。文章最后有源码链接,欢迎指正!

  • 功能介绍
  • 设计模式
  • 实现细节
  • 不足之处

1. 功能介绍###

其实大家都应该用过备忘录,而且本来就很简单,即使看上面的gif图就大致了解了备忘录的功能了,这里简单说明一下。

首先,首页上显示的是账户列表。你可以有很多个账户,我随机选了三个账户作为例子。这里的账户个数是固定的,当然实现可变也是很简单的。

点击任何一个账户选项,进入新的页面,展示了当前选择账户下的备忘录主题列表。列表选项的左边是备忘录的题目,右侧是创建时间。其中如果创建时间在24小时之内,就只显示时和分,否则只显示年月日。

若点击任何一个备忘录选项,进入新的页面,展示当前选择备忘录的详细内容,可以直接编辑修改这个备忘录,但不能是空;若点击“新建”按钮,则进入创建新备忘录的页面,新备忘录的第一行文本默认作为标题;若向左滑动一个选项,则弹出“删除”按钮,点击可以删除这个备忘录。

在创建新备忘录的页面中,若点击“返回”,则什么都不做;若点击“完成”,若新备忘录是空的,则什么都不做,否则添加新的备忘录到内存中。


设计模式###

还是最简单经典的MVC模式。

  • Model####

设计一个JWMemoDetail类,表示一个备忘录信息对象,包括标题,创建时间和具体内容。

@interface JWMemoDetail : NSObject
#pragma mark 标题
@property (nonatomic, strong) NSString *title;
#pragma mark 创建时间
@property (nonatomic, strong) NSString *createTime;
#pragma mark 具体内容
@property (nonatomic,strong) NSString *detail;
#pragma mark 初始化方法
- (JWMemoDetail *)initWithTitle:(NSString *)title andCreateTime:(NSString *)createTime
                      andDetail:(NSString *)detail;
#pragma mark 静态初始化方法
+ (JWMemoDetail *)memoDetailWithTitle:(NSString *)title andCreateTime:(NSString *)createTime
                      andDetail:(NSString *)detail;
@end

再设计一个JWMemoAccount类,表示一个账户信息对象,包括账户名称和所包含的若干备忘录信息对象。

@class JWMemoDetail;

@interface JWMemoAccount : NSObject

#pragma mark 账户名称
@property (nonatomic,strong) NSString *accountName;
#pragma mark 具体内容(标题、时间、内容)
@property (nonatomic,strong) NSMutableArray *memoDetail;
#pragma mark 初始化方法
- (JWMemoAccount *)initWithAccountName:(NSString *)accountName andDetail:(NSMutableArray *)detail;
#pragma mark 静态初始化方法
+ (JWMemoAccount *)memoAccountWithAccountName:(NSString *)accountName andDetail:(NSMutableArray *)detail;

@end

有了这两个Model,可以满足所有的ViewController操作以及所有的View展示了。

Model之间的关系
  • View####

对照功能介绍,就只有四个简单的视图,分别是:

首页视图,homeView,继承自UITableView。 展示账户列表。
目录视图,contentView,继承自UITableView。展示备忘录标题列表。
详细视图,deteailView,继承自UITextView。展示备忘录相信信息。
新建视图,neMemoView,继承自UITextView。编辑新建备忘录。

  • Controller####

由于是多个页面之间的切换,就我目前所学,知道最好的方法是用UINavigationController。所以设置最开始的rootViewController为一个UINavigationController。

self.navViewController = [[UINavigationController alloc] initWithRootViewController:self.homeViewController];
self.window.rootViewController = self.navViewController;

剩余就是跟View对应的几个Controller。

JWHomeViewController,继承自UITableViewController。
JWContentViewController,继承自UITableViewController。
JWDetailViewController,继承自UIViewController。
JWNewMemoViewController,继承自UIViewController。

这个navViewController的rootViewController是首页视图的Controller,既homeViewController。

各个视图之间的切换借助于UINavigationController的
pushViewController:animated:popViewControllerAnimated:方法

[self.navigationController pushViewController:self.contentViewController animated:YES];
[self.navigationController popViewControllerAnimated:YES];

比如从首页到详细信息的过程中,控制器栈的情况如下图:

控制器入栈

实现细节###

  1. 数据持久化
    关于iOS的数据持久化,大家都知道常见的四种方法:属性列表、对象归档、SQLite3和Core Data。在本demo中,选择的是第一种方法,原因有二:(1) 我第一次使用数据持久化,选个最简单的试试先;(2)备忘录数据很简单,而且没有安全性的要求,所以选择属性列表最方便。

这里通过沙盒机制创建和使用 plist。
我在程序启动一开始就记录下数据文件的绝对路径,方便后续的读取和写入。

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *plistPath = [paths objectAtIndex:0];
self.homeViewController.dataFileName = [plistPath stringByAppendingPathComponent:@"MemoInfo.plist"];

那在首页加载完成后,就可以读取数据到内存中了:

NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:self.dataFileName];

在程序退出的时候将内存中最新的数据写入文件中:

[dataToStore writeToFile:self.dataFileName atomically:YES];

MemInfo.plist存储的格式如下图所示:


MemoInfo.plist

首先是一个Dictonary,再是一个Array,每个元素又是一个Dictionary。
由于plist只能存储Array、Dictionary、String等简单数据类型,不能存储自定义类型,所以在存储的时候,还要做个转化。

//将_memoAccount中的memoDetails转化为NSDictionary类型
NSMutableDictionary *dataToStore = [[NSMutableDictionary alloc] init];
  for (JWMemoAccount *account in _memoAccount) {
      NSString *accoutName = [account accountName];
      NSMutableArray *accountDetails = [account memoDetail];
      NSMutableArray *tmpArr = [[NSMutableArray alloc] init];
      for (JWMemoDetail *md in accountDetails) {
          NSMutableDictionary *tmpDic = [[NSMutableDictionary alloc] init];
          [tmpDic setValue:[md title] forKey:@"title"];
          [tmpDic setValue:[md createTime] forKey:@"createTime"];
          [tmpDic setValue:[md detail] forKey:@"detail"];
          [tmpArr addObject:tmpDic];
      }
      [dataToStore setObject:tmpArr forKey:accoutName];
  }
  [dataToStore writeToFile:self.dataFileName atomically:YES];
  1. 共用视图
    这里的视图都是共用的。具体地说,所有账户的备忘录目录都是共用一个contentView的;所有备忘录的具体内容都是共用一个detailView的;在任何账户下新建备忘录时共用的是neMemoView的。实现这一点要注意的就是保证数据源不同:不同的账户展示的contentView的数据源是不同的,不同备忘录选项展示的detailView的数据源也是不同的。其原理就是每次进入新的视图页面时,会传递不同的参数值。
    homeView ->contentView:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    JWMemoAccount *account = [_memoAccount objectAtIndex:indexPath.row];
    self.selectedIndex = indexPath.row;
    //第一次进入contentViewController才分配内存
    //以后直接复用。但是这里不要用initWithArray,
    //要显示赋值,才能使account的memoDetail与contentViewController的
    //memoDetails指向同一块内存,才能使两者保持实时一致性。
    if (self.contentViewController.memoDetails == nil) {
        self.contentViewController.memoDetails = [[NSMutableArray alloc] init];
        self.contentViewController.memoDetails = [account memoDetail];
    } else {
         self.contentViewController.memoDetails = [account memoDetail];
    }
    ...
}

contentView —>detailView:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    JWMemoDetail *md = [_memoDetails objectAtIndex:indexPath.row];
    self.selectedIndex = indexPath.row;
    NSString *detail = [md detail];
    self.detailViewController.detail = detail;
    self.detailViewController.createTime = [md createTime];
    ...
}

此时,新的视图接收到的数据会被更新了,但是由于视图的加载是只有一次的,再次进入视图时并不会自动更新tableView的视图,所以视图显示的数据还是旧的,始终是第一次打开的账户的备忘录列表和内容。 这就需要手动刷新tableView的数据,这里选择的刷新时刻是在视图即将展现的时刻:

- (void)viewWillAppear:(BOOL)animated {
  [self.tableView reloadData];
}

这样,点击新的账户或者备忘录时,下一个视图的数据是新的,而且在展现视图之前已经刷新了tableView,最终达到了共用视图展示不同数据的效果。

  1. 数据同步
    根据前面的Model设计方式,不同的ViewController管理的Model是不同的。JWHomeViewController管理是整个数据结构JWMemoAccount;JWContentViewController管理的是部分数据结构JWMemoDetail;JWDetailViewController管理的是更小部分的数据结构detail和createTime。
    当更新或添加新的备忘录时,不仅要保证当前detail和createTime更新,而且JWMemoDetail和JWMemoAccout也要更新,既要保证数据的全局同步性。实现的原理就是利用OC的引用指针。引用指针使不同的指针对象指向同一块内存区域,任一个指针对象对内存的改变将对所有的指针对象可见。
    JWContentViewController的属性memoDetails是NSMutableArray类型的,但使用的描述符是strong而非copy,这样它与JWHomeViewController的JWMemoContent属性中的memoDetails指向同一块内存。更新数据对两者都可见。
@property (nonatomic,strong) NSMutableArray *memoDetails;

要注意的就是,在JWHomeViewController给JWContentViewController的memoDetails第一次赋值的时候,不要用initWithArray方法,它会默认使用copy,而要显示赋值。

 if (self.contentViewController.memoDetails == nil) {
      self.contentViewController.memoDetails = [[NSMutableArray alloc] init];
      self.contentViewController.memoDetails = [account memoDetail];
  1. 更新/添加备忘录的协议及委托
    更新备忘录后,返回上一级控制器,需要上一级控制器更新数据;同样,添加完新的备忘录后,返回上一级控制器,也要上一级控制器更新数据。
    给下一级控制器传值时可以直接调用下级控制器的setter方法,而给上一级控制器传值时需要用到协议和委托。
    具体方法是:

若控制器C1是控制器C2的上一级,C2返回到C1时需要给C1传值。
1.定义一个协议P,声明一个传值的方法F,参数类型是传值的类型;
2.在C2中定义一个P类型的委托D;
3.在C1中,实现P协议的方法F;
4.在C1中,指定C2的D是self(C1);
5.在C2的合适地方给传值赋值,并调用D的方法F;

这样,就可以让C1获的C2想传递的值了。参考下面具体代码:

//JWNewMemoProtocol.h
@protocol NewMemoProtocol
- (void) addNewMemo:(JWMemoDetail *)memoData;
@end

//JWDetailViewController.h
@property (nonatomic) id<UpdateMemoProtocol> delegate;

//JWContentViewController.m
@interface JWContentVIewController ()<NewMemoProtocol,UpdateMemoProtocol>
@end

- (void) updateMemo:(JWMemoDetail *)memoData {
    [self.memoDetails replaceObjectAtIndex:self.selectedIndex withObject:memoData];
}

self.detailViewController.delegate = self;

//JWDetailViewController.m
[self.delegate updateMemo:memoDetail];
  1. 键盘和中文输入法
    在UITextView中,一开始我这里是获得焦点后没有弹出键盘的,后来google一下,其实很简单,Cmd + Shift + K就可以调出来。

一开始也是不能输中文的,方法是在模拟器中的settings->General->Keyboard->Keyboards->Add New Keyboard->Chinese就可以了。


不足之处###

虽然可以实现基本的功能,但还是有很多不足之处的:

  1. 首页改成课编辑的,既添加/删除 账户。
  1. 备忘录选项的标题长度超过一定长时显示省略号,不遮挡时间。
  2. 数据模型的定义方式与存储方式不同,在写文件时要做一次转化,显得很不雅。

当然还有很多不足之处,毕竟小白第一次自己写iOS小程序,文件组织、代码风格、性能方面肯定有很多需要改进的地方,真诚希望各位大牛指正!

源码:https://github.com/foolish-boy/Memo
其中在Memo目录下有MemoInfo.plist,测试的话可以把他拷贝到你自己的沙盒目录下去。

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

推荐阅读更多精彩内容

  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 8,908评论 3 38
  • 前言 最近忙完项目比较闲,想写一篇博客来分享一些自学iOS的心得体会,希望对迷茫的你有所帮助。博主非科班出身,一些...
    GitHubPorter阅读 1,409评论 9 5
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 1 场景问题# 1.1 开发仿真系统## 考虑这样一个仿真应用,功能是:模拟运行针对某个具体问题的多个解决方案,记...
    七寸知架构阅读 2,110评论 1 50
  • 下午六点钟,我与这个城市照面。 夕阳下 有人下地铁,有人上电梯,有人取车, 有人发传单,有人等绿灯,有人走路。 而...
    葱头越阅读 271评论 0 1