项目源码下载地址:
https://github.com/ShayneYeorg/Meditashayne
1、首先一开始设计这个App的时候,我就希望它能比系统自带的备忘录更方便:比如备忘录需要手动去点击一下保存,我希望我的App可以省略掉点击保存这一步,只需要退出随笔的详情页面便可自动保存内容。
那么退出详情页面有两种方式:左上角的“返回”以及UIViewController自带的左划退出。
(1)、在处理完“返回”按钮之后,发现了第一个问题:自定义NavigationController上的BarButtonItem后,UIViewController自带的左划手势失效了。
最后处理方法是在MDSNavigationController(自定义的NavigationController)里增加以下方法,并在viewDidLoad中调用,左划手势重新生效:
(2)、考虑到刚进入随笔详情页面的时候,页面里还未有内容,这时就不应该存储没有内容的随笔,即是不能允许使用“返回”按钮或左划返回上一页面。
然后使用了以下方法来灵活地开启或关闭当前控制器的左划功能:
(3)、然后发现把退出和保存两个功能捆绑在一起这种处理方式存在问题:
如果我是不小心点到“新建”按钮进入新建随笔详情页面的,那么根据(2)我需要填写点内容才能退出,而退出的时候我填写的这些内容会被自动保存,可是我一开始是误点进来的,我并不想保存任何内容的,这就出现了矛盾。
或者当我点击某篇随笔进入编辑随笔详情页面并做了改动之后,如果我想放弃这些改动,我发现此时就找不到方法来放弃了,直接退出会把我所做的改动保存起来,这也是一样的矛盾。
即是说,当把退出和保存两个功能捆绑在一起之后,App便丧失了“退出但不保存”的功能。虽然可以通过退出的时候弹框提示是否要保存来处理,但是这样的话就会使得退出操作更繁琐,与设计这个功能时为了操作更简便的初衷相违背了。
基于以上考虑,最终去除了“退出同时保存”的功能,使用了在NavigationController上的按钮来进行保存。
2、关于上拉加载更多控件,这个控件仿写公司项目使用的上拉加载更多控件的,使用的时候需要注意:在ScrollView或者TableView有数据之后,即是有了contentSize.height之后,需要再去调用一下控件的setState方法(直接或间接调用),方能让控件显示正确的状态。
3、在随笔列表页面,我希望App能全量展示所有的随笔内容,不希望点击某一条随笔进入随笔详情才看到所有内容。那么cell的高度便是不固定的了,需要用到动态高度的cell。
(1)、为cell定义一个属性article,用来存放随笔模型,然后在setArticle方法内,使用以下方法来动态计算cell的高度,其中contentView是显示内容的TextView:
之所以高度要加1是为了给下分割线空出空间。
这个功能的参考文档:http://www.cocoachina.com/industry/20140604/8668.html
(2)、然后发现,虽然cell能动态计算高度了,但是计算只是依据随笔内容的段落数。即是如果随笔内容是两段,而每一段文字的长度都超过了屏幕的宽度,那么cell计算出来的内容高度只是两行的高度。因为systemLayoutSizeFittingSize:方法并不知道TextView的宽度,也就无法知道在哪里需要换行。于是将计算cell高度的代码修改如下:
4、在进行3(2)的操作时,约束也要相应配合更改。我一开始给UITextView添加了宽度约束后,忘了将UITextView与右边的边距约束去掉,导致执行时出现了以下约束冲突的错误:
2016-02-21 15:21:03.443 Meditashayne[4685:225763] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don‘t want.
Try this:
(1) look at each constraint and try to figure out which you don‘t expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you‘re seeing NSAutoresizingMaskLayoutConstraints that you don‘t understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x7fbf4b5b5ec0 h=-&- v=-&- MDSSearchView:0x7fbf4b5ec510.width == UIView:0x7fbf4b41cd10.width>",
"<NSLayoutConstraint:0x7fbf4b5ee5a0 H:[UITextField:0x7fbf4b5ed780(264)]>",
"<NSLayoutConstraint:0x7fbf4b5f1490 H:|-(8)-[UITextField:0x7fbf4b5ed780] (Names: ‘|‘:UIView:0x7fbf4b5ed610 )>",
"<NSLayoutConstraint:0x7fbf4b5f14e0 H:[UITextField:0x7fbf4b5ed780]-(8)-| (Names: ‘|‘:UIView:0x7fbf4b5ed610 )>",
"<NSLayoutConstraint:0x7fbf4b5cdb40 H:[UIView:0x7fbf4b5ed610]-(20)-| (Names: ‘|‘:UIView:0x7fbf4b5ed4a0 )>",
"<NSLayoutConstraint:0x7fbf4b5a62a0 H:|-(20)-[UIView:0x7fbf4b5ed610] (Names: ‘|‘:UIView:0x7fbf4b5ed4a0 )>",
"<NSLayoutConstraint:0x7fbf4b5fa280 H:[UIView:0x7fbf4b5ed4a0]-(0)-| (Names: ‘|‘:MDSSearchView:0x7fbf4b5ec510 )>",
"<NSLayoutConstraint:0x7fbf4b5ed140 H:|-(0)-[UIView:0x7fbf4b5ed4a0] (Names: ‘|‘:MDSSearchView:0x7fbf4b5ec510 )>",
"<NSLayoutConstraint:0x7fbf4b616a70 ‘UIView-Encapsulated-Layout-Width‘ H:[UIView:0x7fbf4b41cd10(375)]>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fbf4b5ee5a0 H:[UITextField:0x7fbf4b5ed780(264)]>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
原因就在于UITextView设置了与superView的左右边距,又设置了宽度为264,导致了冲突。
5、在动态计算高度的cell里面,要把UITextView的Scrolling Enable设为NO,否则TextView的高度不会跟着内容变化,只会取一个最小的高度;同时也要把UITextView的user interaction enable也设为NO,否则TextView会拦截掉cell的点击事件,选中cell变成了编辑TextView。
6、在做完动态高度的cell之后,发现在真机上App打开后加载数据的时候非常慢。我一开始以为是Core Data的问题,于是自定义了一个加载的浮框控件,在打开App的时候,新开一条线程去Core Data取数据,主线程显示加载控件,样式如下:
但是发现问题仍然存在,打开App之后仍然是会卡住一段时间,并且卡住的是主线程上的加载控件,甚至当时页面上只有1个cell,都整整卡住了8秒钟。那就很明显问题不在于Core Data了,而在于主线程上。
于是我一步一步排查,首先发现耗时操作主要在于获取到数据后刷新TableView的reloadData方法上,再进一步排查发现耗时主要发生在这个方法里:
reloadData方法会调用tableView:cellForRowAtIndexPath:方法,下一步就有可能会调用到上面的loadNibName:owner:options:方法,而主要的耗时就发生在这个方法里。于是着重研究这个xib文件,最后发现问题出在两个地方:
(1)、xib里显示随笔标题的UILabel使用了“苹方”这种字体,而真机是iOS 8系统的,手机系统里匹配不到“苹方”字体,最终使用了系统默认的字体,这个匹配的过程造成了大部分的耗时,最终将UILabel的字体设为系统默认字体,解决了这部分耗时;
(2)、xib里显示随笔内容的UITextView默认的text写了中文,导致了小部分的耗时,将默认text改成英文就解决了这部分耗时。
然后发现仍然有改进的空间:
现在cell的高度cellHeight是在cell设置随笔模型的setArticle:方法里去计算的,每次调用setArticle:方法都会计算一次,而setArticle:方法调用在tableView:cellForRowAtIndexPath:方法里,即是,每次调用tableView:cellForRowAtIndexPath:都会计算一次cell高度。
进入App的时候,在tableView:heightForRowAtIndexPath:会去根据IndexPath取出对应cell,再返回它的高度,这个方法这里也是调用了tableView:cellForRowAtIndexPath:方法去取cell的,所以就造成了这么个情况:进入App的时候,每个cell的高度都会被计算两次。
(3)、于是就使用了以下这个方法预估cell的高度,减少了一次cell高度的计算,又优化了一小部分耗时:
做完这些优化之后,发现加载数据的时间已经非常短了,以至于根本不需要一个加载浮框了,于是就把加载浮框移除掉了。
7、在真机上跑App的时候发现了另一个问题:拖动TableView的时候会卡顿。原因还是在于cell高度的计算上,由于拖动TableView的时候,哪怕这个cell之前已经显示过了,新出现的cell还是会调用tableView:cellForRowAtIndexPath:方法,这样就多次计算了一个cell的高度。而这些能耗在iOS 8系统的iPhone 4S上显得特别明显。
于是我开始思考是否有办法让所有cell都只计算一次,毕竟对于已经计算过高度的cell,是完全没有必要再重复去计算高度的了。最终使用了以下的方法来处理:
(1)、在cell里单独提供一个计算高度的方法,不再在设置随笔模型的setArticle:方法里去计算cell的高度:
(2)、在随笔列表控制器里新增一个属性:
这个属性用来存放cell的高度,以IndexPath.row为key,对应cell的高度为value存在这个字典里。
(3)、然后将tableView:cellForRowAtIndexPath:方法修改如下:
(4)、最后将tableView:heightForRowAtIndexPath:修改如下,这样,所有的cell就只会计算一次了:
8、创建了搜索栏之后,我希望能只点击搜索栏右上方的Handler拖动这个搜索栏,而在Handler左右方的透明部分则不拦截任何手势:
在搜索栏中重写了这个方法完成了这个功能:
9、当搜索栏调出了键盘后,控制器接收到UIKeyboardWillShowNotification通知后在self.view上会添加一层蒙板,用以点击时收起键盘,同时也将蒙板自身remove掉。但是在真机上,发现点击了蒙板后虽然键盘会收起,但是self.view却没有任何点击反应了,似乎它并没有将自身remove掉。
最终排查之后发现,问题出在UIKeyboardWillShowNotification通知上。在iOS 8 系统下,中文键盘出现时会发送3次UIKeyboardWillShowNotification通知,这样就使得页面上添加了3层蒙板,导致了页面点击无反应。
最终在UIKeyboardWillShowNotification通知的处理方法里,处理成先移除已有蒙板再添加新蒙板,解决了这个问题。