Swift 开发 iOS 应用教程
苹果最近宣布了一个改变iOS应用既往开发的一个大变化,一种取代Objective-C称为Swift的完全不同的编程语言。我正在努力学习这门新语言,而且我决定将按照我的学习进程定期发布并共享我所找到的内容。这是众多主题的第一篇文章,我希望你能跟随下去!
下面的Swift代码例子今后有极大的可能会改变。这主要是因为我开发风格是先写代码来测试理念,然后再重构,这主要是因为我(也可能是大家)对Swift完全是新手且正在学习。因此正如我过去学东西一样,随着时间的推移本教程也会随之改变。我会根据需要更新的示例代码,但没有学习过程时这么多,验证代码也是如此。我觉得这样也是个有帮助的过程。
所以我要从一个非常基本的应用开始,来解释代码如何工作。准备好了吗?让我们开始吧。
基础
Swift没有遵从标准的变量声明模式——将变量类型放在变量名前,而是选择类似JavaScript的‘var’关键字来定义所有变量。
比如你在Objective-C中这样实例化:
NSString *myString = @"This is my string.";
你现在需要这样:
var myString = "This is my string."
同时常数使用 ‘let’关键字来设定
let kSomeConstant = 40
在这种情况下kSomeConstant被隐式地定义为一个整数。如果你想更详细也可以指定类型,像这样:
对于数组和字典,使用方括号[]来描述
var colorsArray = ["Blue", "Red", "Green", "Yellow"]
var colorsDictionary = ["PrimaryColor":"Green", "SecondaryColor":"Red"]
其他还有很多,但是我认为这些基础对作为教程的开始很重要。下面,让我们开始“Hello World”。
Hello World
首先,我们会写一个最简单的起步应用,Hello World。
这个应用只做了一件事:在控制台上打印出"Hello World"。你想要跟上技术前进的潮流的话,就需要下载XCode,这要求你有一个开发者账号。如果你已经有了开发者账号,那么就请前往http://devloper.apple.com ,获取XCode。
至此,你已经建立好了集成开发环境。现在就可以向控制台打印hello world了。这个例子不仅仅向你展示了可以完成构建的最简单的应用,同时更为重要的是证明了你已经正确地建立了开发环境。
使用单一视图应用模板建立Xcod项目,然后确定你选择使用Swift语言。在项目的目录树里,现在你应当能够看到文件AppDelegate.swift。 在这个文件里,你可以找到如下一行:
"// Override point for customization after application launch."使用下面神奇的hello world代码替代上面这行:
println("Hello World")然后,按Run,你将会看到一个空白的应用启动起来,接着在控制台上打印出词语"Hello World"。恭喜你,你已经用Swift编写出第一个应用!虽然这个应用不可以获得任何赞许,却可以让我们基于它进行更深入的探讨。
添加表格视图
在这一小节了,我们将真正地给屏幕上放置一些素材,非常好玩!
请先在Xcode里打开Main.storyboard文件,然后从对象库拖动"Table View"对象。把这个对象放置在应用窗口里,并使它填满整个屏幕,然后确保表格视图与各个边对齐。此时如果你运行此应用,你将在模拟器里看到的是一个空的表格视图。
现在,我们需要给表格视图设置delegate和数据源。使用接口构建器做这些工作非常简单。只要一直选中表格视图,然后点击,并拖拉表格视图到storyboard文件列表里的"视图控制器“对象里。紧接着选择”数据源“即可。类似的可以设置"delegate"。
好了,现在我们就可以更深入的看看表格视图的通信协议的处理方法了。由于我们在视图控制器里使用了UITableViewDataSource和
UITableViewDelegate,所以我们需要按照如下方式修改类的定义。
打开ViewController.swift,修改如下行:
class ViewController:UIViewController {
为
class ViewController:UIViewcontroller,UITableViewDataSource,UITableViewDelegate{
在上面两个通信协议中的任何一个点击+号就会在最前端显示所有的函数。在当前的表格视图里,我们至少需要两个函数:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
现在,我们修改视图控制类,添加下面两个函数:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { return 10 } func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! { let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell") cell.text = "Row #\(indexPath.row)" cell.detailTextLabel.text = "Subtitle #\(indexPath.row)" return cell }
第一个函数是获取表格所占用的行数,在这个例子里我们只是硬编码其为10,然而通常这个值是表格视图控制器数组的长度。现在例子里这么做纯粹为了简单。
第二个函数是发生变化的魔法所在。在这个函数里,我们创建了调用每个表格的UITableViewCell实例,而且标题风格为表格所带风格。
接着,我们给每个表格赋予文本串"Row#\(indexPath.row)"。
在Swift里,就是这样在字符串中嵌入变量的。现在我们正在做的事情就是通过在字符串中嵌入(\indexPath.row)来获取indexPaht.row的值,可以用表格的行数动态的替代这个值。这样就会产生下面结果:"Row #1","Row #2"等等。
这个详细文本标签之在子标题单元格中可用,就是我们在这使用的这个。我们把它设置为类似于“Subtitle #1″,“Subtitle #2″,等等。
继续往下来,运行你的应用,你将看到一个让人惊讶的单元格列表,它显示有标题、副标题以及行数。这是在iOS中显示数据最常用的方法,这必然会对你有所帮助。想看我视图控制器文件完整的源代码的话,看这里吧:ViewController.swif。
在第二部分中,我们将探讨一个应用,这个应用使用iTunes的搜索API来发现并显示iTunes商店里的专辑。
在第一部分中我们研究了一些Swift的基础,并搭建了一个简单的示例项目,创建了一个表视图并在其中放置了一些文本。如果你没读过,到这里读一下吧。
在本节中,我们要做一些更有抱负的事。我们将要接触到iTunes商店的搜索API,下载JSON结果,将它们解析为字典然后用这些信息填充我们的Table视图。然后,我们将为Table视图添加单击事件来添加一些用户交互,当点击一个项目后iTunes商店的相应项目将被打开。
这听起来好像工作量很大,别担心。这些都是iOS应用非常基本的功能,它也是每个开发者都要做的最普通的事情。那我们继续吧。
连接到UI
我们需做的第一件事是得到一个Table试图的引用,以便在在代码中使用它。继续往下走将下面这行添加到viewcontroller.swift文件中,放在类定义的下方,但要在任何函数之外。
<p>@IBOutlet var appsTableView : UITableView</p>这段代码可以连接我们分镜头中的Table试图到“appsTableView”这个变量中。保存该文件并打开该分镜。现在,通过ctrl键+单机+拖动,将Table视图拖动到“View Controller”对象,这样便完成了这些对象的连接。容易吧?
使用API进行网络请求
现在我们已经实现了与UI的关联,下面就准备进行API调用。创建一个名为searchItunesFor(searchTerm:String)的函数。我们用它来实现对任意搜索词的网络请求。
为了保持这边文章简短,我将只发布结尾处的代码,并对部分注释进行说明。我们总是在说明部分提出几个问题,然后再做出进一步讨论,因此话题范围将非常广泛!
func searchItunesFor(searchTerm: String) { // iTunes API要求使用+分隔多个搜索词,因此在这儿用+号替代空格符 var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+", options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil) // 现在转义哪些无法识别出其是URL字符的其他字符 var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding) var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=software" var url: NSURL = NSURL(string: urlPath) var request: NSURLRequest = NSURLRequest(URL: url) var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false) println("Search iTunes API at URL \(url)") connection.start() }
我们逐行看看上面代码。
首先,我们需要对传入搜索词进行修正,搜索API要求搜索词的格式为"第一个搜索词+第二个搜索词+第三个搜索词+其他搜索词",而不是"第一个搜索词%20第二个搜索词%20第三个搜索词%20..."。因此,我们没有调用URL编码函数,而是调用了sringByReplacingOccurenesOfString的NSString方法。这个方法将返回搜索变量的修正版本,使用+号替代了其中的空格符。
接着,我们转义了搜索词中哪些无法识别为URL所包含的字符。
再接下来的两行定义了NSURL对象,这个对象将做为iOS网络API的URL参数。
以下两行非常关键:
var request: NSURLRequest = NSURLRequest(URL: url) var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
第一行使用我们前面创建的url变量作为URL参数来创建NSURLRequest对象。接着第二行创建了即将真正用来发送请求的“连接”。注意:我们设置参数中的delegate为该对象自身。这样我们就可以在视图控制器类的内部侦听连接发送来的响应信息。
最后,使用connection.start()发送请求。
接收响应之前要做的准备工作
现在我们已经有一个方法可以请求iTunes搜索结果了。这时,在viewDidLoad..的末尾处要插入下面这行:
searchItunesFor("JQ Software")
这样我们就可以在iTunes商店里查找哪些包含上面关键词的任何软件产品了,此时搜索的结果将包含几年前我所编写的两个游戏和几个较新的软件。当然,你也可以按照自己的意愿更换搜索字符串。
接下来,为了能够真正地接收到响应数据,我们需要跟踪包含返回结果的数据对象。首先,给当前类里增加一个成员:NSMutableData实例,即在类的定义插入下面一行,这行位于大括号之内。要能接收数据,我们还需要创建一个用来存储表格信息数组。
var data: NSMutableData = NSMutableData() var tableData: NSArray = NSArray()
接下来,我们一起看看NSURLConnection发送我们编写的类的函数,由于它是发送请求的代理,所以我们希望NSURLConnection发送的任何信息都能够通过在NSURLConnectionDeatDelegate和NSURLConnectionDelegate里定义的通信方法回送。如果你还不明白,请不要着急。接着向下看,你将很快明白这些是如何运行的。
接受响应
现在我们将要添加目前为止最长的代码块了,不过依旧不是很长,用来处理整个的结果信息。
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) { // Recieved a new request, clear out the data object self.data = NSMutableData() } func connection(connection: NSURLConnection!, didReceiveData data: NSData!) { // Append the recieved chunk of data to our data object self.data.appendData(data) } func connectionDidFinishLoading(connection: NSURLConnection!) { // Request complete, self.data should now hold the resulting info // Convert the retrieved data in to an object through // JSON deserialization var err: NSError var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary if jsonResult.count>0 && jsonResult["results"].count>0 { var results: NSArray = jsonResult["results"] as NSArray self.tableData = results self.appsTableView.reloadData() } }
当NSURLConnection 接受到一个响应,我们可能期望 didReceiveResponse方法代表我们被调用。此时我们可以简单的重置我们的数据,通过 self.data = NSMutableData()来创建一个新的空的Data对象。
连接建立后,我们将开始在didReceiveData中接受数据。这里传递的数据参数是我们所有信息的来源。我们需要抓住传递进来的每一块数据,因此我们将它添加到 self.data对象(我们之前清理过的)的结尾。
当连接最终完成并且所有数据都被接收到,connectionDidFinishLoading方法被调用并且我们可以在我们的应用中使用这些数据了,太棒了。
这里connectionDidFinishLoading方法使用NSJSONSerialization类通过反序列换来自iTune的结果来将我们的原始数据转化为有用的字典对象。
现在,因为我们知道来自iTunes的数据格式略显粗糙,因此一个简单的对“result”键数量的检查足以判断是否是我们想要的数据了。所以我们现在可以设置我们的self.tableData对象为结果数据,并且告诉 appsTableView 去加载内容。这会引起Table View对象运行他自己的委托方法。定义这些是我们教程的最后一步了。
更新Table View UI
你可能还记得上一次我们为Table View完成了两个功能。计数功能决定了行数; 单元格功能,真正的在每一行创建和修改单元格.
我们将要更新Table View使用我们从web下载的数据.
换出你的方法有以下两个功能:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")
var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
cell.text = rowData["trackName"] as String
// Grab the artworkUrl60 key to get an image URL for the app's thumbnail
var urlString: NSString = rowData["artworkUrl60"] as NSString
var imgURL: NSURL = NSURL(string: urlString)
// Download an NSData representation of the image at the URL
var imgData: NSData = NSData(contentsOfURL: imgURL)
cell.image = UIImage(data: imgData)
// Get the formatted price string for display in the subtitle
var formattedPrice: NSString = rowData["formattedPrice"] as NSString
cell.detailTextLabel.text = formattedPrice
return cell
}
numberOfRowsInSection简单的从tableData成员返回结果对象的数量, 并设置我们前面的connectionDidFinishLoading方法.
在这个例子中cellForRowAtIndexPath也不会明显的改变。 而是简单的返回行数, 我们使用行数获取三个信息: 曲目名称, 作品的网址和价格.
使用这些关键字,我们在单元格中构建了标题,副标题和图片。
尝试运行我们的APP,你将第一次看到我们完全使用Swift真正创建了一些看起来像APP的东西。
为什么反应还是这么迟缓!?
在这个表格视图里,我们没能正确地处理许多事情。在接下来的三个小节里,我们将深入探讨问题在哪儿和我们需要正确地做出那些修改。如果你愿意的话,可以跳到第五部分,在第五部分这些问题都得到了解决。
以后再第三部分,我们将关注的应用的交互部分,这样用户就可以搜索他要找的任何东西,而且可以点击表格。
在第一部分和第二部分,我们研究的是Swift基础余元部分,并建立了一个简单的项目例子:创建表格视图,把从iTunes搜索的API结果放入表格里。如果你还仍然还没有阅读这些内容,那么请查看第一部分和第二部分。
在这一小节里,我们先暂时停下来,然后清除目前状况下我们所创建的部分代码:删除视图控制器代码里的网络逻辑,然后修补一些影响性能的问题。做这些工作可能不是编写一个新应用中最吸引人的地方,但却非常重要!我们一起完成这些工作吧...
分离代码
先说重要的, 让我们用一些更有意义的名字来重命名我们的View Controller。打开你的 ViewController.swift文件并且将‘ViewController’ 替换为我们的新名字‘SearchResultViewController’。当然也要重命名文件为SearchResultViewController.swift。
如果你现在运行程序肯定会崩溃的。这是因为我们的storyboard文件还没有被更新呢。因此打开Main.storyboard文件,在这个界面(左边导航栏)选择你的‘View Controller’对象,然后选择身份检查器(右边,第三个按钮)。
在这让我们把‘ViewController’类名改为‘SearchResultViewController’。现在我们应该可以回到正轨了。检查项目是否正常工作,然后我们继续。
现在,我们将把类里的API代码移到外部。在xcode导航面板按鼠标右键,然后选择‘新建文件...'。这时就会进入iOS->代码导航下的Cocoa Touch类。
这个文件将处理所有API的工作,因此我们称它为APIController。
然后,我们选取searchItunesFor()函数和所有像connection(didRecieveResponse...)这样的委托代理函数。把它们从Search controller剪切出来,然后黏贴到APIController里。你还需要拷贝哪些生成响应所需的数据变量。
如果你立刻进行构建,你会看到一下三个错误:
1) SearchResultsViewController当前没有定义searchItunesFor();
2) APIController当前没有定义self.tableData
3)APIController当前没有定义self.appsTableView
为了解决以上问题,我们需要让这些对象之间可以互相识别。因此要使APIController为SearchResultViewController的子对象。要做到这些非常容易,只要在SearchResultsViewController类定义的底部添加以下一行:
var api:APIController = APIController()
然后修改调用searchItunesFor()哪行为:
api.searchItunesFor("Angry Birds")
在这里,唯一不同点是通过APIController实例调用了这个方法,而不是一步完成所有的处理过程。
在要留心第一种错误同时,还需要APIController把结果返回给SearchResultViewController。我们希望API controller能够响应任何API调用,因此我们还应当定义一个通信协议,视图可通过这个通信协议获取到响应。
定义API协议
引用tableData结果的APIController目前有两行错误,我们暂时先删除它们,使用更干净清晰一些的东西。
在我们的APIController 类定义之上,我们添加一个声明了待实现的didReceiveAPIResults函数的协议。
protocol APIControllerProtocol { func didRecieveAPIResults(results: NSDictionary) }
他自己并不做任何事情,但是现在我们可以将这个协议添加到我们的SearchResultViewController中。现在不遵守协议就会引发错误,因此我们不会再犯没有实现didReceiveAPIResults的低级错误了。
遵守协议
现在修改你的SearchResultsViewController来遵守协议:
class SearchResultsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, APIControllerProtocol {
现在编译应该会得到一个SearchResultsViewController没有遵守 APIControllerProtocol协议的错误,很好!现在我们只需向我们的SearchResultsViewController类中添加函数就可以了。他看起来将非常像我们前面connectionDidFinishLoading类的内容。
func didRecieveAPIResults(results: NSDictionary) { // Store the results in our table data array if results.count>0 { self.tableData = results["results"] as NSArray self.appsTableView.reloadData() } }
还有一件需要做的事情是改变我们的API controller使其包含一个委托对象,并且当连接完成一些API数据的加载后调用这个方法。
使用协议
返回APIController.swift文件,让我们在类定义下增加一个委托。
var delegate: APIControllerProtocol?
结尾的问号表示委托是一个可选值。没有问号,我们将获得一个编译错误,提示没有为变量设置初始值,但是对我们来说是不会有这个错误提示的。可以在任何类中定义委托对象,只要它通过didRecieveAPIResults()方法附着到APIControllerProtocol中,因为我们在SearchResultsViewController已经完成上述工作。
现在我们已经添加了一个委托变量,让我们返回到SearchResultsViewController,在方法viewDidLoad中,让我们设置我们的api控制器的委托到自身,因此它可以接受委托函数的调用。
self.api.delegate = self
最后,在connectionDidFinishLoading方法中,让我们移除tableView代码并使用我们华丽的新协议方法替换它。
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Request complete, self.data should now hold the resulting info
// Convert the retrieved data in to an object through JSON deserialization
var err: NSError
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
// Now send the JSON result to our delegate object
delegate?.didRecieveAPIResults(jsonResult)
}
吁,稍休息一下!
到现在为止,我知道所创建的应用都是些样例应用,而且应用也确实像以前一样可以做相同的事情,不过我们在许多方面都拥有了更多的灵活性。现在,我们使用APIController来实现对iTunes搜索API的调用,并让一个定制的代理来接收响应。我认为目前我们把所有的时间都用在这些事情上,因此在第四部分我们将关注的是交互方面。如果你打算继续向下阅读,请通过email注册一下。
这一小节中的全部代码在这儿可以找到。
在第一、第二和第三部分,我们回顾了Swift的一些基础知识,并创建了一个简单示例项目:创建表格视图,并把iTunes的API返回结果放到这些表格里。如果你到现在还没有阅读到这些内容,那么请阅读第一、第二和第三部分。
对一些地方进行修改
好了,现在重中之重是几个地方需要整理。代理函数cellForRowAtIndexPath里创建的表格单元视图不是最有效的方法。在Obj-C里,如果内存里有一个表格单元可用,那么我们总是用dequeueResuableCellWithIdentifier来获得这个表格单元,然不是在每次要用到表格单元的时候创建。这么做可以快速滚动,而且可以降低内存的使用。
因此,在SearchResultsViewcontroller.swift文件里,我们用下面语句替换了表格单元的实例化:
let kCellIdentifier: String = "SearchResultCell" var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell
这么做将给出一个已经初始化的表格单元。在Swift里,要想知道SearchResultCell是什么,我们还需要在storyboard里声明这个表格单元为原类型表格,并且设置这个表格的标识符为SearchResultCell。要做到这些,请打开storyboard,选择表格视图,然后更改"prototype cells"的数目为1,接着点击这个表格单元。在属性栏里,监视器会更改Style为"Subtitle",接着在标识符里键入"SearchResultCell"。
运行这个应用,我们再次看到完全相同的结果,不过这次在内存使用方面确实更高效,也确实最接近现实应用了!
让表格单元“确实”可做某件事
现在,UTTableView可以通过代理类SearchResultsViewController调用多个代理函数了。其中有一种如下:
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!)
无论何时点击表格单元就会运行上面这个函数。这样,我们就可以获取所点击的iTunes数据的行号:通过访问由indexPath.row所设置的数组的索引来确定,即所点击行的整型id。然后在弹出框内显示同样的信息。
如下增加didSelectRowAtIndexPath方法:
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) { // 获取所选行的行数据 var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary var name: String = rowData["trackName"] as String var formattedPrice: String = rowData["formattedPrice"] as String var alert: UIAlertView = UIAlertView() alert.title = name alert.message = formattedPrice alert.addButtonWithTitle("Ok") alert.show() }
这段代码里,我们设置rowData的值为所选下标的数组对象的值,即第一时间把这个数组对象的信息放入到rowData里。然后根据rowData字典设置name和formattedPrice变量。接着,我们实例化了一个UIAlertView对象,并设置其title,message,并添加了一个取消按钮。最后,我们用alert.show()显示警告信息。
运行一下这个应用,现在你就应该能够在弹出的窗口里看到所点击应用的名字和价格了。很酷吧?
第四部分的代码可以在这儿阅读。
或者在这儿下载zip格式压缩文件。
第五部分将集中加快表格视图的显示速度。想通过电子邮件获得这个教程的最新版本,请在这而订阅吧。
在第一到第四部分,我们研究的是Swift基础部分,并建立了一个简单的项目例子:创建表格视图,把从iTunes搜索的API结果放入表格里。如果你还仍然还没有阅读这些内容,那么请阅读第一部分。
表格有些慢!,我们要加快一下显示速度。
现在,我们已经具有了所期望的功能,然而,如果你亲自运行一下这个应用,你将看到这个应用超级慢!问题是:所有表格单元里的图片都是在UI线程里下载的,而且是每次下载一个,而且这些图片也根本没有使用任何缓冲。接下来就修补一下这个问题。
一开始,在我们所创建的类里增加一个字典成员:
var imageCache = NSMutableDictionary()
现在,我们需要对cellForRowAtIndexPath方法做更多修改。 这儿有这一方法的最终版本。这个链接会打开一个新的标签页,这么做可以让后续的工作更容易些。
首先,对人们来说存在这样的问题:使用API关键词进行搜索造成结果混乱,这时人们获取返回数据是他们不想得到的。因此在返回字典中增加一个对trackName值得检查:
// 增加检查,以确保有值 let cellText: String? = rowData["trackName"] as? String cell.text = cellText
在下载要显示的真实图片之前,我们要确保给这个单元设置了用于占位的图片。如果你想让这个单元确实能包含图像视图,这么做就是必须的。否则以后装载真实的图片的时候就不会显示!创建一个空白图片(我用的是52x52像素,不过这无关紧要),然后,把这个图片文件导入到你的项目:在finder里点击并拖拉这个文件到Xcode项目,命名这个文件为Blank52,接着设置单元格使用这个图片为占位图片。你可以从 这儿获取我创建的图片文件。
cell.image = UIImage(named: "Blank52")
现在,这个应用将很少出现闪退了,而且现在始终有一个图片显示在单元格里。
让后台线程下载图片
接下来,我们打算启动一后台线程来进行图片下载。Swift似乎没有自己的GCD版本,因此,我们将使用Obj-C的API dispatch_async来进行图片下载。
我们使用GCD API来开始这段代码:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DE
这就允许可运行这段代码之外的代码,同时还可继续在后台下载图像。这对一个交互式UI体验尤其重要。
看看这段代码,现在我们首先应当对图片缓存进行检查,看看以前是否下载过这张图片。我们使用如下语句设置一个可选名字为image的名字变量。
var image: UIImage? = self.imageCache.valueForKey(urlString) as? UII
如果缓存里没有这张图片(在初始化的时候,也不会存这张图片),那么我们就需要下载它。有两种方法可用来初始化下载。前面我们使用的是NSData的dataWithContentsOfFile,不过这儿我们将使用的是NSURLConnection的sendAsynchronousRequest,它更像我们使用API的方式。这么做的理由是:要真正快速地下载图片,我们需要发送许多短小的请求。因此,我们就采用了这种方法。
我博客日志的这一部分可能没有进行很好的排版,因此请打开gist,看看第35行。在这一行里我们所做的就是调用NSURLConnection的静态方法sendAsynchronousRequest,这个方法将completionHandler函数当作参数。第36行道46行是这个函数的具体内容。
在这个函数里,我们将得到几个返回变量: response,data和error。
在36行,我们将核对是否存在错误,如果没有,将继续向前执行到第38行:根据所给data创建UIImage。第37行事对前面方法的引用说明。
image = UIImage(data: data
继续向下倒第4行,我们设置图片缓存使用这张图片的URL作为名称来保存新图片。使用URL做名称意味着在任何要显示这张图片的时候我们都可以在字典中找到它,甚至在完全不同的环境下也如此。
self.imageCache.setValue(image, forKey: urlString)
最后,我们设置表格单元显示图片:
cell.image = image
好了!运行一下这个项目,你就会看到非常漂亮的而且异常快速的新表格视图!
当前可用的全部代码在GitHub的'Part5'分支上。
你还可以在这儿下载zip格式的第五部分的代码。
要想通过电子邮件获取到这个教程的最新版,请在这儿订阅。
第6部分将集中添加一个新的视图控制器,我们可以打开这个控制器,然后把iTunes数据装载进来。
在第一到第五部分,我们研究的是Swift基础部分,并建立了一个简单的项目例子:创建表格视图,把从iTunes搜索的API结果放入表格里。如果你还仍然还没有阅读这些内容,那么请从第一部分开始阅读。
如果你不愿意,那么你还可以从此处开始, 先下载第5部分的代码。我们将把它作为范本开始后续说明。
在后续的教程里,我们将会做许多事情。
缩减API Controller部分的代码
一开始,我们的目标是现实iTunes的音乐信息。因此,我们修改API controller,以便更好地处理这些信息。在我们做这些更改之前,我们需要简化一下这个类。我们将使用在第五部分学到的sendAsynchronousRequest方法,然后再缩减API controller。要做到这些,我们需要删除一下函数:
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) { func connection(connection: NSURLConnection!, didReceiveData data: NSData!) { func connectionDidFinishLoading(connection: NSURLConnection!) {
还有,我们不再需要互斥的数据对象,因此也可以删除:
var data: NSMutableData = NSMutableData()
最后,我们删除searchItunesFor()函数里的创建和发送请求部分的代码:
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
然后,使用以下代码行替代:
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in if error? { println("ERROR: (error.localizedDescription)") } else { var error: NSError? let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary // Now send the JSON result to our delegate object if error? { println("HTTP Error: (error?.localizedDescription)") } else { println("Results recieved") self.delegate?.didRecieveAPIResults(jsonResult) } } })
这儿,我们所做的就是删除通信协议代理函数,取而代之的是选择更多的使用函数型API sendAsynchronousRequest。这时,函数就会发送'request'对象参数给sendAsynchronousRequest,而在mainqueue里,则是另一个函数的句柄。当执行请求的时候,它就会调用内部的函数,这个内部函数将会检查是否有错误,如果有,就把错误显示在控制台。如果没有错误,那么它接着就会把结果转换为JSON格式的NSDictionary。再通过JSON解析是否成功的检查后,它就会把这个字典返回给代理。这儿有许多地方进行了代码替换,而且在使用iOS开发所提供的更新的API时候,我们期望做这种更改。
我们在第三部分创建APIController这个类的时候,我们让这个类继承了NSObject,然而可以不这么做。因此我们修改这个类的定义如下:
class APIController {
不过,我们做了这样更改后,现在却少了init方法!因此我们要增加一个,而且要确保这个方法里包括通信代理 。没有代理的API Controller根本无法使用,因此为什么不编写一个呢?
init(delegate: APIControllerProtocol?) { self.delegate = delegate }
现在我们还需要对SearchResultsController进行一点修改。首先,我们需要把api变量改为可选的,这是因为我们不能把自己传到初始化函数里。现在,我们把所有'api'声明的地方替换为下面语句:
var api: APIController?
这个语句表明我们声明了一个名称为api的对象,它的类型是APIController,而且其值可为空。
接着,我们需要在viewDidLoad里初始化这个api变量。
self.api = APIController(delegate: self)
这样就不必再有一行来设置通信代理了,现在init方法可以自动设置通信代理了。
创建iTunes专辑的Swift模型
我们还要修改通过关键字对音乐进行搜索的SearchItunesFor()调用,并对其进行调整,通过给其结尾处添加!强制它对api对象展开。我们能这么做事因为我们自己创建了api,而且我们知道有api对象存在。我们还显示了一个networkActivityIndicator,用来告诉用户有网络操作进行。它将显示在手机的顶端状态栏里。
UIApplication.sharedApplication().networkActivityIndicatorVisible = true self.api!.searchItunesFor("Bob Dylan");
然后看看APIController里的urlPath,我们修改专辑的API的参数如下:
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm
非常棒!现在我们编写的APIController更加简洁,代码更少!
我们需要一个模型!
为了方便地传递专辑信息,我们应当创建一个表示专辑的模型.创建一个新的swift文件,命名为Album.swift,内容如下:
class Album { var title: String? var price: String? var thumbnailImageURL: String? var largeImageURL: String? var itemURL: String? var artistURL: String? init(name: String!, price: String!, thumbnailImageURL: String!, largeImageURL: String!, itemURL: String!, artistURL: String!) { self.title = name self.price = price self.thumbnailImageURL = thumbnailImageURL self.largeImageURL = largeImageURL self.itemURL = itemURL self.artistURL = artistURL } }
这是一个非常简单的类,它只为我们提供了专辑的几个属性。我们创建了类型为可选字符串的6个不同的属性,增加了一个在使用这个对象之前对其展开的初始化方法。 初始化方法非常简单,它仅仅依据所提供参数设置所有属性。
现在,我们编写了一个专辑对象类,下面就可以使用了。
使用新的专辑swift模型
回头看看SearchResultController,我们删除了NSArray类型的数组变量tableData,而选择使用表示专辑的Swift数组。在Swift里,做到这些非常简单:
var albums: Album[] = []
这个语句将创建一个可存储专辑的空数组。现在我们需要更改解析专辑的tableView的dataSource和通信代理方法。
在numberOfRowsInSection方法里,我们更改元素数为专辑数组里包含的专辑数:
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { return albums.count }
然后,在cellForRowAtIndexPath方法里,我们用单个专辑查询方法替代了字典查询方法:
let album = self.albums[indexPath.row] cell.text = album.title cell.image = UIImage(named: "Blank52") cell.detailTextLabel.text = album.price
稍后,在获取缩略图的地方,我们用下面语句替换了urlString设置器:
let urlString = album.thumbnailImageURL
你们中的某些人可能已经注意到图像显示有问题,而且彼此覆盖。出现这种情况是因为对表格单元重用引起的。我们修改了代码,以检查sendAsynchronousRequest函数里可看到的表格单元是否存在(即是否可见)。修改的代码段很大,请在 整个函数的gist了查阅。
根据JSON创建专辑对象
现在,如果我们没有第一时间创建专辑信息,那么上面所做的更改将无法使用。我们需要修改didRecieveAPIResults方法,以获取返回的专辑信息,这些专辑信息是根据返回的JSON响应来创建的,接着还要把这些专辑信息存储到专辑数组里。修改
后的这个方法的最终版如下:
func didRecieveAPIResults(results: NSDictionary) { // 把结果存储到表格数据数组里 if results.count>0 { let allResults: NSDictionary[] = results["results"] as NSDictionary[] // 有时候,iTunes返回的是一个集合,而不是音轨,因此我们要核查这两个name值 for result: NSDictionary in allResults { var name: String? = result["trackName"] as? String if !name? { name = result["collectionName"] as? String } // 有时候价格是formattedPrice, 有时是collectionPrice..还有的时候是浮点数,而不是字符串,哎呀,什么情况都有! var price: String? = result["formattedPrice"] as? String if !price? { price = result["collectionPrice"] as? String if !price? { var priceFloat: Float? = result["collectionPrice"] as? Float var nf: NSNumberFormatter = NSNumberFormatter() nf.maximumFractionDigits = 2; if priceFloat? { price = "$"+nf.stringFromNumber(priceFloat) } } } let thumbnailURL: String? = result["artworkUrl60"] as? String let imageURL: String? = result["artworkUrl100"] as? String let artistURL: String? = result["artistViewUrl"] as? String var itemURL: String? = result["collectionViewUrl"] as? String if !itemURL? { itemURL = result["trackViewUrl"] as? String } var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL!, largeImageURL: imageURL!, itemURL: itemURL!, artistURL: artistURL!) albums.append(newAlbum) } self.appsTableView.reloadData() UIApplication.sharedApplication().networkActivityIndicatorVisible = false } }
这段代码似乎存在大量的新代码,不过这段代码却非常简单。
首先,我们从API的返回结果提取results关键字的内容到NSDictionary数组allResults,它包含着所有专辑信息。
let allResults: NSDictionary[] = results["results"] as NSDictiona
然后,我们使用Swift的for-each语法对allResults的每个内嵌的字典进行轮询查找,并把每个元素赋值给临时变量result。
for result: NSDictionary in allResults {
接下来,你将看到许多如下的语句:
var name: String? = result["trackName"] as? String if !name? { name = result["collectionName"] as? String }
这里发生的是iTunes对歌曲和专辑使用不同的键。因此所有这些域应该声明为可选的,并且我们应该检查它们至少它们其中一个被保存。如果你选择使用不同的媒体类型,这对于你的应用会是非常重要的。
最终格式化我们所有信息和验证它们都存在后,我们创建一个新的唱片集,并且添加到我们的唱片集数组中来:
var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL!, largeImageURL: imageURL!, itemURL: itemURL!, artistURL: artistURL!) albums.append(newAlbum)
既然都处理完了,我们重新加载我们的表视图,然后关掉网络监视器。
self.appsTableView.reloadData() UIApplication.sharedApplication().networkActivityIndicatorVisible = false
创建第二个视图
现在需要显示一个唱片集的详细信息了,我们需要一个新的视图。首先我们创建这个类。添加一个叫做DetailsViewController.swift并继承自UIViewController的文件。
我们的视图控制器将非常简单。我们只需添加一个album,然后实现UIViewController的init和viewDidLoad方法即可。
import UIKit class DetailsViewController: UIViewController { var album: Album? init(coder aDecoder: NSCoder!) { super.init(coder: aDecoder) } override func viewDidLoad() { super.viewDidLoad() } }
这段代码没有做多少,但是没关系。我们需要这个类以便设置我们的storyboard。
既然我们会在栈中向后向前推视图,那我们一定需要一个导航栏。很难用文字解释,并且有保密协议不允许我展示Xcode 6截图,因此取而代之,我创建了一个短视频来展示怎样在Xcode 5中做这些。这一个过程跟在Xcode 6 Beta 中几乎一样,并且不受保密协议的约束。
在这段视频里,我们完成了以下工作:
使用编辑菜单中的Xcode快捷键在控制器导航里嵌入了我们创建的视图控制器:点击试图控制器,然后选择编辑-〉嵌入-〉控制器导航
添加了新的视图控制器
设置DetailsViewController对应的类和storyboard ID。
把我们最先创建的视图控制器里的表格单元视图通过Ctrl、点击加上拖拉到我们刚创建的心得视图控制器里,接着选择segue类型为'push'。
最后一步所做的事情就是在控制器导航上创建一个segue,它将新的视图放在堆栈的最顶端。此时运行这个一下这个应用,然后点击单元格,你将能够看到动态载入了新的视图。
接着我们给新的视图增加一个简单的UI。这个UI将包括100x100像素的UIImageView,一个标题,一个按钮和一个文本视图。从对象库中拖拉出上面所说的所有对象,然后在新的视图上按照自己的意愿对这些对象进行布局。在创建这个UI之前,请注意:XCode6有一个显示尺寸分类的概念。为了达到我们的目标,我们需要设置显示尺寸分类为iPhone。这时在显示窗口的底部,你将可以看到“宽多少,高多少”这样的描述。这就是当前storyboard的大小。如果你奇怪为什么你所创建的视图是正方形的,这就是原因所在。点击它,更改它以显示iPhone大小图片,即设置其为"宽度压缩|高度任意“。
给专辑信息提供新的视图
当启动storyboard里的segue的时候,它首先调用的时当前屏幕的视图控制器所对应的名字为prepareForSegue函数。我们将拦截这个函数调用,通过这种方式来告诉新的视图控制器我们正在查看哪个专辑。向SearchResultsViewController添加以下代码:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject) { var detailsViewController: DetailsViewController = segue.destinationViewController as DetailsViewController var albumIndex = appsTableView.indexPathForSelectedRow().row var selectedAlbum = self.albums[albumIndex] detailsViewController.album = selectedAlbum }
这儿传入的segue参数具9有一个名字为destinationViewController的成员,它就是我们刚刚创建的DetailsViewController。为了设置专辑信息,我们首先需要使用上面代码中所示的'as'关键字把它转换为DetailsViewController。
接着,我们通过调用表格视图的indexPathForSelectedRow()方法确定运行此segue的时选择的是哪一专辑。
有了这些信息,我们就可以告诉detailsViewController在显示专辑信息之前我们点击了哪一专辑。
接下来,我们将会向你展示Xcode的一个非常强大的特性。通过使用这个特性,Xcode将会替我们编写部分代码。
再次打开storyboard,开始给图像视图、标签、按钮和文本视图创建对应的IBOutlet。在Xcode的右上角有一个"辅助“按钮。它的图标像一个带有领结的男外套。点击一下将会在storyboard窗口的右下方开启一个代码窗口。看一下是否其中一个面板显示的DetailsViewController.swift,另一个显示的是Main.storyboard。
然后,按住ctrl,接着点击并拖拉图像视图到代码文件。这时DetailsViewController类的定义下方就会出现一条横线。提示你输入名称,我们输入"albumCover"。选项就选默认的就可以了。做完这些操作后,你就会看到新增了下面这行代码:
@IBOutlet var albumCover : UIImageView
此时,我们创建了一个新的IBOutlet,并且把它与storyboard里的DetailsViewControll关联起来。非常酷吧?
然后对你添加到视图的其他对象也做同样的操作。完成这些以后,我们还要修改viewDidLoad,以便装载传递给视图对象的信息。下面是DetailsViewController代码的最终版本:
import UIKit class DetailsViewController: UIViewController { @IBOutlet var albumCover : UIImageView @IBOutlet var titleLabel : UILabel @IBOutlet var detailsTextView : UITextView @IBOutlet var openButton : UIButton var album: Album? init(coder aDecoder: NSCoder!) { super.init(coder: aDecoder) } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = self.album?.title albumCover.image = UIImage(data: NSData(contentsOfURL: NSURL(string: self.album?.largeImageURL))) } }
上面的几个@IBOutlet是由storyboard创建的UI与视图控制器之间的关联,而viewDidLoad方法则根据装载的专辑对象设置专辑标题和专辑封面。
现在,试着运行应用程序并看一下结果。我们现在可以深入探究相册的详细信息,并且得到一个与专辑封面和标题相关的详细信息视图。因为我们之前做了导入导航控制器的操作,因此我们还免费获得功能后退按钮 !
如果目前为止,你已经做到这些,请在 我的推ter上面告诉我你已经成功完成,因为我想要亲自祝贺你!你在用Swift开发iOS应用的道路上进展不错。
下一次,我们要弄清楚一些需要放在我们大段文本字段中的东西,并且我们要让我们的按钮能在展示专辑时做一些酷酷的事情。 所以一定要注册,以便下一次通知。
这一节的完整源代码可以 在这里获得