歡迎加入QQ討論群258996829
來自星星的你 頭像
蘋果3袋
3
來自星星的你

Swift 可展開可收縮的表視圖示例

發(fā)布時間:2015-01-22 21:42  回復:0  查看:3291   最后回復:2015-01-22 21:42  

主要學習與運行效果

在本節(jié)的內(nèi)容中,我們將通過一個具體實例的實現(xiàn)過程,詳細講解在表視圖當中,如何創(chuàng)建一個可展開可收縮的表視圖。為了讓讀者有著更為直觀的印象,我們將通過模仿QQ好友列表來實現(xiàn)這個效果。

該示例主要演示:

1.表視圖外觀設計

2.自定義用戶組設計

3.從plist文件中讀取數(shù)據(jù)

4.將數(shù)據(jù)顯示在表視圖中

5.實現(xiàn)表格的展開、收縮效果

運行效果如下所示:

74454-b6eeeddfe8f96b70.png

表視圖外觀設計

我們使用Single View Application模板創(chuàng)建一個swift項目,命名為Friend List,為了簡便起見,Devices我們選擇iPhone進行開發(fā)。

打開Main.storyboard文件,刪除View Controller Scene,從Xcode右下方的Object Library面板中將Navagation Controller拖動到故事板中,如下圖所示:

74454-72ccab93df2dbfd0.png

Navigation Controller Scene包含了兩個視圖,一個是導航視圖,一個是表視圖。選中Navagation Controller Scene,取消Use Size Classes選項,勾選Is Initial View Controller選項,將其作為初始視圖控制器運行。

設置完成后,故事板將如下圖所示:

74454-c7b660c7b72463ce.png

選中Root View Controller的導航欄,修改其標題為“聯(lián)系人”。完成此操作后,選擇Table View中的Table View Cell,在Identifier項中輸入:FriendCellIdentifier來為其增加標識符。對于Xcode來說,每一種表視圖單元格都需要聲明其標識符,以讓Xcode能夠?qū)ζ溥M行定位。如圖所示:

74454-f8afef2e54181509.png

此時的單元格高度還比較窄,為了能夠達到我們想要的結(jié)果,我們需要設置其高度,同時也因為其高度固定,因此我們定位到單元格的Size inspector中,將Row Height設置為66(或者其他你看起來舒服的值)。如圖所示:

74454-22a3322873820a1a.png

向單元格中添加一個Image控件,兩個Label控件,以完成粗略的QQ好友信息設計。設計效果如下圖所示:

74454-3101ba12b7f426aa.png

由于我們創(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來說,則更注重于重用和定制。

74454-d4e4c78806d74da7.png

向xib界面中拖入一個View,將其Attributes inspector中的Size修改為Freeform(允許調(diào)整View的大?。琒tatus Bar修改為None(取消狀態(tài)欄顯示)。

向view中拖入一個Button控件和Label控件,適當調(diào)整大小,如圖所示:

74454-457d5eb9bd5092b7.png

這時我們同樣需要創(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)建出來的類,但是我們可以手動輸入這個類的名稱。

74454-fc68f5c9efc4d578.png

此時我們僅僅只是定義了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)容:

74454-25bfb5d431b7a1cb.png

接下來打開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)可展開可收縮的表視圖。

您還未登錄,請先登錄

熱門帖子

最新帖子

?