Swift 可展開可收縮的表視圖

jopen 9年前發布 | 56K 次閱讀 Swift Apple Swift開發

主要學習與運行效果

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

該示例主要演示:

1.表視圖外觀設計

2.自定義用戶組設計

3.從plist文件中讀取數據

4.將數據顯示在表視圖中

5.實現表格的展開、收縮效果

運行效果如下所示:

Swift 可展開可收縮的表視圖

表視圖外觀設計

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

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

Swift 可展開可收縮的表視圖

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

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

Swift 可展開可收縮的表視圖

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

Swift 可展開可收縮的表視圖

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

Swift 可展開可收縮的表視圖

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

Swift 可展開可收縮的表視圖

由于我們創建了一個自定義的表視圖單元格,因此我們最好為其創建一個專門的類來定義其結構,以便以后能夠借用此數據結構來保存從文件中讀取的信息。

創建一個Friend.swift文件,文件內容如下:

import UIKit
class Friend: NSObject {
    var Avatars: String =  "user_default"   // 圖片名稱,定義朋友的頭像
    var Name: String = ""           // 字符串,定義朋友的名字
    var Intro: String = ""            // 字符串,定義朋友的個性簽名
    var VIP: Bool = false     // 布爾值,確定朋友是否為VIP
}

其中,user_default文件為用戶默認頭像,已經放置在Image.xcassets中,用戶可以自行添加你喜愛的頭像并放置在xcassets中。

然后我們創建一個FriendCell.swift文件,這個文件將和我們剛剛創建的那個單元格進行綁定。文件內容如下:

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要設置為我們剛剛創建的FriendCell類。

接下來的代碼就比較直觀。這里我們對頭像的讀取進行了一個可選值的判定。首先先根據記錄中的頭像名稱去讀取存儲好的頭像。如果沒有讀取成功,那么Image變量將會返回一個nil值,這時就將頭像設置為我們默認的頭像:user_default。

自定義用戶組設計

在iOS應用中,我們可以自定義表視圖單元格的風格,其實原理就是向單元格中添加子視圖。添加子視圖的方法主要有三種:使用代碼、從.xib文件加載以及直接使用storyboard進行設計。在上一節中我們就是使用storyboard進行設計,非常方便和直觀。

在本節自定義用戶組中,我們要設計單元格折疊后的父類單元格。出于代碼的簡便和直觀起見,同時為了也為了讓讀者盡可能多的掌握自定義表視圖的方法,因此這里我們采用.xib文件進行加載。

新建一個xib文件,依次選擇New File -> iOS -> User Interface -> Empty,命名為SectionHeaderView

。這樣我們就創建了一個xib文件。由圖中可以看到,xib文件和storyboard的區別并不是很大。簡單理解來說,可以把StoryBoard看做是一組viewController對應的xib,以及它們之間的轉換方式的集合。

我們強烈建議大家采用storyboard進行界面設計,因為storyboard是iOS 5之后蘋果提供的以及強烈建議開發者使用的配置。但是,由于storyboard中已經不允許有單個view的存在,因此在某些時候我們還是需要借助于單個的xib來自定義UI。這是由于storyboard的設計理念造成的。storyboard重視層次結構,重視UI的架構和設計,更重視項目的流程。而對于單個的UI來說,則更注重于重用和定制。

Swift 可展開可收縮的表視圖

向xib界面中拖入一個View,將其Attributes inspector中的Size修改為Freeform(允許調整View的大小),Status Bar修改為None(取消狀態欄顯示)。

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

Swift 可展開可收縮的表視圖

這時我們同樣需要創建一個類來定義這個自定義用戶組的數據結構,新建一個Group.swift文件,文件內容如下:

import UIKit
class Group: NSObject {
    var name: String = ""       // 字符串,定義組名稱
    var friends: NSArray = NSArray()    // 數組,定義了該組內所有朋友
}

接下來,我們需要為我們自定義的表視圖創建一個類來與之進行綁定。創建一個SectionHeaderView.swift文件,文件內容如下:

import UIKit
// 該協議將被用戶組的委托實現; 當用戶組被打開/關閉時,它將通知發送給委托,來告知Xcode調用何方法
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)
            }
        }
    }
}

使用協議的原因是SectionHeaderView是我們自定義的一個視圖,而且我們專門為其新建了一個類文件來進行管理。……這個協議定義了兩個方法,這兩個方法名稱相同,但是參數不同,被稱為函數重載。在接下來我們的使用中,可以直接使用Selector選擇其參數名來進行調用。

awakeFromNib()是.nib文件被加載的時候,創建view對象前調用的方法。其和viewDidLoad()的區別是,當view對象被加載到內存時系統才會調用viewDidLoad()方法。因此,Xcode會先執行awakeFromNib()方法,才會執行viewDidLoad()方法。

那為什么我們要用awakeFromNib()方法呢,那是因為UITableViewHeaderFooterView里面并不存在viewDidLoad()方法,我們沒有辦法對其重載調用,所以只能使用awakeFromNib()方法。

由于向.xib文件中直接添加手勢,會導致該nib文件注冊失敗,因此我們用代碼的形式來定義一個單擊手勢。這個手勢將控制單元格的展開和收縮。

最后要注意的是,由于我們的類繼承的是UITableViewHeaderFooterView,雖然在UIKit中,這個類是UIView的子類,但是由于swift還不完善的原因,在IB面板中(包括xib文件和storyboard文件)的Identity inspector的Class項中都不能夠顯示出我們剛剛創建出來的類,但是我們可以手動輸入這個類的名稱。

