如何构建Twitter iPad用户体验

Aaron Brethorst,2011年5月16日

大约两个月前,我在Twitter上询问是否有人创建了开源版的Twitter iPad用户界面。之后,我在GitHub上看到了三个独立的项目,它们为你提供了一个很好的框架来构建自己的类似Twitter的iPad界面。今天,我们将看看我发现的一个项目,并看看如何在其之上构建尽可能接近Twitter iPad UI1的克隆版。

tl; dr

以下是将此构建所需的GitHub仓库

StackScrollView项目

我们将使用StackScrollView,考虑到它能做的一切,这是一个相当简单的项目。你可以访问Cocoa Controls页面查看,或者直接前往GitHub仓库

StackScrollView是由Raw Engineering(一家在孟买和旧金山设有办事处的网站和移动咨询公司)创建的。

一旦你克隆了GitHub仓库,请使用Xcode打开xcodeproj文件,并在模拟器中启动它。你将看到一个左侧导航面板和类似于Twitter iPad应用的滑动视图。

今天的课程

本文将介绍以下四个主题

  1. 基本外观和感觉更改。
  2. 圆角表格视图。
  3. 左侧导航面板的表格单元格、标题和页脚。
  4. 融入时间轴。

基本外观和感觉更改

首先,我们将用苹果提供的纹理滚动视图背景颜色替换StackScrollView项目中使用的背景,并移除导航面板右侧的垂直1px白色线条2

(查看此提交)

圆角表格视图

接下来,让我们解决Twitter在其堆栈上推送到所有视图上时看到的圆角问题。幸运的是,Cocoa Controls上还有一个项目显示了如何完成这个操作。Jeremy Collins的RoundedUITableView项目展示了如何复制Apple的Weather应用的经验,这碰巧也完全适用于我们的需求。

(查看此提交)

现在我们已经将RoundedUITableView类添加到我们的项目中,我们需要使用它。它并不完全是即插即用,但它很接近。

DataViewController.h中,为RoundedUITableView添加一个类声明,并将_tableView实例变量的类型更改为。

同时,在DataViewController的实现文件中,使用#import "RoundedUITableView.h",并根据需要调整表格视图的实例化。当你在这里时,将表格视图的背景颜色从透明更改为白色;否则,完成时看起来会很奇怪!

如果现在尝试运行项目,你会发现东西看起来不太对劲;表格视图仍然会有直角。为了解决这个问题,我们需要修改 RoundedUITableView 中的一个 -init 方法。你将会注意到 DataViewController 调用了 RoundedUITableView-initWithFrame:style:,但 RoundedUITableView 并没有显式处理。打开 RoundUITableView.m 文件,并将 -initWithFrame: 方法的签名修改为包括一个样式参数,并将其添加到对父类调用中。

最后,我们需要去掉在 StackScrollViewController.m 中设置的默认背景色。这很简单,只需在 665 行删除对 backgroundColor 设置器的调用。

查看此提交

回顾我们的成就和一些错误修复

我们取得了相当多的成就,而无需做很多工作。当然,已经出现了一些小问题。

  • 错误的圆角半径 – 我们堆叠视图控制器的圆角稍有不准确。目测来看,圆角可能应该是 6 而不是 10。
  • 从属堆叠视图控制器应该有方形左边距 – 如果你查看 iPad 版 Twitter 的屏幕截图,你会看到二级(以及更高级的)堆叠视图控制器的左边距是方形的。这是一个小细节,但仍然很重要。

修复圆角问题是极其简单的。为此,我定义了一个预处理器宏 kCornerRadius 将其设置为 6,然后使用它而不仅仅是直接将圆角值设置为 10.0,就像我们代码中之前所做的那样。

查看此提交

方形左边距稍微复杂一些。作为第一步,让我们为 RoundedUITableViewMask 类添加一个属性,以确定是否应该绘制所有圆角或方形+圆角。由于我们是手动处理所有角落的遮挡,结果是我们可以在 -setupView 中删除 cornerRadius 属性。最后,我们将修改 -drawRect: 方法以根据具体情况绘制正确的圆角或方形。

查看此提交

整合

