Project1:Storm Viewer:Swift简介

概述

摘要:从制作一个看图app和了解关键概念开始swift编程。

概念:Constants and variables, method overrides, table views and image views, app bundles, NSFileManager, typecasting, arrays, loops, optionals, view controllers, storyboards, outlets, UIImage.

1. 设置

2.删除基本代码

3.用NSFileManager展示图片

4.Interface Builder介绍

5.用UIImage载入图像

6.最后的微调:hidesBarsOnTap

7.总结


设置

在这个项目中你会完成一个让用户可以滚屏查看图像列表,然后选定一张仔细观察的图片浏览app。项目很简单,是因为做的同时又很多其他东西需要你学,所以提醒自己要努力——学完的时间会有点漫长!

启动Xcode,在欢迎界面中选择“Create a new project”。选择Master-Detail Application,然后点击Next。Product Name输入Project1,语言选择Swift ,设备选择Universal。

其中的Organization Indentifier这一项,要倒着填写你的个人网站域名,比如com.hackingwithswift 。(本来网址是hackingwithswift.com,现在倒过来。)如果你想在iPhone或iPad上测试你的app,你需要填写一些有效的地址,或者就填个com.example。

重要提示:有些Xcode的项目模板里有选项Use Core Data、Include Unit Tests和Include UI Tests。请确保在这个系列的所有项目中,这些选项都没有被勾选。

现在再次点击Next然后可以选择项目保存的位置——桌面就行。选好保存位置,你就可以看到Xcode为你准备好的示例项目。首要任务是确定所有的设置都正确,这样就可以保证项目按照预定的计划进行。

当你运行一个app的时候,你要选择它所要运行的iOS模拟器,又或者是你插到电脑上的实体设备。这些选项可以在Product>Destination 菜单下可以看到,还有其他如iPad2,iPad Air等等。

还有快捷键。在Xcode窗口的左上角有个play和stop按钮,按钮右边有个Project1和device name。你可以点击这个device name 来选择不同的设备。

在本项目中,请选择iPhone 5s,然后点击play按钮。这样一来代码会被编译,也就是将你写的代码转换成iPhone们可以理解的代码,然后启动模拟器,运行app。你会看到你跟app交互的时候,也就是点击“+”列表中会添加新的日期时间;点击“Edit”可以进入编辑模式,你可以更改或者删除日期时间,按住时间向左拖动也可以删除;点击时间可以在新页面中显示时间。

你可以随意开始或者终止你的项目,不论多少次。这里有些基本的忠告你得知道:

1.运行项目的快捷键是Cmd+R。

2.结束项目的快捷键是Cmd+.

3.如果你修改了项目,再按次Cmd+R。Xcode默认结束当前运行中的项目,然后另起一个。确定你已经勾选了“Do not show this message again”,这样就不会再有烦人的提问了。

这个项目是关于让用户选择一张图去看,所以你得导入一些图片。从GitHub下载相关的文件,从里面找到Project1 文件夹,在里面的Content文件夹里有你需要的图片。

我想让你直接把Content文件夹拖进你的Xcode项目中,就放在Info.plist下面。这时有个窗口会弹出来问你想怎么添加这些文件——确保“Copy items if needed”和“Create groups”已经勾选。

!:如果选择了“Create folder references”,项目就无法工作。

点击Finish后你就会在Xcode中看到一个黄色文件夹的图标。如果图标是蓝色的,表示你没有选择“Create groups”,你就会悲剧了。


删除基本代码

Apple的示例包含了太多我们不需要的代码,让我们擦除掉。打开MasterViewController.swift进行编辑。大约在17行的位置你会看到override func viewDidLoad() {,然后几行代码之后会有个}在28行,对,一行只有一个}。如果你不确定自己选对了},只要确定它跟override的o垂直对其即可。

没有行号?如果你的Xcode默认不显示行号,我建议你打开它。进入Xcode的菜单,选择Preferences,然后选择Text Editing标签然后勾选“Line numbers”。

当系统创建好视窗(screen),viewDidLoad()方法中的代码块就会被调用,你可以通过它来做一些初始配置。

这个方法(method)从func viewDidLoad() {开始,然后在不远处的 } 结束。{ 和 } 用来标记一大块代码。而方法名下面一行的缩进,让辨认代码块的起和止变得非常方便。说的够多了,现在删除这个方法中除了super.viewDidLoad()的所有代码,因为这些对于本项目来说完全没用。

Note:当我说删除内容时,表示除了方法名和 { } 以外,其他都要删除。所以,删完的结果是:

、、、

override func viewDidLoad() {

super.viewDidLoad()

}

、、、

override func viewDidLoad() {

super.viewDidLoad()

}

这个方法现在啥都不干,等会儿咱们会添加一些内容进去。

接下来,找到insertNewObject()方法,然后删除整个方法,包括insertNewobject(sender: AnyObject) { 和 } 。

swift中所有的方法都以func(func是function的缩写)开头,不论这个方法是用来完成什么任务的。只有一个小例外,不过在project24之前你都不会遇到,所以现在你可以简单的认为函数和方法说的是一回事。

最后,删除文件中的最后一个方法。它有个奇怪的名字,是Objective-C的遗留物。这个方法很长,虽然这里用不上它,但是你还是需要理解它表示什么意思。下面是方法描述:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)

让我们来分解下:

