六.用户界面
主要讨论如火如何最小化更新UI所需的时间。
6.1视图控制器
视图控制器的生命周期:
创建试图控制器时需要遵循的基本最佳实践:
- 保持视图控制器轻量。在MVC结构应用中,控制器只是纽带,而不是存放所有业务逻辑的地方。它甚至不属于模型。业务逻辑应该属于服务层或业务逻辑组件。
- 不要在视图控制器中编写动画逻辑。动画可以在独立的动画类中实现,该类接受视图作为参数传入,这些视图就是用来运行动画的视图。然后视图控制器会将动画添加至视图或转场效果上。
- 使用数据源和委托协议,将代码按照数据检索,数据更新和其他业务逻辑进行分离。视图控制器只能用来选择正确的视图,并将它们连接到供应源。
- 视图控制器响应来自视图的事件,如按钮点击事件或单元格的选择事件,然后将它们连接至数据接收器。
- 视图控制器响应来自操作系统的UI相关事件,如方向变化和低内存警告。这可能会触发视图的重新布局。
- 不要编写自定义的init代码。如果视图控制器被重新切换至XIB或故事板,那init方法永远都不会被调用。
- 不要在视图控制器中使用代码手工布局UI,也不要在视图控制器中实现全部的UI,视图创建和视图布局逻辑等操作。使用nib或者故事板。
手工布局代码不会持续很久,因为应用在不断增长,并且设计也在改变。在重新设计方面,使用interface Builder比根据像素坐标来手动编写代码更快。 如果某一设计被分在独立的nib和故事板,可以比较灵活运行A/B测试https://www.zhihu.com/question/20045543,因为在不同的约束之间很容易选择最终需要的。而且自定义代码不容易适配设备。 - 创建一个实现公共设置的基类控制器。
- 在各视图控制器之间,使用category创建可复用的代码。这样就不会被限制只能使用预定义的基类,同时还能得到复用带来的好处。
6.1.1视图加载
视图初始化时会涉及两个方法--loadview和viewDidLoad. 当添加一个新的视图控制器时,通过Xcode生成的模板代码只有viewDidLoad方法。当视图控制器的view被请求时,loadview方法会被调用,但因为它还未被创建,所以会是nil。
三种加载视图方式:
- 从nibs(xibs)
- 使用故事板
- 自定义代码创建UI
如果通过覆写loadview方法创建了自定义UI
- 将view属性设置大片视图层级的跟上。
- 确保视图正被其他的视图控制器所共享。
- 不要调用[super loadview]
iOS用户界面:Storyboards vs. NIBs vs. Custom Code http://www.jianshu.com/p/10dd75d34a20
6.1.2视图层级
视图结构和渲染包括以下步骤: (1)构造子视图。 (2)计算并提供约束。 (3)为子视图递归地执行步骤1和步骤2. (4)递归渲染 viewDidAppear:方法会因为过度动画的原因在约300毫秒后被调用。
6.1.3视图可见性
视图控制器提供了4个生命周期方法,以接受有关视图可视性的通知。
-
viewWillAppear:当视图层级已经准备好,且视图即将被放入视图窗口时,此方法会被调用。 在这个时刻,过渡动画还未开始,视图对终端用户也是不可见的。不要启动任何视图动画,因为没有任何作用。
-
viewDidAppear: 当视图在视图窗口展示出来,且过渡动画完成后,此方法会被调用。 启动或恢复任何想要呈现给用户的视图动画。
-
viewWithDisappear:该方法表示视图将要从屏幕上隐藏起来。这可能是因为其他视图控制器想要接管屏幕,或该视图控制器将要出栈。此方法被调用时,没有办法能够直接判断这是由当前视图控制器要出栈还是其他视图控制器入栈导致的。区分的唯一方法是扫描当前视图控制器navigationController的viewController属性
NSInteger index = [self.navigationController.viewControllers indexOfObject:self];
if(index == NSNotFound){
//即将出栈,销毁
}else{
//只是保存状态,暂停
}
- viewDidDisappear:当上一个/下一个视图控制器的过渡动画完成时,此方法会被调用。
高效使用生命周期事件的最佳实践:
- 不要重写loadview。
- 将viewDidLoad作为最后的检查点,查看来自数据源的数据是否可用。如果可用,则更新UI。
- 如果每次都需要展示最新的信息,那么就用viewwillappear:更新UI元素。
- viewDidAppear:中开始动画。
- viewWillDisappear:来暂停或停止动画。
- viewDidDisappear:销毁内存中的复杂数据结构。也可以在这里注销与视图控制器绑定的数据源通知,以及与动画,数据源,UI更新有关的应用事件通知中心。
6.2视图
基本规则:
- 尽量减少在主线程中所做的工作。
- 避免较大的xibs或故事板。
- 避免在视图层次结构中多层嵌套。在层次结构的任何位置添加视图时,它的祖先树节点会执行值为YES的setNeedsLayout:方法,当事件队列正在执行时,该设置会触发layoutSubviews:。这个调用代价较大,因为视图必须根据约束重新计算子视图的位置。
- 尽可能延迟加载视图并进行重用。
- 对于复杂的UI而言,最好使用自定义绘图。这样只会触发一个视图进行绘制,而不是多个子视图,同时也避免了调用代价较高的layoutsubviews和drawRect:方法。
6.2.1UILabel
(1)使用字体、字体类型以及要被渲染的文本时,计算需要的像素数目。这是一个消耗较大的过程,应尽可能少地去做。 (2)检查要被渲染的宽度。 (3)检查numberOfLines,计算将要展示的行数。 (4)sizeToFit是否被调用?如果是,计算高度。 (5)如果sizeToFit没有被调用,检查当前的内容能否在给定的高度下展示出来。 (6)如果frame不够,使用lineBreakMode确定隐藏或截断的位置。 (7)使用字典、类型及颜色来渲染最终展示的文本。
6.2.2UIButton
渲染按钮的方式:
- 使用自定义文本的默认渲染。https://robots.thoughtbot.com/designing-for-ios-taming-uibutton
- 全尺寸资源的按钮
- 可变大小的资源
- 使用CALayer和贝塞尔路径定义绘制
选项 | 优点 | 缺点 |
---|---|---|
自定义文本 | 最简单的方式,可直接使用 | 通常是比较呆板,毫无装饰的按钮 |
全尺寸资源 | 可自定义的背景 无需代码即可实现 可实现A/B测试—图像可 在运行试验时下载 | 图片打包在应用中,导致包变大 |
可变大小资源 | 可自定义的背景 无需代码即可实现 <div>可实现A/B测试—图像可</div> <div>在运行试验时下载</div><div>包大小的增量相对较小</div> | 资源的任何更改可能都需要重新计算 重置UIEdgeInsets值 |
使用CALayer和 贝塞尔路径定义绘制 | 完全是自定义绘图 | 任何格式的更改或升级都可能需要更新 应用 |
6.2.3UIImageView
iOS仍旧不支持GIF动画,只能创建animationImages的一个数组来存放可以生成动画的图片。或使用自定义编码和第三方库。 自定义编码:http://www.imagemagick.org/script/index.php 第三方库:https://github.com/mattt/AnimatedGIFImageSerialization
使用UIimage和UIimageview的最佳实践:
- 对于已知的图像,使用imageNamed:方法加载图片。它可以确保内容只被加载至内存一次,还可以确保多个UIImage对象间改变用途。
- 在使用imageNamed:方法加载图片时,使用资源包。如果应用有一堆图标,且每个图标都较小时,这种方法极其有用。可以随意创建相关图像的多个目录。如果想加载一个只使用一次的大图像,最好谨慎思考一下,考虑使用imageWithContentsOfFile:代替资源目录和imageNamed:方法,因为资源目录缓存了这些图片。
- 对于其他图像,使用高性能的图像缓存库。AFNetWorking和SDWebImage。当使用内存中的图片时,确保正确配置了内存的使用参数。不要使用硬编码。让它能够自适应--使用合理的RAM百分比可以较好的进行配置。
- 载入的图像与即将渲染的UIImageView大小相同。因为调整图像大小是一个耗费较大的操作。
- 无论使用何种技术加载图像,在非主线程中执行,最好在一个专用队列中执行。尤其要在非主线程中解压JPG/PNG图像。
- 确认是否真的需要使用图像,最好使用直接绘制的自定义视图,而不是多个图像。
6.2.4UITableView
最佳实践:
- 在数据源的cellForRowAtIndexPath:方法里,使用dequeueReusableCellWithIdentifier:进行单元格的重用。
- 尽可能避免动态高度的单元格。如果想要使用可变高度的单元格,最好选择较高的单元格,因为这只需要为较少的单元格计算高度,从而减少了计算量。
- 如果你真的需要动态高度的单元格,那么定义一个规则来标记单元格为脏的。如果某个单元格是脏的,计算它的高度并缓存。在委托的heightForRowAtIndexPath:回调中继续返回缓存的高度,直到单元格不再被标记为脏。
- 当用自定义视图重用单元格时,要避免通过调用layoutIfNeeded每次都对其进行布局。
- 避免透明的单元格子视图。
- 在快速滚动时,考虑使用界面外壳。当用户快速滚动列表视图时,虽然使用了所有的优化,但视图的重用和渲染仍然需要超过16毫秒,还有可能出现偶发的丢帖现象,从而导致不流畅的体验。在这种情况下,使用一个界面外壳是一个较好的选择,外壳可以被预定义,它的唯一目的就是告诉终端用户这些部分即将展示一些数据。当滚动速度降低,并低于阈值时,刷新最终的视图并填充数据。可以使用与列表视图相关联的panGestureRecognizer属性获取速度值。
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
CGPoint velocity = [self.tableView.panGestureRecognizer velocityInView:self.view];
self.velocity = velocity;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (fabs(self.velocity.y) > 2000){
//返回界面外壳
}else{
//返回真正的单元格
}
}
- 避免渐变、图像缩放以及任何屏幕外的绘制。
6.2.5UIWebView(iOS7),WKWebView(iOS8),SFSafariViewController(iOS9)
UIWebView是用于渲染未知或动态内容的最常见视图。通常情况下,会将web视图指向一些内嵌的HTML或web URL. 常用场景:
- 除了原生UI渲染登陆表单。如果想用CAPTCHA(验证码https://zh.wikipedia.org/wiki/验证码)筛选刷屏的机器人,就需要为所有的格式提供支持,并将其打包至应用中,或将用户指向一个网页登陆URL,让服务器生成任何需要的复杂UI。
- 在任何应用中显示隐私政策或使用条款。因为这些会随着时间变化,并且需要大量的格式化,使用原生视图不是较好的选择。
- 新闻或文章阅读器,因为大部分的文章都是Web创建的,几乎都是HTML。
- 邮件应用。例如,初始邮件是HTML形式,当呈现消息或跟帖,以及撰写回复时。
需要展示较小的富文本,使用UIlabel的NSAttributedString.
最佳实践:
- UIWebView可能比较笨重且迟钝,所以尽量复用web view。同时,UIWebView也因内存泄露而知名。无论何时想向用户展示新的URL,先将内容重置为空的HTML。这样就能确保web view不会将之前的内容展示给用户。想要实现这一功能,在loadRequest:方法后调用loadHTMLString:baseURL:即可。
- 实现webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 方法。要留意URL scheme。如果是http或https以外的东西,需要注意:应用应该知道如何处理这种情况,或警告用户该网站正试图脱离应用。
- 可以通过stringByEvaluatingJavaScriptFromString:方法创建一个桥来连接应用和JavaScript(iOS8后WKWebView的evaluateJavaScript:completionHandler: 代替),从而字当前已经加载的web页面执行JavaScript。
- 实现委托的webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error方法,保持对所有可能出现的错误进行追踪。如果域名与NSURLErrorDomain相等,那么NSError对象有不同的意义。
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if ([NSURLErrorDomain isEqualToString:error.domain]){
switch (error.code) {
case NSURLErrorBadURL:
//处理错误的URL
break;
case NSURLErrorTimedOut:
//处理超时
break;
case NSURLErrorUnknown:
//未知
break;
//其他。。。
default:
break;
}
}
}
- UIWebView不会通知任何的HTTP协议错误,例如响应是404或500错误。所以需要出发两次调用,第一次使用自定义的NSURLConnection调用,然后是通过web view的调用。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (self.shouldValidate){
[NSURLConnection connectionWithRequest:request delegate:self];
return NO;
}
return YES;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSInteger status = [(NSHTTPURLResponse*)response statusCode];
if (status >= 400){
//展示警报或隐藏web view ---不要展示错误的网页
}else{
self.shouldValidate = YES;
[self.webView loadRequest:connection.originalRequest];
}
}
-
嵌入的UIWebView的容器应该提供一下元素。
1.导航按钮。 2.重载按钮。 3.取消按钮,用于取消当前正在加载的页面。 4.用于展示标题的UILable. 5.退出web view的关闭按钮。
6.2.6 自定义视图
简单复合视图UI的基本实现可能包含以下内容: (1)UIImageView作为头像图片。 (2)用UIlabel的NSAttributedText展示用户名称。 (3)UITextView,展示主要内容,因为里面可能包含链接。 (4)一般数据展示UILable。
自定义视图则通过在drawRect:中直接绘制全部的元素。
1.复合视图 创建一个UITableViewCell的子类,勾选XIB file。然后直接在XIB排列所需元素。 针对复合视图,在动画过程中使用视图光栅化http://swift.diagon.me/shouldRasterize/
2.直接绘制 不勾选XIB文件选项,覆盖drawRect:方法以自定义渲染元素。
优缺点比较:运行时性能和代码维护 *直接绘图的自定义视图中的运行时性能更好。(通过首次初始化时间,后续初始化,滚动后的首次初始化,滚动后的第二次初始化,重用等对比) *从维护的角度来看,代码会难以维护和发展。一旦应用稳定下来,可以比较明确的将复合UI换成直接绘图。
6.3自动布局
自动布局使用本地约束(元素彼此之间的位置关系)会比使用全局约束(相对于父视图的位置)更快。 自动布局衡量展示视图和渲染视图所需的时间,如果超过了阈值,应该考虑使用自定义代码。阈值根据具体的应用而定。
6.4尺寸类别
ppi(像素密度)不是点到像素的比例。 尺寸类别:https://isux.tencent.com/ios9-guideline-ch1.html 尺寸类需要自动布局。如果因为性能原因不选择使用自动布局,那就不能使用尺寸类
6.5iOS8中新的交互特性
6.5.1交互式通知
交互式通知,允许用户提供一个针对输入的快速响应。 交互式通知的可能动作:
- 邮件:回复,标记为垃圾。
- 信息:提醒,回复。
- 在社交应用中评论信息:回复评论,点赞评论。
- 任务和提醒:稍后,标记完成。
6.5.2应用扩展
iOS8可用的应用扩展:
- 今日:通知中心的“今日”,帮助用户快速更新或完成某一任务。
- 自定义键盘
- 分享:允许用户更加无缝地跨应用共享数据。
- 行动:帮助用户查看或改变在主应用中发起的内容。
- 照片编辑:允许用户编辑照片应用中的照片或视频。
- 文档提供者:允许其他应用访问自己应用所管理的文件。