让我们充分利用我们新发现的绘制正确角落的能力。我们将 squareCorners 属性贯穿到所有相关类。这可能不是最优雅的架构解决方案,并且一旦我们开始向应用中添加新的视图控制器类型,它可能不会很好地扩展,但至少目前它能足够地解决问题。

查看此提交

左侧导航面板表格单元格

在继续之前,我刚刚注意到我们应用的背景色是错误的。Twitter 使用纹理滚动视图的背景色,但它们的颜色明显变暗了。我们可以通过将 RootViewControllerbackgroundColor 在 99 行更改为 [[UIColor scrollViewTexturedBackgroundColor] colorWithAlphaComponent:0.5] 来轻松地模拟这种效果。

我们将使用来自 (非常出色的) CC BY 许可的 Glyphish 图标集 的图标来作为导航面板的图标。它没有包含我们所需的一切,但对于我们的目的来说足够好。一旦我们有了图标,我们将为单元格打下基础。

导航面板中将有 exactly six 个单元格

  • 时间线
  • 提及
  • 列表
  • 消息
  • 个人资料
  • 搜索

为此, easiest 的方法是为填充单元格创建并填充一个数组,该数组将存储所需的所有信息。我们将在 MenuViewController 中添加一个名为 _cellContentsNSMutableArray

在我们开始着手完全像素匹配的 UITableViewCell 替换之前,让我们花点时间确保一切按预期工作,并检查我们所做的更改。

(查看此提交)

继承UITableViewCell

创建自定义且功能正常的UITableViewCell子类可能是一个很麻烦的过程。我从2008年开始以来一直在专业进行iOS开发,这部分内容至今仍让我感到头疼!为了使事情更容易,我们将重用尽可能多的默认UITableViewCell。这意味着,我们不会指定新视图,而是将已提供的textLabelimageView控件稍微移动一下。

此外,我们还需要添加新的UIViews来表示每个单元格顶部和底部的1px线条,以及一个UIImageView来表示当你在时间轴上有新推文、新直接消息等情况时看到的“发光”效果。

表格视图单元格的内容应该相当直观。值得强调的一点是,我在-layoutSubviews方法中重新布局了textLabelimageView。这是在UITableViewCell子类中修改此类内容的正确位置,如果你正在编辑由超类提供的视图布局,那么这是唯一能够做到这一点的地方。

最后,imageView的精确宽度是根据堆叠视图控制器需要覆盖除单元格图像之外的一切的需求来确定的。

此外,我们还需要修改我们将显示这些单元格的表格视图控制器

(查看此提交)