override :表示该方法已经被定义过了,我们想用新的描述来重写。如果不重写,原先的定义的方法就会被执行,在这个实例中新方法什么也不会做。

func tableView :方法的名字是tableView,听上去不是很有用。但Apple定义方法的方式是保证其中传递的信息——也就是参数——被命名的很有用。在这里,最先被传递的是被触发的表视图(table view)。表视图是在样例项目中包含所有日期的、可以滚动的东西,是iOS中一个核心组件。

tableView: UITableView :这就是刚才说的被传递到方法中的第一个参数——被代码触发的表视图。它包含两个信息:tableView 是方法中我们可以使用的表视图的名字,UITableView是它的数据类型——用来描述它是什么的数据。每一个以“UI”开头的都是Apple自带的开发工具,所以UITableView是默认的Apple表视图。

commitEditingStyle editingStyle: UITableViewCellEditingStyle :这一部分说明了方法最核心的内容——它打算做什么。通过它的名字,我们知道该方法需要一个表视图,但commitEditingStyle部分才是实际的动作:代码将会在用户尝试进入表视图编辑模式时被触发。也就是说,当用户修改表格的内容时,这些代码将会被调用。

还有:方法中有tableView: UITableView表示我们可以通过“tableView”这个参数引用表视图,但这里参数是commitEditingStyle,听上去就很傻很累赘。所以swift允许你给参数额外的名字:一个用在传递数据(这里是commitEditingStyle),另一个在方法内使用(editingStyle)。然后是冒号和数据类型UITableViewCellEditingStyle,意思是说,commitEditingStyle和editingStyle都是这个类型的数据。

forRowAtIndexPath indexPath: NSIndexPath:又是个不太清楚的参数名,但同时你也能看到他的用处:人们可以通过forRowAtIndexPath来调用这个方法,方法内部你可以直接使用indexPath。它的数据类型是NSIndexPath,包含了两个信息:所在表的部位和表的行号。这里我们不需要部位,但需要行号——在示例中,每个被插入的时间都是一行。

我不会假装Swift的方法长什么样和怎么工作是很好理解的事情,但现在如果你不是很懂也不需要太担心,因为几小时的编程之后它们就会变成一种第二本能。至少你得知道当你在用它们的名字的时候,是在用什么方法还有什么参数。Parameters without names are just referenced as underscores:_.

于是,你刚删除的方法名为tableView(_:commitEditingStyle:forRowAtIndexPath:)——很晦涩,我知道,所以人们才用最重要的部分来称呼它,比如“在commitEditingStyle 方法中。”

最后一点要删除的是很细微的东西:找到tableView(_:cellForRowAtIndexPath:)方法(接下来我会叫它cellForRowAtIndexPath)然后你会看到以下两行代码:

let object = objects[indexPath.row] as! NSDate

cell.textLabel!.text = object.description

我要你删掉as! NSDate 和 .description,然后最后你的代码是介个样子:

let object = objects[indexPath.row]

cell.textLabel!.text = object

当你改好之后,Xcode就会开始提示你这里有问题。这完全正常——还有更多的地方需要改动。

现在,找到prepareForSegue() 方法然后你会看到另外一个你需要删掉的as NSDate,删掉之后这行代码看起来是这样:

let object = objects[indexPath.row]

如果现在你可以运行项目,你会看到这货基本没用,因为添加按钮已经被删掉了。但你现在无法运行这个程序,因为还有问题需要修复。

有问题?OK。如果你不确定什么代码可以删除,我写了个特别的Xcode程序包含了本章中的所有改动。记住,现在的程序有问题所以跑不起来,但是下个章节就会修复。


用NSFileManager陈列图片

使用的照片来自于国家海洋和气象管理局(NOAA),这是一个美国政府机构,它网站上的内容我们都可以免费使用。一旦你的项目采用这些图片,Xcode会自动把它们内建到程序中,这样你就可以获取它们。

在后台中,一个iOS app实际就是一个包含了很多文件的目录:二进制代码(就是编译完成后的代码,可以运行),所有的app中用到的媒体资源,任何可视化排版(visual layout)文件,还有其他的比如元数据(metadata)和安全保障(security entitlements)。

这些app目录被称为bundles,而且他们有扩展文件(extension.app)。因为媒体文件在文件夹中很松散,所以我们可以让系统告诉我们都有哪些文件在内,然后可以拉出我们想要的来。你可能已经注意到这些图片名都是以“nssl”(National Severe Storms Laboratory)开头的,所以任务很简单:列出app目录中所有的文件,然后找到以“nssl”开头的那些。

就像我之前说的,方法viewDidLoad()从func viewDidLoad() { 开始到几行后的 } 结束。我们要在其中添加更多代码,就在super.viewDidLoad()下面:

let fm = NSFileManager.defaultManager()

let path = NSBundle.mainBundle().resourcePath!

let items = try! fm.contentsOfDirectoryAtPath(path)

for item in items {

      if item.hasPrefix("nssl") {

          object.append(item)

      }

}

我已经跟你说过任何以UI开头的数据类型都是Apple的iOS自带开发工具,但其实这只说对了一部分。UI表示用户界面,所以这些类型主要是跟用户可以接触到的东西有关——UITableView是表格,UITextField是文本输入框等等。但还有很多其它数据类型也是Apple提供的,比如这里的NSFileManager 和NSBundle。

