对一些地方进行修改
好了,现在重中之重是几个地方需要整理。代理函数cellForRowAtIndexPath里创建的表格单元视图不是最有效的方法。在Obj-C里,如果内存里有一个表格单元可用,那么我们总是用dequeueResuableCellWithIdentifier来获得这个表格单元,然不是在每次要用到表格单元的时候创建。这么做可以快速滚动,而且可以降低内存的使用。 因此,在SearchResultsViewcontroller.swift文件里,我们用下面语句替换了表格单元的实例化: 1 | let kCellIdentifier: String = "SearchResultCell" |
2 | var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell |
这么做将给出一个已经初始化的表格单元。在Swift里,要想知道SearchResultCell是什么,我们还需要在storyboard里声明这个表格单元为原类型表格,并且设置这个表格的标识符为SearchResultCell。要做到这些,请打开storyboard,选择表格视图,然后更改"prototype cells"的数目为1,接着点击这个表格单元。在属性栏里,监视器会更改Style为"Subtitle",接着在标识符里键入"SearchResultCell"。 运行这个应用,我们再次看到完全相同的结果,不过这次在内存使用方面确实更高效,也确实最接近现实应用了!
让表格单元“确实”可做某件事现在,UTTableView可以通过代理类SearchResultsViewController调用多个代理函数了。其中有一种如下: 1 | func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) |
无论何时点击表格单元就会运行上面这个函数。这样,我们就可以获取所点击的iTunes数据的行号:通过访问由indexPath.row所设置的数组的索引来确定,即所点击行的整型id。然后在弹出框内显示同样的信息。
如下增加didSelectRowAtIndexPath方法:
01 | func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) { |
03 | var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary |
05 | var name: String = rowData[ "trackName" ] as String |
06 | var formattedPrice: String = rowData[ "formattedPrice" ] as String |
08 | var alert: UIAlertView = UIAlertView() |
10 | alert.message = formattedPrice |
11 | alert.addButtonWithTitle( "Ok" ) |
这段代码里,我们设置rowData的值为所选下标的数组对象的值,即第一时间把这个数组对象的信息放入到rowData里。然后根据rowData字典设置name和formattedPrice变量。接着,我们实例化了一个UIAlertView对象,并设置其title,message,并添加了一个取消按钮。最后,我们用alert.show()显示警告信息。
运行一下这个应用,现在你就应该能够在弹出的窗口里看到所点击应用的名字和价格了。很酷吧?
第四部分的代码可以在这儿阅读。
或者在这儿下载zip格式压缩文件。
第五部分将集中加快表格视图的显示速度。想通过电子邮件获得这个教程的最新版本,请在这而订阅吧。
在第一到第四部分,我们研究的是Swift基础部分,并建立了一个简单的项目例子:创建表格视图,把从iTunes搜索的API结果放入表格里。如果你还仍然还没有阅读这些内容,那么请阅读第一部分。
表格有些慢!,我们要加快一下显示速度。 现在,我们已经具有了所期望的功能,然而,如果你亲自运行一下这个应用,你将看到这个应用超级慢!问题是:所有表格单元里的图片都是在UI线程里下载的,而且是每次下载一个,而且这些图片也根本没有使用任何缓冲。接下来就修补一下这个问题。
一开始,在我们所创建的类里增加一个字典成员:
1 | var imageCache = NSMutableDictionary() |
现在,我们需要对cellForRowAtIndexPath方法做更多修改。 这儿有这一方法的最终版本。这个链接会打开一个新的标签页,这么做可以让后续的工作更容易些。
首先,对人们来说存在这样的问题:使用API关键词进行搜索造成结果混乱,这时人们获取返回数据是他们不想得到的。因此在返回字典中增加一个对trackName值得检查:
2 | let cellText: String? = rowData[ "trackName" ] as? String |
在下载要显示的真实图片之前,我们要确保给这个单元设置了用于占位的图片。如果你想让这个单元确实能包含图像视图,这么做就是必须的。否则以后装载真实的图片的时候就不会显示!创建一个空白图片(我用的是52x52像素,不过这无关紧要),然后,把这个图片文件导入到你的项目:在finder里点击并拖拉这个文件到Xcode项目,命名这个文件为Blank52,接着设置单元格使用这个图片为占位图片。你可以从 这儿获取我创建的图片文件。 1 | cell.image = UIImage(named: "Blank52" ) |
现在,这个应用将很少出现闪退了,而且现在始终有一个图片显示在单元格里。
让后台线程下载图片 接下来,我们打算启动一后台线程来进行图片下载。Swift似乎没有自己的GCD版本,因此,我们将使用Obj-C的API dispatch_async来进行图片下载。
我们使用GCD API来开始这段代码: 1 | dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DE |
这就允许可运行这段代码之外的代码,同时还可继续在后台下载图像。这对一个交互式UI体验尤其重要。
看看这段代码,现在我们首先应当对图片缓存进行检查,看看以前是否下载过这张图片。我们使用如下语句设置一个可选名字为image的名字变量。 1 | 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行事对前面方法的引用说明。 1 | image = UIImage(data: data |
继续向下倒第4行,我们设置图片缓存使用这张图片的URL作为名称来保存新图片。使用URL做名称意味着在任何要显示这张图片的时候我们都可以在字典中找到它,甚至在完全不同的环境下也如此。 1 | self.imageCache.setValue(image, forKey: urlString) |
最后,我们设置表格单元显示图片:
好了!运行一下这个项目,你就会看到非常漂亮的而且异常快速的新表格视图!
当前可用的全部代码在GitHub的'Part5'分支上。
你还可以在这儿下载zip格式的第五部分的代码。
要想通过电子邮件获取到这个教程的最新版,请在这儿订阅。
第6部分将集中添加一个新的视图控制器,我们可以打开这个控制器,然后把iTunes数据装载进来。
在第一到第五部分,我们研究的是Swift基础部分,并建立了一个简单的项目例子:创建表格视图,把从iTunes搜索的API结果放入表格里。如果你还仍然还没有阅读这些内容,那么请从第一部分开始阅读。
如果你不愿意,那么你还可以从此处开始, 先下载第5部分的代码。我们将把它作为范本开始后续说明。
在后续的教程里,我们将会做许多事情。
缩减API Controller部分的代码 一开始,我们的目标是现实iTunes的音乐信息。因此,我们修改API controller,以便更好地处理这些信息。在我们做这些更改之前,我们需要简化一下这个类。我们将使用在第五部分学到的sendAsynchronousRequest方法,然后再缩减API controller。要做到这些,我们需要删除一下函数:
1 | func connection(connection: NSURLConnection!, didFailWithError error: NSError!) |
2 | func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) { |
3 | func connection(connection: NSURLConnection!, didReceiveData data: NSData!) { |
4 | func connectionDidFinishLoading(connection: NSURLConnection!) { |
还有,我们不再需要互斥的数据对象,因此也可以删除: 1 | var data: NSMutableData = NSMutableData() |
最后,我们删除searchItunesFor()函数里的创建和发送请求部分的代码: 1 | var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false ) |
然后,使用以下代码行替代: 01 | NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in |
03 | println( "ERROR: (error.localizedDescription)" ) |
07 | let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary |
10 | println( "HTTP Error: (error?.localizedDescription)" ) |
13 | println( "Results recieved" ) |
14 | self.delegate?.didRecieveAPIResults(jsonResult) |
这儿,我们所做的就是删除通信协议代理函数,取而代之的是选择更多的使用函数型API sendAsynchronousRequest。这时,函数就会发送'request'对象参数给sendAsynchronousRequest,而在mainqueue里,则是另一个函数的句柄。当执行请求的时候,它就会调用内部的函数,这个内部函数将会检查是否有错误,如果有,就把错误显示在控制台。如果没有错误,那么它接着就会把结果转换为JSON格式的NSDictionary。再通过JSON解析是否成功的检查后,它就会把这个字典返回给代理。这儿有许多地方进行了代码替换,而且在使用iOS开发所提供的更新的API时候,我们期望做这种更改。
我们在第三部分创建APIController这个类的时候,我们让这个类继承了NSObject,然而可以不这么做。因此我们修改这个类的定义如下:
不过,我们做了这样更改后,现在却少了init方法!因此我们要增加一个,而且要确保这个方法里包括通信代理 。没有代理的API Controller根本无法使用,因此为什么不编写一个呢? 1 | init(delegate: APIControllerProtocol?) { |
2 | self.delegate = delegate |
现在我们还需要对SearchResultsController进行一点修改。首先,我们需要把api变量改为可选的,这是因为我们不能把自己传到初始化函数里。现在,我们把所有'api'声明的地方替换为下面语句:
这个语句表明我们声明了一个名称为api的对象,它的类型是APIController,而且其值可为空。
接着,我们需要在viewDidLoad里初始化这个api变量。 1 | self.api = APIController(delegate: self) |
这样就不必再有一行来设置通信代理了,现在init方法可以自动设置通信代理了。
创建iTunes专辑的Swift模型 我们还要修改通过关键字对音乐进行搜索的SearchItunesFor()调用,并对其进行调整,通过给其结尾处添加!强制它对api对象展开。我们能这么做事因为我们自己创建了api,而且我们知道有api对象存在。我们还显示了一个networkActivityIndicator,用来告诉用户有网络操作进行。它将显示在手机的顶端状态栏里。 1 | UIApplication.sharedApplication().networkActivityIndicatorVisible = true |
2 | self.api!.searchItunesFor( "Bob Dylan" ); |
然后看看APIController里的urlPath,我们修改专辑的API的参数如下:
非常棒!现在我们编写的APIController更加简洁,代码更少!
我们需要一个模型!
为了方便地传递专辑信息,我们应当创建一个表示专辑的模型.创建一个新的swift文件,命名为Album.swift,内容如下: 04 | var thumbnailImageURL: String? |
05 | var largeImageURL: String? |
07 | var artistURL: String? |
09 | init(name: String!, price: String!, thumbnailImageURL: String!, largeImageURL: String!, itemURL: String!, artistURL: String!) { |
12 | self.thumbnailImageURL = thumbnailImageURL |
13 | self.largeImageURL = largeImageURL |
14 | self.itemURL = itemURL |
15 | self.artistURL = artistURL |
这是一个非常简单的类,它只为我们提供了专辑的几个属性。我们创建了类型为可选字符串的6个不同的属性,增加了一个在使用这个对象之前对其展开的初始化方法。 初始化方法非常简单,它仅仅依据所提供参数设置所有属性。
现在,我们编写了一个专辑对象类,下面就可以使用了。
使用新的专辑swift模型 回头看看SearchResultController,我们删除了NSArray类型的数组变量tableData,而选择使用表示专辑的Swift数组。在Swift里,做到这些非常简单: 1 | var albums: Album[] = [] |
这个语句将创建一个可存储专辑的空数组。现在我们需要更改解析专辑的tableView的dataSource和通信代理方法。
在numberOfRowsInSection方法里,我们更改元素数为专辑数组里包含的专辑数: 1 | func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { |
然后,在cellForRowAtIndexPath方法里,我们用单个专辑查询方法替代了字典查询方法: 1 | let album = self.albums[indexPath.row] |
3 | cell.image = UIImage(named: "Blank52" ) |
4 | cell.detailTextLabel.text = album.price |
稍后,在获取缩略图的地方,我们用下面语句替换了urlString设置器: 1 | let urlString = album.thumbnailImageURL |
你们中的某些人可能已经注意到图像显示有问题,而且彼此覆盖。出现这种情况是因为对表格单元重用引起的。我们修改了代码,以检查sendAsynchronousRequest函数里可看到的表格单元是否存在(即是否可见)。修改的代码段很大,请在 整个函数的gist了查阅。
根据JSON创建专辑对象 现在,如果我们没有第一时间创建专辑信息,那么上面所做的更改将无法使用。我们需要修改didRecieveAPIResults方法,以获取返回的专辑信息,这些专辑信息是根据返回的JSON响应来创建的,接着还要把这些专辑信息存储到专辑数组里。修改 后的这个方法的最终版如下:
01 | func didRecieveAPIResults(results: NSDictionary) { |
05 | let allResults: NSDictionary[] = results[ "results" ] as NSDictionary[] |
08 | for result: NSDictionary in allResults { |
10 | var name: String? = result[ "trackName" ] as? String |
12 | name = result[ "collectionName" ] as? String |
16 | var price: String? = result[ "formattedPrice" ] as? String |
18 | price = result[ "collectionPrice" ] as? String |
20 | var priceFloat: Float? = result[ "collectionPrice" ] as? Float |
21 | var nf: NSNumberFormatter = NSNumberFormatter() |
22 | nf.maximumFractionDigits = 2; |
24 | price = "$" +nf.stringFromNumber(priceFloat) |
29 | let thumbnailURL: String? = result[ "artworkUrl60" ] as? String |
30 | let imageURL: String? = result[ "artworkUrl100" ] as? String |
31 | let artistURL: String? = result[ "artistViewUrl" ] as? String |
33 | var itemURL: String? = result[ "collectionViewUrl" ] as? String |
35 | itemURL = result[ "trackViewUrl" ] as? String |
38 | var newAlbum = Album(name: name!, price: price!, thumbnailImageURL: thumbnailURL!, largeImageURL: imageURL!, itemURL: itemURL!, artistURL: artistURL!) |
39 | albums.append(newAlbum) |
43 | self.appsTableView.reloadData() |
44 | UIApplication.sharedApplication().networkActivityIndicatorVisible = false |
这段代码似乎存在大量的新代码,不过这段代码却非常简单。
首先,我们从API的返回结果提取results关键字的内容到NSDictionary数组allResults,它包含着所有专辑信息。 1 | let allResults: NSDictionary[] = results[ "results" ] as NSDictiona |
然后,我们使用Swift的for-each语法对allResults的每个内嵌的字典进行轮询查找,并把每个元素赋值给临时变量result。 1 | for result: NSDictionary in allResults { |
接下来,你将看到许多如下的语句: 1 | var name: String? = result[ "trackName" ] as? String |
3 | name = result[ "collectionName" ] as? String |
|