Swift 可展開可收縮的表視圖

此時我們僅僅只是定義了SectionHeaderView.xib的屬性和方法,但是由于用戶組里面會包含多個FriendCell,因此我們還要在定義一個類來標明Group和SectionHeaderView之間的結構聯系。

新建一個SectionInfo.swift文件,文件內容如下:

import Foundation
// 定義了用戶組以及FriendCell的一系列屬性、方法
class SectionInfo: NSObject {
    var group: Group = Group()
    var headerView: SectionHeaderView = SectionHeaderView()
}

至此,所有的表格結構、數據結構已經定義完成,接下來就是要從數據層面進行操作了。

從plist文件中讀取數據

為了方便數據管理,我們使用數據持久化功能來保存用戶組信息和朋友信息。這里我們采用屬性列表來保存數據。應用程序在啟動時會將該文件的全部內容讀入內存,并在退出時注銷。

新建一個plist文件,依次選擇New File -> iOS -> Resource -> Property List,命名為FriendInfo。這樣我們就創建了一個plist文件。將文件中的Property List Type修改為None,然后按照下圖所示設計文件內容:

Swift 可展開可收縮的表視圖

接下來打開ViewController.swift文件,創建一個函數,命名為loadFriendInfo,用來讀取文件。函數如下:

func loadFriendInfo() -> NSArray {
    var FriendInfo: NSMutableArray?
    // 定位到plist文件并將文件拷貝到數組中存放
    var fileUrl = NSBundle.mainBundle().URLForResource("FriendInfo", withExtension: "plist")
    var GroupDictionariesArray = NSArray(contentsOfURL: fileUrl!)
    FriendInfo = NSMutableArray(capacity: GroupDictionariesArray!.count)
    // 遍歷數組,根據組和單元格的結構分別賦值
    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!
}

我們來逐次分析這個函數。首先我們需要定位到我們創建的FriendInfo.plist文件,NSBundle.mainBundle中保存了一系列當前項目的信息,包括版本號、程序名等等內容,這里我們使用URLForResource()來獲取FriendInfo.plist的URL地址。

NSArray的contentsOfURL:

由此,我們就完成了從plist文件中讀取數據的操作,接下來我們可以使用FriendInfo數組里面的值,來完成值的賦予。

將數據顯示在表視圖中

有了上面的操作,現在我們就可以將我們讀取到的數據顯示在表視圖當中了。

我們首先定義一個類型為NSArray數組的變量,用來存放我們讀取后的數據,以及存放用戶組、單元格信息的NSMutableArray變量:

var groups: NSArray!
var sectionInfoArray: NSMutableArray!

接下來,我們在viewDidLoad()方法中添加如下語句,完成數據的讀取和存放:

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是否已被創建,如果已被創建,則檢查組的數量是否匹配當前實際組的數量。通常情況下,您需要保持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
    }
}

接下來依次實現這幾個方法:

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中有多少個元素,也就是多少行。在這里我們先確定用戶組是否被打開,如果打開則返回對應的用戶組中的所有朋友數量,否則為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)
}

這個方法返回指定的單元格的高度,這里我們返回的是單元格的默認高度。

實現表格的展開、收縮效果

我們給ViewController這個類繼承SectionHeaderViewDelegate協議,此時,類的頭部變成這樣:

class ViewController: UITableViewController, SectionHeaderViewDelegate

接下來,我們在ViewController類中實現協議中定義的兩個函數:

func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) {
    var sectionInfo: SectionInfo = sectionInfoArray[sectionOpened] as SectionInfo
    sectionInfo.headerView.HeaderOpen = true
    //創建一個包含單元格索引路徑的數組來實現插入單元格的操作:這些路徑對應當前節的每個單元格
    var countOfRowsToInsert = sectionInfo.group.friends.count
    var indexPathsToInsert = NSMutableArray()
    for (var i = 0; i < countOfRowsToInsert; i++) {         indexPathsToInsert.addObject(NSIndexPath(forRow: i, inSection: sectionOpened))     }     // 創建一個包含單元格索引路徑的數組來實現刪除單元格的操作:這些路徑對應之前打開的節的單元格     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()     }

我們來解析一下這個函數。首先創建了一個包含單元格索引路徑的數組來實現插入單元格的操作,這個數組存放有將要打開的用戶組中的所有朋友信息。接下來是創建了一個包含單元格索引路徑的數組來實現刪除單元格的操作。首先將先前已打開的用戶組關閉(調用toggleOpen()函數),隨后將數組中放入已打開的用戶組中的所有朋友信息。

最后,執行刪除行的操作,再執行插入行的操作,注意順序不要顛倒了(想想為什么?)

我們使用beginUpdates()方法和endUpdates()方法將刪除、插入操作“包”了起來,這兩個方法是配合起來使用的,標記了一個tableView的動畫塊,分別代表動畫的開始和結束。

func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) {
    // 在表格關閉的時候,創建一個包含單元格索引路徑的數組,接下來從表格中刪除這些行
    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 }

和上面的方法類似,在此我們不做過多的解釋了。

到這里,我們的教程就結束了。有興趣的同學可以去我的 Github 上面下載demo項目的源代碼:TVAnimationsGestures-Swift,這個 demo 是蘋果官方提供的 demo 的 Swift 版本,大家可以基于這個版本來實現可展開可收縮的表視圖。

來源:星夜暮晨的簡書

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!