长话短说,NS是Apple在97年买进的软件NeXSTEP的缩写。NeXSTEP开发的技术现在仍是iOS的核心部分。NSBundle 和NSFileManager是可以帮你完成伟大任务的数据类型,但他们没有一个视觉化的组件(component)。附带一提,这些NS数据类型同样在OSX和iOS中存在,然而UI的数据类型只存在于iOS。

好了,现在让我看看这些代码做了些什么:

let fm = NSFileManager.defaultManager() 代码定义了一个名为fm的常量然后将NSFileManager.defaultManager()返回的值赋予给它。这是个能让我们操作文件系统的数据类型,在这里我们用它来寻找文件。

let path = NSBundle.mainBundle().resourcePath!  这里定义了一个名为path的常量,其值被设置成我们的app目录中的资源的路径。所以,这行的意思是“告诉我在哪可以找到这些我添加进app中的图像”。

let items = try! fm.contentsOfDirectoryAtPath(path)  这里定义了第三个常量,名为items,被赋予path路径指向的目录下的内容。什么path路径?就是前一行代码返回的。如你所见,Apple的长方法名确实能让它们的代码具有自解释性!

for item in items {  这里开始了一个循环。循环是一堆被反复执行多次的代码。在这里,每当我们在app 目录(bundle)中找到一项时循环就被执行一次。注意这里还有个 { ,表示一个新代码块的开始,几行后可以找到对应的 } 。

每次循环进行时,所有在大括号中的内容都会被执行一边。我们可以将这行翻译成“把这些小项当成一系列的文本字符串,然后拉出其中的每一个,然后给他们命名为item,然后执行后面的代码块。。。”我们使用文本字符串是因为contentsOfDirectoryAtPath()返回的是一个文件名的列表。

if item.hasPrefix("nssl") { 这是循环中的第一行。此时,我们会得到第一个需要处理的文件名,即item。我们用hasPrefix()方法来确定这是否为我们想要的那个:它需要一个参数(前缀),然后返回真或假。

“if”在这里表示这一行是一个条件语句:如果item有前缀“nssl”,那么……是的,另一个左大括号去标记另一个新的代码块。这次,代码只会在hasPrefix()返回真时被执行。

objects.append(item) 这行代码只有在hasPrefix()返回真的时候被执行,它把适配的文件名添加到列表objects的末尾。这就是在Xcode样例项目中的,我们并没有创建过它。

寥寥几行代码,却有很多东西,总结下:

我们用let来定义常量。常量是我们要引用,但不会改变的值。比如,你的生日是个常量,但你的年龄不是——年龄是个变量,因为它总在变。

在其他开发者大量使用变量的时候,swift程序员更喜欢使用常量。这是因为当你真正开始码代码的时候,你会发现很多数据其实根本没有变化过,所以你最好让它成为常量。这让系统可以做一些优化,也可以增加安全性,因为当你想要改变一个常量的值时,系统会出现错误提示。

Swift中的文本使用数据类型字符串(String)。Swift字符串非常强大——无论你使用何种语言,都没有问题。

值的集合称之为数组,一次仅限存储一种数据类型。字符串数组写作[String],只能存储字符串。放进数字就会提示错误。有一种特殊的数据类型叫AnyObject,意思是说,你能想象的任何数据类型都可以被放在这个位置上。

关键字try!不太常用,这里表示“我知道调用这些代码可能会失败,但我确定它不会(失败)。”如果失败了,app就会崩溃。与此同时,如果代码无法执行意味着app无法读取它自己的数据,也就是说可能存在某些严重的错误,这就是为啥try!可以被用在这里。 

你可以用for someVar in someArray 来循环数组里的每一个元素。Swift逐个取出元素然后为他们各执行一次循环中的代码。

如果你十分善于观察,你可能注意到一个很小,很小的事情,这也是Swift种最复杂的部分。所以我会尽可能让它简单一点,然后慢慢展开:就是NSBundle.mainBundle().resourcePath!最后的感叹号“!”。这不是印刷错误,如果你删掉它,程序将停止执行,所以明摆着Xcode认为它很重要——而且它确实很重要。Swift有三种处理数据的方式:

        1. 储存着数据的变量或常量。比如foo: String 表示一个叫foo的字符串。

        2. 可能储存着数据的变量或常量,但我们不确定。这被称为可选类型(optional type),看起来是这样:foo: String? 你不能直接使用它,你必须先打开它。

        3. 可能储存着数据的变量或常量,我们实际确定它储存着——至少一开始被设置好了的。它被称为隐式解包可选类型,比如foo: String! ,你可以直接使用它(foo)。

当我跟人们解释这些的时候,人们总是混淆可选和隐式解包可选,很大程度上是因为他们看上去很相似。隐式解包可选——就是“!”们——为两个目的服务:让码代码更简单,也让众多的Apple API更好地兼容。

稍后我们会更深入地认识可选(optional),但现在重要的是NSBundle.mainBundle().resourcePath 是否会返回字符串。它返回的是可选字符串 String? 。在末尾加上“!”意味着我们强制将可选字符串解包,表示“我非常肯定返回的是字符串而不会是nil,所以请把它当做常规字符串给我。”

重要提醒:如果你解包的是值为nil的常量或变量,你的app将会崩溃。结果,有些人就称“!”为崩溃操作符就因为它很容易出错。try! 也是如此,很容易出错。别担心现在这些听起来很难懂——接下来你会经常用到它们,然后会越来越有感觉。

在我们进入下一步之前,还有个小改动要做:现在你知道AnyObject,String 和数组是什么了,你应该可以看到MasterViewController的顶部有一行var objects = [AnyObject] (),它把数组objects定义为AnyObject类型。我们做了很多改动,所以现在我们可以肯定我们要增加的objects一定是字符串,所以,我们可以把定义语句做如下改动:

var objects = [String] ()

这下Swift就会确信我们在objects中存取的都是字符串了,这让我们的代码变得安全。现在你的项目已经可以正常运行了,运行之后你能看到一张都是你的图片的名字的列表了。成功了!让我们让它变得更好玩儿……


Interface Builder 介绍

当你第一次运行模板app时你会看到,点击一个日期就会有新的页面自动跳出来显示日期。这是由一个从左到右的圆滑动画完成的,同时带有一个Back键这样你就可以回到先前的页面。你可能也已经注意到你可以从左边沿开始向右滑动来返回表视图。

所有这些动作iOS的两个重要的数据类型都已经为我们提供了:UISplitViewController和UINavigationController。凭这俩名字你可以猜到:

1.“UI”是指为iOS设计的交互组件。

2. “Controller”表明它提供了设计功能。

控制器是软件开发三位一体的一部分:模型(Model),视图(View),控制器(Controller)。理想情况下,你的app的每个部分都可以被分解成这三种类型:要么是模型(描述你正在操作中的数据的东西),视图(app的用户界面),或者是控制器(将数据在视图和模型之间传递的代码)。

现实中,事情几乎不可能如此清晰,臃肿的控制器问题随处可见:很多本该是模型和视图的代码最终进入到了控制器代码区。虽然不理想,但你可以回头重写得更整洁。(然而你根本不会这么做!)

现在你可能正在想,你根本没写任何区分视图控制器或导航控制器的代码,你是对的。这是因为Apple提供了一个可以直观修改app排版的工具——Interface Builder。在你的项目中选择Main.storyboard,然后IB(Interface Builder)就出现了。

IB(也被认作storyboard editor) 是为了展示程序的视觉流程而设计的。你可能需要拉远,尽管你的app很简单,但用户界面相当的复杂!同时按住Cmd和“-”一次来缩小一个视图水平。现在你可以看到屏幕中有五个方块:一个是分割视图控制器(Split View Controller),两个导航控制器(Navigation Controller),还有一个表视图(Table View)和一个详情页("Detail view content goes here.")。

在方块之间有从左到右的箭头,显示程序流程。这些叫做segues(切换标志)。中间像个哑铃的叫关系切换,用于连接分割视图控制器和两个导航控制器,还有两个导航控制器和它们右边的方块。

视图控制器我们等会儿说,现在你需要知道这关系切换描述的是内嵌内容页面。也就是说,顶部导航控制器和表视图之间的关系是表视图内嵌于导航控制器中。

分割视图控制器和导航控制器之间的关系切换有点复杂,但也相当聪明。它内嵌了两个导航控制器,但不能同时呈现。因为之前我们要求模拟器模拟的是iPhone5s。

如果你把模拟器改成iPad2再运行一次,你会看到新东西:竖直屏幕时,Master在顶部,你点击图片的名字时图片详情页面会从左侧滑入;而如果你按下键盘上的Cmd+→ ,你会同时看到表视图和详情视图,因为屏幕自动分成了两部分。

这就是Apple所说的“用户界面自适应”,就是说你只要设计过你的app一次,不同设备上的iOS系统就会根据自己的设备信息来决定最好的呈现方式。现在我们已经看到同一信息的三种不同的呈现方式:iPhone,iPad竖屏和iPad横屏,而代码完全相同。虽然分割视图控制器和两个导航控制器略显复杂,但结果是多设备的全适应。

再回到IB中,在顶部的表视图和底部的导航控制器之间出现了第二种切换标志(segue)。这种切换标志看起来像两个长方形中间被向左箭头穿过,它叫“显示详情”切换。你已经看到它创建的动作了:从右侧滑入的详情视图控制器。这是个自适应的切换标志,“iPhone中动画从右侧进入,在iPad中只是改变详情视图控制器。”

现在正好是个简单接触视图控制器的好机会,因为它是iOS中最重要的组件之一。数据类型UIViewController代表app中所有的页面,而且自带异常丰富的内建功能。比如,它可以跟着设备旋转,它可以在内存不足时响应,它可以隐藏其他视图控制器等等。

视图控制器也可以用一种聪明的方式来代表app中的部分页面。比如,iPhone上的邮箱app有个视图控制器显示的是收件箱(inbox)中的信息,当你点击其中一条信息时,会有一个新的视图控制器来显示信息详情。

这样的设置对iPhone来说简直完美,特别是屏幕空间不足,一次只能做一件事的时候。但在iPad端,同样的邮箱app就会同时显示信息(屏幕左侧)和详情(屏幕右侧),即同时显示两个视图控制器。

屏幕背后,用的就是同一个分割视图控制器,而iOS只是确保右排版(right layout)被自动使用。为代码的反复利用欢呼!

在我们当前的app中,我们有五个视图控制器:分割视图控制器、两个导航控制器、我们的主视图控制器(即表视图)和详情视图控制器。所有的数据类型都是UIViewController,但是利用继承技术添加了他们各自的功能——每个定制视图控制器逐字逐句地继承了UIViewController的所有功能,然后加上他们自己的。

所以,分割视图控制器负责根据设备选择右侧布局,导航控制器在顶部加上标题,主控制器上有表视图和我们用NSFileManager写的代码,详情控制器有显示被选中日期的文本。你可以使用多层次继承,比如数据类型D继承自C,而C继承自B,B继承自A。听上去有点复杂,但是这意味着你要尽可能提高代码的利用率。

好,说够了就该动动手了。我们需要修改详情视图控制器因为现在它的中间是显示日期的文本。我们想显示的是图片而不是日期,所以我们得重做用户界面。双击详情视图控制器以放大和选中,在中间你会看到内有“Detail view content goes here”的UILabel,用于显示用户无法修改的文本。选中并删除它。

我们要用一个大大的UIImageView来替代这个UILabel,是的,就是一个用来显示图片的UI组件。iOS有一个顶呱呱的视图类型集合,你可以从中拖拽任何你想要的到故事板(storyboard)中,所有这些都存储在对象库中。它就在Xcode的右下角,如果你看不到,可以使用快捷键Ctrl+Alt+Cmd+3。

有些用户把对象库设置成图标视图,也就是你会看到一个箭头带着一系列的黄色圆圈。这对初学者来说没用,所以在对象库的底部你会看到一个搜索框,点击作伴的按钮就会以列表的形式呈现对象库。

对象库包含所有可以使用的内建视图类型,你会发现数量不少。晚点你爱看多少就看多少,但现在请使用搜索框:输入image来找到图像视图组件(Image View component)。选中按住鼠标左键然后将它拖进详情视图控制器,然后松手。现在拖动它的边沿让它覆盖整个视图控制器——甚至包括名为Detail的灰色导航条。

现在的图像视图没有内容,所以只有蓝色背景和Image View的字眼。我们现在不会把任何内容赋予它——这是我们将要在程序运行中做的事。相对的,我们需要告诉图像视图如何根据我们的屏幕去调整自己的大小,无论iPhone还是iPad。

一开始可能会觉得奇怪,说到底你只是让它充满了视图控制器,然后它俩一样大了,不就这样了?但,不确切。一开始,详情视图控制器是正方形的,但你看过正方形的iPhone嘛?如果你要支持各种设备,那又会怎样呢?所以图像视图到底该如何响应?

iOS有自己的解决办法,而且看上去像是一种魔法。它叫做自动排版(Auto Layout):它让你定义一些关于视图的规则,然后它能确保规则被很好的遵守。但它自己有两条你必须要遵守的规则:

你的排版规则必须完整,也就是说,你不能只规定X方向上的要求,你也必须规定Y方向上的。X从屏幕左侧开始,Y从屏幕顶部开始。

你的排版规则必须不会互相冲突。比如,你不能规定一个视图离左边界10点,离右边界也是10点同时宽度还是1000点。而iPhone5s只宽320点。自动排版会尝试通过打破规则来解决这些问题,但最终结果肯定不是你想要的。

你完全可以在IB中创建自动排版规则,也就是限制条件(constraints),而且它会在你没有遵守规则时提示你,甚至自动修正你的错误。

我们将要创建4条限制条件:图像视图的上、下、左、右各一条,以此来无视详情视图控制器地进行填充。添加限制条件的方法很多,最简单的是Pin按钮,就在IB的底部右侧。

右下角有4个按钮:第一个是三个叠加长方形中间带一个向下箭头,第二个是上下俩长方形,第三个是两根线夹一个正方形,第四个是两根线夹一个三角形。全都没有名字——谁说Apple总在用户界面设计上表现的很出色?我们想要的是Pin按钮,也就是第三个。你可以把鼠标放在上面看看工具提示,就是Pin。

选中图像视图,然后点击Pin按钮。这时会有个窗口弹出,名字是Add New Constraints。反选“Constrain to margins”,点击上面的四条虚线,然后他们就会变成实线。

这样就会创建规定图像视图的四边跟详情视图四边的距离限制条件了。当你把上面的虚线变成实线时,底部的按钮就会变成“Add 4 Constraints”,也就是增加四条限制条件,现在点击它就完成自动排版的设计任务了。

看起来你的排版没啥变化,但确实有些细微的不同。首先,在详情视图控制器中的UIImageView周围会有一圈蓝色细线框,这是IB在提示图像视图的自动排版没有问题。

接下来你会在文档概要框中的image view下看到新出现的Constraints。如果你不太清楚,或者你的 文档概要框被隐藏了,选中图像视图(image view)然后进入编辑菜单(Editor),选择Reveal in Document Outline 来显示文档概要和高亮image view。4条限制条件全都隐藏在Constraints项目下,你可以点开来逐个察看。

限制条件完成后,还要在IB中做一件事,就是把我们新建的图像视图跟代码连接起来。在界面中放置好图像视图显然是不够的——如果我们想在代码中引用它,我们还得为它创建一个跟排版直接相关的属性。

啥叫属性?你已经见过常量(用let定义)和变量(用var定义),但这些都是临时的所以只能用到方法结束的时候。属性是指附属于某个数据类型的常量或者变量。这里的图像视图就是我们为详情视图控制器创建的一个属性,它跟详情视图控制器同时出现同时消失,它属于详情视图控制器。

Xcode的样板项目的详情视图控制器中有个UILabel,而且有个对应的属性。我们会删掉它然后再创建一个。

为了让这过程变得简单点儿,Xcode有个特别的布局叫辅助编辑器(Assistant Editor)。它会将Xcode的界面一分为二,上半部分是IB,下半部分是对应的代码(也就是详情视图控制器的代码)。

Xcode根据IB中选中的对象来决定显示哪一部分的代码,所以,确保选中图像视图,然后点击菜单栏中的View>Assistant Editor>Show Assistant Editor,也可以通过快捷键Alt+Cmd+Return实现,又或者点击辅助编辑器按钮,也就是Xcode窗口右上角六个按钮中的第二个,看起来像是俩重叠的圆圈。

右上角这六个按钮分别是:标准编辑器(the standard editor),辅助编辑器(the assistant editor),版本编辑器(the version editor),然后是控制左窗格、下窗格和右窗格出现/隐藏的按钮。在这六个按钮下面的是很多探测器,最常用的是其中的第3、4、5个。

现在可以你看到上面的窗格是IB中的详情视图控制器,下面的窗格是我们还没看过的DetailViewController.swift。这是管理详情视图控制器的代码,在顶部你会看到以下代码:

@IBOutlet weak var detailDescriptionLabel: UILabel!

这里有些新内容,让我们分解下:

@IBOutlet:这个属性用于告诉Xcode这行代码跟IB之间有关联。

weak:weak告诉iOS我们不会将这个对象放入内存中。这是因为这个对象已经被放到视图中了,所以它属于视图。

var:定义一个新的变量或变量属性。我们已经用过let来定义常量了。记住,一个常量的值只能被设置一次,而一个变量的值可以被改变。

detailDescriptionLabel:这是赋予使用的UILabel的名字。注意名字中大小写的使用规则:变量和常量必须以小写字母开头,然后接下来的单词的首字母则换成大写的。比如myAwesomeVarible。这叫做驼峰拼写法。

UILabel!:这申明了属性的类型为UILabel,而且再次看到了隐式解析可选符号:!。这表示UILabel可能在那或者不在那,但我们可以确定它一定在那所以可以用符号“!”。

如果你还在理解隐式解析可选中挣扎,这行代码可能会让它简单点儿。你看,当详情视图控制器被创建出来时,它的视图还没有载入——它只是一些运行于CPU中的代码而已。

当所有基本要素都完成(比如给它分配了足够多的内存),iOS继续载入故事板中的排版布局,然后把所有的IBOutlet跟代码进行关联。

所以,当详情控制器刚被创建时,UILabel还不存在因为还未被创建——但我们还需要给它留一些内存空间。此时,属性为nil(空),即空内存。但当视图载入和输出口(outlet)连接完成,UILabel会指向一个真实的UILabel,而不是nil,这样我们就可以使用它了。

简单说,UILabel始于nil,然后在我们使用之前它被赋予一个值,这样我们就可以确信想使用的时候它的值不会是nil——一个文本版的隐式解析可选。

(PS:如果你还是不懂隐式解析可选,完全OK——保持继然后它们就会变得越来越明朗。)

返回项目中:我们没有UILabel,所以你可以删除整行代码。别担心:学@IBOutlet的知识并不是虚的,我们接下来要为图像视图创建一个新的输入口(outlet)。在图上操作就可以完成创建:按住Ctrl,鼠标左键点击UImageView然后拖到代码窗格中的@IBOutlet原来的位置上。如果你忘记掉了,它原来就在class DetailViewController: UIViewController {这行下面。

当你Ctrl拖拽时,会出现一条蓝线。当你鼠标移动到代码区时,会出现个提示“Insert Outlet or Outlet Collection(插入输入口或输入口集合)”,同时第二条水平蓝线会出现在你将要插入outlet的位置。

松掉鼠标左键时会弹出一个窗口让你填如一些信息。你需要做的是给创建的outlet一个名字。所以在名字栏输入detailImageView然后点击连接(connect)。完成后你会看到这行代码:

@IBOutlet weak var detailImageView: UIImageView!

这跟刚才删掉的代码没啥区别,但现在这是个图像视图而不是标签。至关重要的是,如果你看这行代码的最左边,你会看到一个带圆圈的灰色圆点。这是Xcode在告诉你这个outlet已经跟IB连接上了。如果没有,你看到的会是一个不带点的圆圈。


用UIImage载入图像

为了改变布局,我们破坏了代码——你会注意到代码编辑器中出现了一条红线,Xcode顶部还有个红色的警告标志。这是因为DetailViewController.swift的其他部分指向的是UILabel,而我们刚把它给删了。所以我们需要做点小改动让它知道怎么去操作我们的新数据,然后修改下MasterViewController.swift来正确地传递新数据。

首先,在DetailViewController.swift中找到以下代码:

var detailItem: AnyObject? {

      didSet {

           // Update the view.

           self.configureView()

      }

}

这里创建了一个名为detailItem的属性,类型为AnyObject?——Swift这是在表明它可能是个什么类型的对象,或者什么都不是。但这个属性有个弯儿,因为附带了一个属性观察器,就是用didSet的形式。

每当属性的值改变了之后,didSet属性观察器中的这块代码就会被执行。还有个对应的willSet属性观察器,是在属性改变之前被执行的,没didSet这么常用。

这里的didSet被用来调用self.configureView(),“调用我自己的configureView()方法。” self.其实并不必需,所以可以删除。你可能对用“self.”来调用变量或方法的两类程序员的思路感兴趣。

第一类人从来不喜欢用self.除非逼不得已,因为用self.被使用的时候代表了十分重要的意义,所以用到其他地方会让人困惑。另一类人则能用就用,甚至一些不必要的地方。公平起见,到处使用self.是个OC中的好习惯,而且习惯很难改。

在我们修复错误之前,我们再多介绍点知识。detailItem被定义为AnyObject?类型,挺蠢的。我们已经知道什么类型的数据将被主视图控制器传输,因为我们操作的就是一组字符串。尽管是个可选类型,但detailItem只能是String。

所以,把AnyObject?改成String?,然后Cmd+B编译你的项目。这会产生另一个错误,这没关系,因为我们即将修复它!

错误出现在DetailViewController.swift中的configureView()方法内。现在的代码是:

if let detail: AnyObject = self.detailItem {

      if let label = self.detailDescriptionLabel {

            label.text = detail.description

      }

}

detailItem属性是个可选数据类型:原来是AnyObject? ,现在是String? 。这意味着要先解析它的值然后再使用它——我们需要确认其中是否有值,如果有,就取出来。最常见的可选类型取值方法是用“if let” 条件语句,也就是现在的代码正在做的。比如:

if let foo = bar {

      doStuff(foo)

}

这些代码的意思是“看下变量bar是否有值,如果有则把它的值赋予变量foo,然后调用doStuff()并把foo作为参数传入。”此时,如果bar是String?类型,foo就会是String类型因为它不再有可选项了。如果bar没有值,doStuff()永远不会被调用。“if let”句法的好处是一次性完成检查和解包的任务。

想到这里,让我们再看次代码:

if let detail: AnyObject = self.detailItem {

        if let label = self.detailDescriptionLabel {

                label.text = detail.description

        }

}

你会看到它首先检查detailItem是否有值,如果有就将它解包,并值取出来放到一个名为detail的新常量中。接着检查detailDescriptionLabel是否有值,如果有就取出其值,放到另一个名为label的新常量中。如果两次解包都有值,那就把detail.description赋值给label的text属性。因为日期本来就在,所以代码会以文本的形式来显示日期。这些代码出错是因为我们不再有label,所以将其改写为:

if let detail = self.detailItem {

       if let imageView = self.detailImageView {

               imageView.image = UIImage(named: detail)

       }

}

让我们看看都改了些什么:

我们不再需将detail定义为AnyObject,因为self.detailItem是String?类型而不是AnyObject?

我们将detailImageView解包为imageView,因为我们现在用的是图像视图而不是标签。

我们现在设置的是图像视图的图像,而不是标签的文本。

新代码引进了新数据类型,叫UIImage。它名儿里不带View所以它不是个视图——用户看不到它。UIImage是一种用来载入图像数据的数据类型,比如PNG或JPEG。

当你创建UIImage时,它有个named参数用于存放选定的图片名。接着UIImage就会在app的目录下寻找到这个图片名,然后载入到app中。通过detail这个常量,也就是detailItem解包得到的,UIImage就会载入用户选择的图片。

这是DetailViewController.swift中唯一的错误,但你可能比较好奇选中的图片名是怎么从主视图控制器传入到详情视图控制器中的。想知道就看看MasterViewController.swift。

图像名的传递是由MasterViewController.swift中的prepareForSegue()方法完成的。当跳转将要发生时,该方法就会被调用,给你个机会给新的视图控制器设定信息。这就是我们告诉详情视图控制器哪个文件被选中的地方,然后完成操作的代码是下面的部分:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

      if segue.indentifier == "showDetail"  {

             if let indexPath = self.tableView.indexPathForSelectedRow {

                  let object = objects[indexPath.row]

                  let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController

                  controller.detailItem = object

                  controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()

                  controller.navigationItem.leftItemSupplementBackButton = true

              }

       }

}

代码已完整,但只有三行是重要的:

let object = objects[indexPath.row]

let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController

controller.detailItem = object

为了让它好懂点,我们要改写它。用下面的代码来替代上面的三行:

let navigationController = segue.destinationViewController as! UINavigationController

let controller = navigationController.topViewController as! DetailViewController

controller.detailItem = objects[indexPath.row]

第一行寻找跳转的目标视图控制器,就是“现实详情(show detail)”的跳转目标。如果你还记得,目标是导航控制器,所以这行代码的作用是让目标控制器被当做导航控制器来处理。翻译一下就是:“我知道你以为目标视图控制器会是个常规的UIViewController,但信我:实际上那是个UINavigationController。”

当prepareForSegue()被调用时,新的视图控制器在创建之后即将出现,改造新视图得提供它需要的数据,所以确定新视图控制器的类型很有必要。我们可以通过读取segue.destinationViewController属性来获得新的视图控制器,但问题是它可能是任何一种视图控制器——Swift确实知道它是某种确定的UIViewController,但不知道具体是哪种。

有时这并不重要。如果你只是想展示一些东西而且不关心它的值,你不需要做任何的类型转换,甚至不需要这些代码——跳转会自动发生。但prepareForSegue()很特别,可以让我们在视图控制器显示之前做任何定制化操作,这里就是给详情视图控制器初始化一个属性。

我们需要把destinationViewController的类型转换为UINavigationController是因为第二行会发生的事:导航控制器有个名为topViewController的属性指向现在正显示的任何视图控制器。对我们来说,就是我们的详情视图控制器,但iOS并不知道——它认为topViewController就是个常规的UIViewController。所以我们需要再次使用as! 改写它:我们就是在告诉Swift这个对象的数据类型确实是DetailViewController。

最后,一旦我们发现了导航控制器(第一行),发现里面的详情视图控制器(第二行),我们就可以把详情视图控制器的属性detailItem改成选中的图片。把属性detailItem设置成objects[indexPath.row]就是为了做这件事。

属性objects,是我们从app目录中载入的一个字符串数组。数组是一个接一个排列的一组值,你可以通过从零开始的数组脚标来读取,objects中的第一个元素是objects[0],第二个是objects[1],第十个是objects[9],以此类推。

所以,什么是indexPath.row?indexPath是指表格中出现的几行——比如,用户点击来触发跳转的那些。它的属性row是指在表格中的位置,所以,objects[indexPath.row]意思是“获取用户点击位置的数组objects中的元素”。


最后的改进:hidesBarsOnTap

现在,你有了个可以干活的app:你可以按Cmd+R来运行下,拨动列表中的图片,然后点击一个来看大图。在完成项目之前,还有两个小改动来让它更闪亮。

首先,你可能已经注意到所有的图像都是拉伸来填满整个屏幕。这并不是个以外——这是UIImageView的默认设置。几下就能改好:选中Main.storyboard,在详情视图控制器中选择图像视图,然后选择属性观察器(Attributes Inspector),就是Xcode界面右上角窗格六个观察器中的第四个。

如果你懒得找,那就Cmd+Alt+4。拉伸是视图模式的默认选项“尺寸适应Scale to Fit”,改成视图适应(Aspect Fit)。

Aspect Fit让你能看见整幅图,Aspect Fill让显示中没有空白部分——这意味着剪短图片的一部分,不是横向就是纵向。如果你使用Aspect Fill,图像会充分地利用它的视图区域,所以你得确保Clip Subviews已经勾选来避免太多的图像。

第二个要做的改动是允许用户使用全屏幕来看图,就是隐藏掉导航栏。很简单,UINavigationController有个属性叫hidesBarsOnTap。当它被设置为真,用户就可以触碰当前视图控制器的任何位置来隐藏的导航条,再触碰下来重新显示它。

警告:在iPhone上你得小心点设置它。如果让它一直被处在开启状态,它会影响到表视图的触碰,特别是用户想选择东西的时候会引发大灾难。所以在看详情视图控制器时需要激活它,在隐藏时使之无效。

你已经见过viewDidLoad()方法了,就是在视图控制器的布局完成之后调用的。当视图就要显示时、已经被显示时、显示完成之前、显示完成之后,还有好些其他方法被调用了。它们是:viewWillAppear(),viewsDidAppear(),viewWillDisappear()和viewDidDisappear()。我们要用viewWillAppear()和viewDidDisappear()来修改hidesBarsOnTap属性让它只有在详情视图控制器显示的时候启动。

打开DetailViewController.swift,然后直接在viewDidLoad()方法下面添加两个新方法:

override func viewWillAppear(animated: Bool) {

        super.viewWillAppear(animated)

        navigationController?.hidesBarsOnTap = true

}

override func viewWillDisappear(animated: Bool) {

       super.viewWillDisappear(animated)

       navigationController?.hidesBarsOnTap = false

}

几条重要的提示:

我们在每个方法都使用了override,因为它们已经在UIViewController中被定义过了,而且我们要用我们自己的。别担心自己不知道什么要用什么时候不用,因为如果你不用它,Xcode会用出错来提示你。

两个方法都只有一个参数:动作是否被激活。这里我们并不十分关心这一点,所以这里我们会忽视它。

两个方法都用了super前缀,意思是“告诉我的父数据类型这些方法被调用了。”在这里表示它把调用的方法继承到UIViewController中,也就是它会自己完成自己的进程。

两个视图控制器都有一个可选属性:navigationController,可以让我们引用本身所在的导航控制器。可选是因为不是所有的视图都在导航控制器中。在Swift中你可以在语句中使用问号来评价一个可选项,而语句只有当可选可以被解包时会被执行。所以,如果我们不在导航控制器中,hidesBarsOnTap就没用。

如果你现在运行app,你可以轻触图片来看完整尺寸的图片,它再也不会被拉伸。当你看图时,你可以轻触图片来显示或隐藏导航栏。搞定!


总结

项目很简单,但你已经学到很多关于Swift,Xcode还有storyboards的东西。我知道这不简单,但是相信我,你已经通过最难的部分了。

为了让你知道你学到多远了,这里是我们涉及到了的东西:常量和变量,方法重写,表视图和图像视图,app目录,NSFileManager,类型转换,数组,循环,可选项,视图控制器,故事板,输入口,自动布局,UIImage等。

很大的量,同时很残酷的是你已经忘掉一半了。但是没问题,因为我们总是通过重复来学习,所以如果你继续跟着剩下的系列来学习,你会一次又一次的重复它们直到它们对你来说易如反掌。

如果你想花更多时间在这个app上,试着调查两个视图控制器的title属性。这能让你定制顶部导航条中的文字——让它显示选中图像的名字是一件很简单的事。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容