本文的例子和Swift版本是基於Xcode7.2的。以后也許不知道什么時候會更新。
我們要干點啥
用新浪微博的Open API做后端來實現我們要提到的功能。把新浪微博的內容,圖片和文字展示在collection view中。本文只簡單的展示內容。下篇會用pinterest一樣的效果來展示這些內容。
我們准備優先展示圖片。你的好友花了那么多時間拍照或者從相冊里選擇圖片發上來多不容易。如果微博返回的數據中有中等大小的縮略圖,那么久展示這個縮略圖。否則的話顯示文本。文本都沒有的話。。。這個就不是微博了。但是我們還是會准備一個顏色顯示出來。
啥是UICollectionView
UICollectionView有一個靈活的布局,可以用各種不同的布局展示數據。
UICollectionView的使用和UITableView類似,也是需要分別去實現一組datasource的代理和UICollectionView本身的代理來把數據展示在界面中。
UICollectionView也是UIScrollView的一個子類
其他的還有:
1. UICollectionViewCell:這些Cell組成了整個UICollectionView,並作為子View添加到UICollectionView中。可以在Interface builder中創建,也可以代碼創建。
2. Header/Footer:跟UITableView差不多的概念。顯示一些title什么的信息。
UICollectionView還有一個叫做Decoration view的東西。顧名思義,主要是裝飾用的。
不過要用這部分的功能你需要單獨寫定制的layout。
除了以上說到的內容之外,collection view還有一個專門處理布局的UICollectionViewLayout
。你可以繼承UICollectionViewLayout
來創建一個自己的collection view的布局。蘋果給了一個基礎的布局UICollectionViewFlowLayout
,可以實現一個基本的流式布局。這些會在稍后的教程中介紹。
開始我們的項目:
首先創建一個single view的應用。
然后給你的項目起一個名字,我們這里就叫做CollectionViewDemo
。Storyboard中默認生成的Controller已經木有什么用處了。直接干掉,拖一個UICollectionViewController
進去並設置為默認的Controller。並刪除默認生成的ViewController.swift文件,並創建一個叫做HomeCollectionViewController.swift的文件。之后在interface builder中把collection view的類設置為HomeCollectionViewController
。
然后:
- 在Storyboard中添加一個navigation controller
- 把collection view設置為上面的navigation controller的root view controller。
- 把這個navigation controller設置為initial view controller。
接下來再次回到collection view controller。這個
進一步了解UICollectionView
如前文所述,UICollectionView和UITableView類似,都有datasource和delegate。這樣就可以設置datasource和設置一些用戶的交互,比如選中某一個cell的時候怎么處理。
UICollectionViewFlowLayout
有一個代理:UICollectionViewDelegateFlowLayout
。通過這個代理可以設定布局的一些行為比如:cell的間隔,collection view的滾動方向等。
下面就開始在我們的代碼中給UICollectionViewDataSource
和UICollectionViewDelegateFlowLayout
兩個代理的方法做一個填空。UICollectionViewDelegate
里的方法暫時還用不着,稍后會給這個代理做填空。
實現UICollectionViewDataSource
這里我們用微博開放API為例。從微博的開發API上獲取到當前用戶的全部的微博,然后用UICollectionView展示。獲取到的微博time line最后會放在這里:
private var timeLineStatus: [StatusModel]?
在data source中的代碼就很好添加了。
// MARK: UICollectionViewDataSource override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 //1 } override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.timeLineStatus?.count ?? 0 //2 } override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) cell.backgroundColor = UIColor.orangeColor() //3 return cell }
- 我們只要一個section,所以這里返回數字1。
- 返回的time line都會放在類型為
StatusModel
的數組里。這個數組可能為空,因為很多情況會影響到網絡請求,比如網絡不通的時候。這個時候返回的time line就是空了。所以self.timeLineStatus?.count
得出的數字也可能是空,那么這個時候就應該返回0。 - 由於沒有合適的Cell返回,現在只好用改變Cell的背景色的方式看到Cell的排布。
效果是這樣的:
UICollectionViewFlowLayoutDelegate
這個代理的作用和UITableView的func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
有非常類似的作用。heightForRowAtIndexPath
的作用是返回UITableViewCell的高度。而UICollectionViewCell有非常多的不同的大小,所以需要更加復雜的代理方法的支持。其中包括兩個方法:
// 1 class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout // 2 private let sectionInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) // MARK: UICollectionViewDelegateFlowLayout // 3 func collectionView(collectionView: UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { return CGSize(width: 170, height: 300) } // 4 func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { return sectionInsets }
- 首先需要實現layout的代理
UICollectionViewDelegateFlowLayout
。 - 給類添加一個
sectionInsets
的屬性。 UICollectionViewDelegateFlowLayout
的第一個方法,用來返回indexPath
指定位置的Cell的Size。- layout代理的另外一個方法,用來返回每一個section的inset。
看看運行效果:
創建自定義UICollectionViewCell
下面就要處理內容在展示的時候具體應該怎么展示了。我們這里分兩種情況,如果用戶的微博有圖片,那么就展示圖片。如果沒有圖片就展示文字。可惜的是微博的API沒有圖片的大小返回回來。展示的時候需要大小參數來決定這個 UICollectionViewCell
到底要多大的size,由於沒有就只好弄個方塊來展示圖片了。至於圖片的拉伸方式就有你來決定了,我們這里為了簡單就使用默認的方式拉伸圖片。
在文字上就需要根據文字的多少了決定size了。由於我們的寬度是一定的,也就是說在autolayout中UILabel
的preferredMaxLayoutWidth
是一定的。然后就可以很方便的根據這個寬度來計算多行的UILabel
到底需要多少的高度來全部展示微博中的文字。
首先是展示圖片的Cell。
在Cell上放一個UIImageView
,保證這個image view的四個邊距都是0。
創建一個文件WeiboImageCell.swift,里面是類WeiboImageCell
,繼承自UICollectionViewCell
。
把這個Cell的custom class設置為WeiboImageCell
。
然后把Cell代碼中的image view和interface builder的image view關聯為IBOutelt:
class WeiboImageCell: UICollectionViewCell { @IBOutlet weak var weiboImageView: UIImageView! }
重復上面的步驟添加一個只有一個UILabel
的Cell,類型為WeiboTextCell
。設置這個UILabel
的屬性numberOfLines
為0,這樣就可以顯示多行的文字。然后設置這個label的上、左、下、右都是-8。
為什么是-8呢,因為蘋果默認的給父view留了寬度為8的margin(邊距),如果要文字和Cell的邊距貼合的話 需要覆蓋這個系統預留的邊距,因此需要設置邊距為-8。
最后關聯代碼和label。
class WeiboTextCell: UICollectionViewCell { @IBOutlet weak var weiboTextLabel: UILabel! }
添加完這兩個Cell之后,回到HomeCollectionViewController
。刪除self.collectionView!.registerClass(WeiboImageCell.self, forCellWithReuseIdentifier: reuseIdentifier)
方法,以及全部的registerClass
。
`registerClass`, 這個方法的調用會把我們在storyboard里做的一切都給抹掉。在調用Cell里的image view或者label的時候得到的永遠是nil。
到這,我們需要討論一下text cell對於label的約束問題。首先我們同樣設置label的約束,讓這個label貼着cell的邊。也就是,top、leading、trailing和bottom為-8。
但是這樣的而設置讓label在顯示出來的cell中是居中的。尤其在文字不足夠現實滿cell的空間的時候。所以,我們需要改一個地方。修改bottom的優先級,設置為low,最低:UILayoutPriorityDefaultLow
。這樣在labe計算高度的時候會優先考慮的是文字填滿label后的高度,而不是像之前一樣直接把labe的高度設置為cell的高度。這個時候不論文字是否填滿cell,都是從頂開始顯示有多少控件用多少空間。
集成SDWebImage
我們那什么來拯救圖片cell惹?辣就是SDWebImage
是一個著名的圖片請求和緩存的庫。我們這里用這個庫來請求微博中的圖片並緩存。
添加:
在Podfile里添加SDWebImage
的pod應用pod ‘SDWebImage’, ‘~>3.7’。當然了之前我們已經添加了user_frameworks!
。為什么用這個看原文:
You can make CocoaPods integrate to your project via frameworks instead of static libraries by specifying use_frameworks!.
多了就不多說了,需要了解更多的可以看這里。
pod更新完成之后。引入這個framework。
import SDWebImage
然后就可以給cell的image view上圖片了。
weiboImageCell.weiboImageView.sd_setImageWithURL(NSURL(string: status.status?.bmiddlePic ?? ""))
SDWebImage
給image view寫了一個category。里面有很多可以調用的方法。比如可以設置一個place holder的image。也就是在image沒有下載下來之前可以給image view設置一個默認的圖片。
http請求和數據
這里只是簡單說一下,更過的內容請看這里。
下面我們看看微博的Open API能給我們返回什么:
{ "statuses": [ { "created_at": "Tue May 31 17:46:55 +0800 2011", "id": 11488058246, "text": "求關注。", "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>", "favorited": false, "truncated": false, "in_reply_to_status_id": "", "in_reply_to_user_id": "", "in_reply_to_screen_name": "", "geo": null, "mid": "5612814510546515491", "reposts_count": 8, "comments_count": 9, "annotations": [], "user": { "id": 1404376560, "screen_name": "zaku", "name": "zaku", "province": "11", "city": "5", "location": "北京 朝陽區", "description": "人生五十年,乃如夢如幻;有生斯有死,壯士復何憾。", "url": "http://blog.sina.com.cn/zaku", "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1", "domain": "zaku", "gender": "m", "followers_count": 1204, ... } }, ... ], "ad": [ { "id": 3366614911586452, "mark": "AB21321XDFJJK" }, ... ], "previous_cursor": 0, // 暫時不支持 "next_cursor": 11488013766, // 暫時不支持 "total_number": 81655 }
我們只需要我們follow的好友的微博的圖片或者文字。所以由這些內容我們可以定義出對應的model類。
import ObjectMapper class BaseModel: Mappable { var previousCursor: Int? var nextCursor: Int? var hasVisible: Bool? var statuses: [StatusModel]? var totalNumber: Int? required init?(_ map: Map) { } func mapping(map: Map) { previousCursor <- map["previous_cursor"] nextCursor <- map["next_cursor"] hasVisible <- map["hasvisible"] statuses <- map["statuses"] totalNumber <- map["total_number"] } }
和
import ObjectMapper class StatusModel: BaseModel { var statusId: String? var thumbnailPic: String? var bmiddlePic: String? var originalPic: String? var weiboText: String? var user: WBUserModel? required init?(_ map: Map) { super.init(map) } override func mapping(map: Map) { super.mapping(map) statusId <- map["id"] thumbnailPic <- map["thumbnail_pic"] bmiddlePic <- map["bmiddle_pic"] originalPic <- map["original_pic"] weiboText <- map["text"] } }
其中內容全部都放在類StatusModel
中,圖片我們用屬性bmiddlePic
,文字用weiboText
。其他屬性留着以后使用。
請求完成以后,這些time line的微博會存在一個屬性里做為數據源使用。
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { private var timeLineStatus: [StatusModel]? // 1 //2 Alamofire.request(.GET, "https://api.weibo.com/2/statuses/friends_timeline.json", parameters: parameters, encoding: .URL, headers: nil) .responseString(completionHandler: {response in let statuses = Mapper<BaseModel>().map(response.result.value) if let timeLine = statuses where timeLine.totalNumber > 0 { self.timeLineStatus = timeLine.statuses // 3 self.collectionView?.reloadData() } }) }
- 存放數據源的屬性。
Alamofire
發出http請求。- 請求成功之后解析數據,並把我們需要的微博數據存放在屬性
self.timeLineStatus
。
在展示數據的時候需要區分微博的圖片是否存在,存在則優先展示圖片,否則展示文字。
一個不怎么好的做法是在方法cell for collection view里判斷數據源是否存在,遍歷每一個數據源的item判斷這個item是否有圖片。。。
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { if let statuses = self.timeLineStatus { let status = statuses[indexPath.item] if status } }
這樣顯然太過冗長了,所以我們要把這一部分代碼提升出來。
/** get status and if this status has image or not @return: status, one of the timeline Int, 1: there's image, 0: there's no image, -1: empty status */ func getWeiboStatus(indexPath: NSIndexPath) -> (status: StatusModel?, hasImage: Int) { // 1 if let timeLineStatusList = self.timeLineStatus where timeLineStatusList.count > 0 { let status = timeLineStatusList[indexPath.item] if let middlePic = status.bmiddlePic where middlePic != "" { // there's middle sized image to show return (status, 1) } else { // start to consider text return (status, 0) } } return (nil, -1) }
swift是可以在一個方法里返回多個值的。這個多個內容的值用tuple
來存放。調用時這樣的:
let status = self.getWeiboStatus(indexPath) let hasImage = status?.hasImage // if there's a image let imageUrl = status.status?.bmiddlePic // image path let text = status.status?.weiboText // text
只要通過let hasImage = status?.hasImage
就可以判斷是否有圖片。所以Swift的這一點還是非常方便的。那么在判斷要顯示哪一種Cell的時候就非常的方便了。修改后的代碼也非常的簡潔。這個習慣需要一直保持下去。
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let status = self.getWeiboStatus(indexPath) var cell: UICollectionViewCell = UICollectionViewCell() guard let _ = status.status else { cell.backgroundColor = UIColor.darkTextColor() return cell } if status.hasImage == 1 { cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) let weiboImageCell = cell as! WeiboImageCell weiboImageCell.weiboImageView.backgroundColor = UIColor.blueColor() weiboImageCell.weiboImageView.sd_setImageWithURL(NSURL(string: status.status?.bmiddlePic ?? "")) } else if status.hasImage == 0 { cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseTextIdentifier, forIndexPath: indexPath) let weiboTextCell = cell as! WeiboTextCell weiboTextCell.setCellWidth(self.cellWidth) weiboTextCell.weiboTextLabel.text = status.status?.weiboText ?? "" weiboTextCell.contentView.backgroundColor = UIColor.orangeColor() weiboTextCell.weiboTextLabel.backgroundColor = UIColor.redColor() } else { cell = UICollectionViewCell() } cell.backgroundColor = UIColor.orangeColor() //3 return cell }
跑起來,看看運行效果。
好丑!!!
全部代碼在這里。