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

前面对着别人的代码学习做了第一个iOS的Demo-通讯录,这次完全靠自己设计编码实现了另一个简单的Demo-备忘录。没错,就是仿iPhone上的备忘录。虽然demo很简单,但是我完全自己做的第一个demo,涵盖了我所学到的大部分知识,比如委托和协议、UITableViewController、UINavigationController等等,自认为对初学者有点帮助。

备忘录.gif

本文将详细讲解我的设计思路和源码分析。文章最后有源码链接,欢迎指正!

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

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];

2. 共用视图

这里的视图都是共用的。具体地说,所有账户的备忘录目录都是共用一个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,最终达到了共用视图展示不同数据的效果。

3. 数据同步

根据前面的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];

4. 更新/添加备忘录的协议及委托

更新备忘录后,返回上一级控制器,需要上一级控制器更新数据;同样,添加完新的备忘录后,返回上一级控制器,也要上一级控制器更新数据。 给下一级控制器传值时可以直接调用下级控制器的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];

5. 键盘和中文输入法

在UITextView中,一开始我这里是获得焦点后没有弹出键盘的,后来google一下,其实很简单,Cmd + Shift + K就可以调出来。

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

不足之处

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

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

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

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

comments powered by Disqus