主要學習與運行效果
在本節(jié)的內(nèi)容中,我們將通過一個具體實例的實現(xiàn)過程,詳細講解在表視圖當中,如何創(chuàng)建一個可展開可收縮的表視圖。為了讓讀者有著更為直觀的印象,我們將通過模仿QQ好友列表來實現(xiàn)這個效果。
該示例主要演示:
1.表視圖外觀設計
2.自定義用戶組設計
3.從plist文件中讀取數(shù)據(jù)
4.將數(shù)據(jù)顯示在表視圖中
5.實現(xiàn)表格的展開、收縮效果
運行效果如下所示:
表視圖外觀設計
我們使用Single View Application模板創(chuàng)建一個swift項目,命名為Friend List,為了簡便起見,Devices我們選擇iPhone進行開發(fā)。
打開Main.storyboard文件,刪除View Controller Scene,從Xcode右下方的Object Library面板中將Navagation Controller拖動到故事板中,如下圖所示:
Navigation Controller Scene包含了兩個視圖,一個是導航視圖,一個是表視圖。選中Navagation Controller Scene,取消Use Size Classes選項,勾選Is Initial View Controller選項,將其作為初始視圖控制器運行。
設置完成后,故事板將如下圖所示:
選中Root View Controller的導航欄,修改其標題為“聯(lián)系人”。完成此操作后,選擇Table View中的Table View Cell,在Identifier項中輸入:FriendCellIdentifier來為其增加標識符。對于Xcode來說,每一種表視圖單元格都需要聲明其標識符,以讓Xcode能夠?qū)ζ溥M行定位。如圖所示:
此時的單元格高度還比較窄,為了能夠達到我們想要的結(jié)果,我們需要設置其高度,同時也因為其高度固定,因此我們定位到單元格的Size inspector中,將Row Height設置為66(或者其他你看起來舒服的值)。如圖所示:
向單元格中添加一個Image控件,兩個Label控件,以完成粗略的QQ好友信息設計。設計效果如下圖所示:
由于我們創(chuàng)建了一個自定義的表視圖單元格,因此我們最好為其創(chuàng)建一個專門的類來定義其結(jié)構(gòu),以便以后能夠借用此數(shù)據(jù)結(jié)構(gòu)來保存從文件中讀取的信息。
創(chuàng)建一個Friend.swift文件,文件內(nèi)容如下:
import UIKit class Friend: NSObject { var Avatars: String = "user_default" // 圖片名稱,定義朋友的頭像 var Name: String = "" // 字符串,定義朋友的名字 var Intro: String = "" // 字符串,定義朋友的個性簽名 var VIP: Bool = false // 布爾值,確定朋友是否為VIP }
其中,user_default文件為用戶默認頭像,已經(jīng)放置在Image.xcassets中,用戶可以自行添加你喜愛的頭像并放置在xcassets中。
然后我們創(chuàng)建一個FriendCell.swift文件,這個文件將和我們剛剛創(chuàng)建的那個單元格進行綁定。文件內(nèi)容如下:
import UIKit class FriendCell: UITableViewCell { @IBOutlet weak var ImgAvatars: UIImageView! @IBOutlet weak var LblName: UILabel! @IBOutlet weak var LblIntro: UILabel! var friend: Friend = Friend() // 設置朋友信息 func setFriend(newfriend: Friend) { var Image: UIImage? = UIImage(named: "\(newfriend.Avatars)") if Image != nil { ImgAvatars.image = Image! }else{ ImgAvatars.image = UIImage(named: "user_default") } LblName.text = newfriend.Name LblIntro.text = newfriend.Intro if friend.VIP { LblName.textColor = UIColor.redColor() }else{ LblName.textColor = UIColor.blackColor() } } }
其中,ImgAvatars與單元格中的Image控件進行綁定,LblName和LblIntro分別與單元格中的兩個Label控件進行綁定。注意此時單元格的Identify inspector中的Class要設置為我們剛剛創(chuàng)建的FriendCell類。
接下來的代碼就比較直觀。這里我們對頭像的讀取進行了一個可選值的判定。首先先根據(jù)記錄中的頭像名稱去讀取存儲好的頭像。如果沒有讀取成功,那么Image變量將會返回一個nil值,這時就將頭像設置為我們默認的頭像:user_default。
自定義用戶組設計
在iOS應用中,我們可以自定義表視圖單元格的風格,其實原理就是向單元格中添加子視圖。添加子視圖的方法主要有三種:使用代碼、從.xib文件加載以及直接使用storyboard進行設計。在上一節(jié)中我們就是使用storyboard進行設計,非常方便和直觀。
在本節(jié)自定義用戶組中,我們要設計單元格折疊后的父類單元格。出于代碼的簡便和直觀起見,同時為了也為了讓讀者盡可能多的掌握自定義表視圖的方法,因此這里我們采用.xib文件進行加載。
新建一個xib文件,依次選擇New File -> iOS -> User Interface -> Empty,命名為SectionHeaderView
。這樣我們就創(chuàng)建了一個xib文件。由圖中可以看到,xib文件和storyboard的區(qū)別并不是很大。簡單理解來說,可以把StoryBoard看做是一組viewController對應的xib,以及它們之間的轉(zhuǎn)換方式的集合。
我們強烈建議大家采用storyboard進行界面設計,因為storyboard是iOS 5之后蘋果提供的以及強烈建議開發(fā)者使用的配置。但是,由于storyboard中已經(jīng)不允許有單個view的存在,因此在某些時候我們還是需要借助于單個的xib來自定義UI。這是由于storyboard的設計理念造成的。storyboard重視層次結(jié)構(gòu),重視UI的架構(gòu)和設計,更重視項目的流程。而對于單個的UI來說,則更注重于重用和定制。
向xib界面中拖入一個View,將其Attributes inspector中的Size修改為Freeform(允許調(diào)整View的大?。琒tatus Bar修改為None(取消狀態(tài)欄顯示)。
向view中拖入一個Button控件和Label控件,適當調(diào)整大小,如圖所示:
這時我們同樣需要創(chuàng)建一個類來定義這個自定義用戶組的數(shù)據(jù)結(jié)構(gòu),新建一個Group.swift文件,文件內(nèi)容如下:
import UIKit class Group: NSObject { var name: String = "" // 字符串,定義組名稱 var friends: NSArray = NSArray() // 數(shù)組,定義了該組內(nèi)所有朋友 }接下來,我們需要為我們自定義的表視圖創(chuàng)建一個類來與之進行綁定。創(chuàng)建一個SectionHeaderView.swift文件,文件內(nèi)容如下:
import UIKit // 該協(xié)議將被用戶組的委托實現(xiàn); 當用戶組被打開/關閉時,它將通知發(fā)送給委托,來告知Xcode調(diào)用何方法 protocol SectionHeaderViewDelegate: NSObjectProtocol { func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) } class SectionHeaderView: UITableViewHeaderFooterView { @IBOutlet weak var LblTitle: UILabel! @IBOutlet weak var BtnDisclosure: UIButton! var delegate: SectionHeaderViewDelegate! var section: Int! var HeaderOpen: Bool = false // 標記HeaderView是否展開 override func awakeFromNib() { // 設置disclosure 按鈕的圖片(被打開) self.BtnDisclosure.setImage(UIImage(named: "carat-open"), forState: UIControlState.Selected) // 單擊手勢識別 var tapGesture = UITapGestureRecognizer(target: self, action: "btnTap:") self.addGestureRecognizer(tapGesture) } @IBAction func btnTap(sender: UITapGestureRecognizer) { self.toggleOpen(true) } func toggleOpen(userAction: Bool) { BtnDisclosure.selected = !BtnDisclosure.selected // 如果userAction傳入的值為真,將給委托傳遞相應的消息 if userAction { if HeaderOpen { delegate.sectionHeaderView(self, sectionClosed: section) } else { delegate.sectionHeaderView(self, sectionOpened: section) } } } }
使用協(xié)議的原因是SectionHeaderView是我們自定義的一個視圖,而且我們專門為其新建了一個類文件來進行管理?!@個協(xié)議定義了兩個方法,這兩個方法名稱相同,但是參數(shù)不同,被稱為函數(shù)重載。在接下來我們的使用中,可以直接使用Selector選擇其參數(shù)名來進行調(diào)用。
awakeFromNib()是.nib文件被加載的時候,創(chuàng)建view對象前調(diào)用的方法。其和viewDidLoad()的區(qū)別是,當view對象被加載到內(nèi)存時系統(tǒng)才會調(diào)用viewDidLoad()方法。因此,Xcode會先執(zhí)行awakeFromNib()方法,才會執(zhí)行viewDidLoad()方法。
那為什么我們要用awakeFromNib()方法呢,那是因為UITableViewHeaderFooterView里面并不存在viewDidLoad()方法,我們沒有辦法對其重載調(diào)用,所以只能使用awakeFromNib()方法。
由于向.xib文件中直接添加手勢,會導致該nib文件注冊失敗,因此我們用代碼的形式來定義一個單擊手勢。這個手勢將控制單元格的展開和收縮。
最后要注意的是,由于我們的類繼承的是UITableViewHeaderFooterView,雖然在UIKit中,這個類是UIView的子類,但是由于swift還不完善的原因,在IB面板中(包括xib文件和storyboard文件)的Identity inspector的Class項中都不能夠顯示出我們剛剛創(chuàng)建出來的類,但是我們可以手動輸入這個類的名稱。
此時我們僅僅只是定義了SectionHeaderView.xib的屬性和方法,但是由于用戶組里面會包含多個FriendCell,因此我們還要在定義一個類來標明Group和SectionHeaderView之間的結(jié)構(gòu)聯(lián)系。
新建一個SectionInfo.swift文件,文件內(nèi)容如下:
import Foundation // 定義了用戶組以及FriendCell的一系列屬性、方法 class SectionInfo: NSObject { var group: Group = Group() var headerView: SectionHeaderView = SectionHeaderView() }
至此,所有的表格結(jié)構(gòu)、數(shù)據(jù)結(jié)構(gòu)已經(jīng)定義完成,接下來就是要從數(shù)據(jù)層面進行操作了。
從plist文件中讀取數(shù)據(jù)
為了方便數(shù)據(jù)管理,我們使用數(shù)據(jù)持久化功能來保存用戶組信息和朋友信息。這里我們采用屬性列表來保存數(shù)據(jù)。應用程序在啟動時會將該文件的全部內(nèi)容讀入內(nèi)存,并在退出時注銷。
新建一個plist文件,依次選擇New File -> iOS -> Resource -> Property List,命名為FriendInfo。這樣我們就創(chuàng)建了一個plist文件。將文件中的Property List Type修改為None,然后按照下圖所示設計文件內(nèi)容:
接下來打開ViewController.swift文件,創(chuàng)建一個函數(shù),命名為loadFriendInfo,用來讀取文件。函數(shù)如下:
func loadFriendInfo() -> NSArray { var FriendInfo: NSMutableArray? // 定位到plist文件并將文件拷貝到數(shù)組中存放 var fileUrl = NSBundle.mainBundle().URLForResource("FriendInfo", withExtension: "plist") var GroupDictionariesArray = NSArray(contentsOfURL: fileUrl!) FriendInfo = NSMutableArray(capacity: GroupDictionariesArray!.count) // 遍歷數(shù)組,根據(jù)組和單元格的結(jié)構(gòu)分別賦值 for GroupDictionary in GroupDictionariesArray! { var group: Group = Group() group.name = GroupDictionary["GroupName"] as String var friendDictionaries: NSArray = GroupDictionary["Friends"] as NSArray var friends = NSMutableArray(capacity: friendDictionaries.count) for friendDictionary in friendDictionaries { var friendAsDic: NSDictionary = friendDictionary as NSDictionary var friend: Friend = Friend() friend.setValuesForKeysWithDictionary(friendAsDic) friends.addObject(friend) } group.friends = friends FriendInfo!.addObject(group) } return FriendInfo! }
我們來逐次分析這個函數(shù)。首先我們需要定位到我們創(chuàng)建的FriendInfo.plist文件,NSBundle.mainBundle中保存了一系列當前項目的信息,包括版本號、程序名等等內(nèi)容,這里我們使用URLForResource()來獲取FriendInfo.plist的URL地址。
NSArray的contentsOfURL:
由此,我們就完成了從plist文件中讀取數(shù)據(jù)的操作,接下來我們可以使用FriendInfo數(shù)組里面的值,來完成值的賦予。
將數(shù)據(jù)顯示在表視圖中
有了上面的操作,現(xiàn)在我們就可以將我們讀取到的數(shù)據(jù)顯示在表視圖當中了。
我們首先定義一個類型為NSArray數(shù)組的變量,用來存放我們讀取后的數(shù)據(jù),以及存放用戶組、單元格信息的NSMutableArray變量:
var groups: NSArray! var sectionInfoArray: NSMutableArray!接下來,我們在viewDidLoad()方法中添加如下語句,完成數(shù)據(jù)的讀取和存放:
self.tableView.sectionHeaderHeight = CGFloat(HeaderHeight) // 用戶組高度 opensectionindex = NSNotFound groups = loadFriendInfo() let sectionHeaderNib: UINib = UINib(nibName: "SectionHeaderView", bundle: nil) self.tableView.registerNib(sectionHeaderNib, forHeaderFooterViewReuseIdentifier: SectionHeaderViewIdentifier)
后面兩個語句本章后面會對其詳細介紹。
接下來,我們從父類視圖中重寫viewWillAppear方法,來完成分組表的定義。
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) // 檢查SectionInfoArray是否已被創(chuàng)建,如果已被創(chuàng)建,則檢查組的數(shù)量是否匹配當前實際組的數(shù)量。通常情況下,您需要保持SectionInfo與組、單元格信息保持同步。如果擴展功能以讓用戶能夠在表視圖中編輯信息,那么需要在編輯操作中適當更新SectionInfo if sectionInfoArray == nil || sectionInfoArray.count != self.numberOfSectionsInTableView(self.tableView) { // 對于每個用戶組來說,需要為每個單元格設立一個一致的SectionInfo對象 var infoArray: NSMutableArray = NSMutableArray() for group in groups { var dictionary: NSArray = (group as Group).friends var sectionInfo = SectionInfo() sectionInfo.group = group as Group sectionInfo.headerView.HeaderOpen = false infoArray.addObject(sectionInfo) } sectionInfoArray = infoArray } }接下來依次實現(xiàn)這幾個方法:
override func canBecomeFirstResponder() -> Bool { return true }
判斷一個對象是否可以成為第一響應者。默認返回false。
如果一個響應對象通過這個方法返回true,那么它成為了第一響應對象,并且可以接收觸摸事件和動作消息。
我們的UITableView是UIView的子類,因此必須重寫這個方法才可以成為第一響應者。
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return self.groups.count }numberOfSectionsInTableView()方法返回表視圖有多少個section。一個用戶組對應一個section。
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { var sectionInfo: SectionInfo = sectionInfoArray[section] as SectionInfo var numStoriesInSection = sectionInfo.group.friends.count var sectionOpen = sectionInfo.headerView.HeaderOpen return sectionOpen ? numStoriesInSection : 0 }tableView:numberOfRowsInSection:方法返回對應的section中有多少個元素,也就是多少行。在這里我們先確定用戶組是否被打開,如果打開則返回對應的用戶組中的所有朋友數(shù)量,否則為0。
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let FriendCellIdentifier = "FriendCellIdentifier" var cell: FriendCell = tableView.dequeueReusableCellWithIdentifier(FriendCellIdentifier) as FriendCell var group: Group = (sectionInfoArray[indexPath.section] as SectionInfo).group cell.friend = group.friends[indexPath.row] as Friend cell.setFriend(cell.friend) return cell }tableView:cellForRowAtIndexPath:方法返回指定的行的單元格。一個朋友對應一個單元格。在這個方法中,我們通過dequeueReusableCellWithIdentifier()方法來讀取對應標識符的單元格,在這里是我們在main.Stroyboard中定義的那個單元格。還記得我們給那個單元格添加了“FriendCellIdentifier”標識符嗎?
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { // 返回指定的section header視圖 var sectionHeaderView: SectionHeaderView = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(SectionHeaderViewIdentifier) as SectionHeaderView var sectionInfo: SectionInfo = sectionInfoArray[section] as SectionInfo sectionInfo.headerView = sectionHeaderView sectionHeaderView.LblTitle.text = sectionInfo.group.name sectionHeaderView.section = section sectionHeaderView.delegate = self return sectionHeaderView }
和上面的方法相似,tableView:viewForHeaderInSection:方法返回section中的表頭(Header)類型。我們的SectionHeaderView聲明的是UITableViewHeaderFooterView類型,這個方法是專門用來返回該類型的實例的。
可以看到,這個方法中使用了和上面方法極其相似的dequeueReusableHeaderFooterViewWithIdentifier()方法。這個方法的作用同樣也是讀取對應標識符的單元格。不過不同的是,使用這個方法前需要注冊nib文件或者注冊描述這個單元格的類。因此,之前我們就使用了如下兩條語句注冊nib文件,以便于swift能夠讀取到這個單元格。
let sectionHeaderNib: UINib = UINib(nibName: "SectionHeaderView", bundle: nil) self.tableView.registerNib(sectionHeaderNib, forHeaderFooterViewReuseIdentifier: SectionHeaderViewIdentifier)
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return CGFloat(DefaultRowHeight) }
這個方法返回指定的單元格的高度,這里我們返回的是單元格的默認高度。
實現(xiàn)表格的展開、收縮效果
我們給ViewController這個類繼承SectionHeaderViewDelegate協(xié)議,此時,類的頭部變成這樣:
class ViewController: UITableViewController, SectionHeaderViewDelegate接下來,我們在ViewController類中實現(xiàn)協(xié)議中定義的兩個函數(shù):
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) { var sectionInfo: SectionInfo = sectionInfoArray[sectionOpened] as SectionInfo sectionInfo.headerView.HeaderOpen = true //創(chuàng)建一個包含單元格索引路徑的數(shù)組來實現(xiàn)插入單元格的操作:這些路徑對應當前節(jié)的每個單元格 var countOfRowsToInsert = sectionInfo.group.friends.count var indexPathsToInsert = NSMutableArray() for (var i = 0; i < countOfRowsToInsert; i++) { indexPathsToInsert.addObject(NSIndexPath(forRow: i, inSection: sectionOpened)) } // 創(chuàng)建一個包含單元格索引路徑的數(shù)組來實現(xiàn)刪除單元格的操作:這些路徑對應之前打開的節(jié)的單元格 var indexPathsToDelete = NSMutableArray() var previousOpenSectionIndex = opensectionindex if previousOpenSectionIndex != NSNotFound { var previousOpenSection: SectionInfo = sectionInfoArray[previousOpenSectionIndex] as SectionInfo previousOpenSection.headerView.HeaderOpen = false previousOpenSection.headerView.toggleOpen(false) var countOfRowsToDelete = previousOpenSection.group.friends.count for (var i = 0; i < countOfRowsToDelete; i++) { indexPathsToDelete.addObject(NSIndexPath(forRow: i, inSection: previousOpenSectionIndex)) } } // 設計動畫,以便讓表格的打開和關閉擁有一個流暢的效果 var insertAnimation: UITableViewRowAnimation var deleteAnimation: UITableViewRowAnimation if previousOpenSectionIndex == NSNotFound || sectionOpened < previousOpenSectionIndex { insertAnimation = UITableViewRowAnimation.Top deleteAnimation = UITableViewRowAnimation.Bottom }else{ insertAnimation = UITableViewRowAnimation.Bottom deleteAnimation = UITableViewRowAnimation.Top } // 應用單元格的更新 self.tableView.beginUpdates() self.tableView.deleteRowsAtIndexPaths(indexPathsToDelete, withRowAnimation: deleteAnimation) self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: insertAnimation) opensectionindex = sectionOpened self.tableView.endUpdates() }
我們來解析一下這個函數(shù)。首先創(chuàng)建了一個包含單元格索引路徑的數(shù)組來實現(xiàn)插入單元格的操作,這個數(shù)組存放有將要打開的用戶組中的所有朋友信息。接下來是創(chuàng)建了一個包含單元格索引路徑的數(shù)組來實現(xiàn)刪除單元格的操作。首先將先前已打開的用戶組關閉(調(diào)用toggleOpen()函數(shù)),隨后將數(shù)組中放入已打開的用戶組中的所有朋友信息。
最后,執(zhí)行刪除行的操作,再執(zhí)行插入行的操作,注意順序不要顛倒了(想想為什么?)
我們使用beginUpdates()方法和endUpdates()方法將刪除、插入操作“包”了起來,這兩個方法是配合起來使用的,標記了一個tableView的動畫塊,分別代表動畫的開始和結(jié)束。
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) { // 在表格關閉的時候,創(chuàng)建一個包含單元格索引路徑的數(shù)組,接下來從表格中刪除這些行 var sectionInfo: SectionInfo = self.sectionInfoArray[sectionClosed] as SectionInfo sectionInfo.headerView.HeaderOpen = false var countOfRowsToDelete = self.tableView.numberOfRowsInSection(sectionClosed) if countOfRowsToDelete > 0 { var indexPathsToDelete = NSMutableArray() for (var i = 0; i < countOfRowsToDelete; i++) { indexPathsToDelete.addObject(NSIndexPath(forRow: i, inSection: sectionClosed)) } self.tableView.deleteRowsAtIndexPaths(indexPathsToDelete, withRowAnimation: UITableViewRowAnimation.Top) } opensectionindex = NSNotFound }
和上面的方法類似,在此我們不做過多的解釋了。
到這里,我們的教程就結(jié)束了。有興趣的同學可以去我的 Github 上面下載demo項目的源代碼:TVAnimationsGestures-Swift,這個 demo 是蘋果官方提供的 demo 的 Swift 版本,大家可以基于這個版本來實現(xiàn)可展開可收縮的表視圖。