前面对着别人的代码学习做了第一个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展示了。
-
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];
比如从首页到详细信息的过程中,控制器栈的情况如下图:
实现细节###
-
数据持久化
关于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存储的格式如下图所示:
首先是一个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];
-
共用视图
这里的视图都是共用的。具体地说,所有账户的备忘录目录都是共用一个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,最终达到了共用视图展示不同数据的效果。
-
数据同步
根据前面的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];
-
更新/添加备忘录的协议及委托
更新备忘录后,返回上一级控制器,需要上一级控制器更新数据;同样,添加完新的备忘录后,返回上一级控制器,也要上一级控制器更新数据。
给下一级控制器传值时可以直接调用下级控制器的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];
-
键盘和中文输入法
在UITextView中,一开始我这里是获得焦点后没有弹出键盘的,后来google一下,其实很简单,Cmd + Shift + K就可以调出来。
一开始也是不能输中文的,方法是在模拟器中的settings->General->Keyboard->Keyboards->Add New Keyboard->Chinese就可以了。
不足之处###
虽然可以实现基本的功能,但还是有很多不足之处的:
- 首页改成课编辑的,既添加/删除 账户。
- 备忘录选项的标题长度超过一定长时显示省略号,不遮挡时间。
- 数据模型的定义方式与存储方式不同,在写文件时要做一次转化,显得很不雅。
当然还有很多不足之处,毕竟小白第一次自己写iOS小程序,文件组织、代码风格、性能方面肯定有很多需要改进的地方,真诚希望各位大牛指正!
源码:https://github.com/foolish-boy/Memo
其中在Memo目录下有MemoInfo.plist,测试的话可以把他拷贝到你自己的沙盒目录下去。