此外,这是我们对新表格单元格的UI如何发展的快速查看(图标不错,对吧?3

表格部分标题

为了完成导航窗格体验,我们需要在表格中添加两个额外的元素

  1. 一个自定义表格部分标题视图
  2. 水印页脚

我们首先从部分标题开始。为了开始,我们需要在MenuViewController中实现两个新的UITableViewDelegate方法

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

-tableView:heightForHeaderInSection:的实现很简单:只需返回值70

-tableView:viewForHeaderInSection:的实现并不那么简单。我们最好创建一个自定义视图。让我们看看我们了解的内容

  • 固定大小为200×70px。
  • 用户头像图片大小为48×48px,位于视图的原点(即左上角)(11,11)。
  • 用户头像图片具有3px的cornerRadius。
  • 用户头像图片具有阴影效果。
  • 带有用户帐户名称的文本标签。从设计上看,它看起来与导航窗格中的其他文本相似,除了字体大小看起来像是labelFontSize

所以,让我们再构建一个自定义控件!我们将称这个为MenuHeaderView。这个控件基本上按照你期望的方式工作

请注意,目前,阴影和cornerRadius是互斥的。我们现在将显示阴影,稍后我们将回过头来处理这个问题。

(查看此提交)

水印页脚

为了完成导航窗格的外观和感觉,让我们添加水印页脚。这将是一个简单的自定义视图。它需要有一个与我们在其他地方使用的相同的半透明白色色调的顶部线条,然后是在其下方居中的我们的标志。与我们所做的其他东西相比,这应该是一件轻而易举的事情

最后,我们需要将我们的水印页脚视图连接到导航窗格的表格视图。只需要两行更改,包括#import

好了,我们现在越来越接近完成。让我们再次看看我们的用户界面是如何发展的。看起来不错!4

(查看这个提交)

整合时间线

很好,我们进入最后一个阶段:我们需要将时间线拉入并在中间的表格中渲染它。为了本次演示的目的,我们将避免异步网络操作,而是从项目中一个 plist 文件加载一些数据5

创建 TweetTableViewCell

我们的 TweetTableViewCell 将是 UITableViewCell 的另一个子类,它将具有四个相对简单明了的属性

  • imageView
  • authorLabel
  • tweetLabel
  • timestampLabel

创建单元格并安排其内容的过程与之前所见的过程几乎没有区别,只是有一个新的变化:由于推文的长度不同,无法提前预测包含一个推文的单元格应有的高度。因此,我们将在运行时调整单元格的高度。

为了实现这一点,我们需要向我们的应用程序添加两个新方法

  • DataViewController 中添加 -tableView:heightForRowAtIndexPath:
  • TweetTableViewCell 中添加 +heightForTweetWithText:

-tableView:heightForRowAtIndexPath:UITableViewDelegate 中的一个方法,允许其 UITableView 确定指定单元格的高度。在我们的案例中,我们通过调用 +heightForTweetWithText: 来确定 TweetTableViewCell 的高度并返回该值。

+heightForTweetWithText: 也相对简单,尽管它是硬编码的

我们计算单元格中每个固定元素的大小。然后,我们使用 NSString 类别方法 -sizeWithFont:constrainedToSize:lineBreakMode: 根据我们想要的字体大小确定推文的主体大小。最后,我们将该值截断到 int(为了避免当您尝试在屏幕上的非整数点上渲染字符串时出现的可怕、模糊的文本问题)。

(查看这个提交)

看吧,我们已经有不同的行高!

我们还需要对这些单元格做的最后一件事是正确处理时间戳。我们将首先填写 TweetTableViewCell 中的 timestampLabel 属性的详细信息,然后进入正题:从控制器为单元格提供相对时间戳。

为了完成任务,我们将转向一个名为 DistanceOfTimeInWordsNSDate 类别。这个类别让我们把一个 NSDate 转换成一个像“大约5小时前”这样的字符串。为了在我们的项目中使用这个类别,我们首先需要从 GitHub 上获取相关的文件并添加到项目中。然后,按照这个差异来了解如何将 NSDates 转换为有意义的相对字符串

以下是摘要6

  • 将一个新实例变量添加到头文件中,命名为 formatter
  • 初始化格式化器,提供一个区域设置,并且最重要的是,使用从 Stack Overflow 上找到的 Unicode 日期格式来调用其 -setDateFormat: 方法7
  • 使用 formatter 上的 -dateFromString: 方法将每个推文的 created_at 字符串值转换为 NSDate 对象。
  • -distanceOfTimeInWords 的结果分配给 timeStampLabel's text 属性。

一个相当重要的注意事项:创建 NSDateFormatters 并不便宜,所以如果这是一些生产代码,我可能会修改 -distanceOfTimeInWords 以接受一个日期格式化器作为参数,这样它就无需在每次 -tableView:cellForRowAtIndexPath: 通过时始终创建新的。

(查看这个提交)

总结

嘿,看这儿:我们完成了!从我们开始以来,我们在外观和感觉方面已经走了很长的路,但如果没有我们能够利用的所有惊人的免费组件,我们绝对不可能到达这里。

当然,您可以从我的个人GitHub账号获取完整的项目:获取完整项目

请告诉我您对这方面的看法,无论是通过Twitter还是下面的评论。如果感兴趣的人足够多,我很乐意继续制作第二部分!

1 模仿,赞美等。

2 我们最终将用2px的蚀刻线替换它,但这可以稍后再处理。

3 坦白说,我对图标所做的更改并不特别美观。我是一名软件开发人员,对用户体验有一定的控制欲,但不是一名图形设计师。

4 除了水印的图形设计,但我们在那方面已经确定了我的不足。

5 虽然值得提到的是,我创建这个plist文件的方式有点巧妙。我喜欢为这个原因使用Ruby...

6 哈哈,我有时真的很累。

7 我没有在SO的帮助下编写过许多Unicode日期格式字符串,这绝不是一种我喜欢的体验。