iOS開發tips-神奇的UITableView


概述

UITableView是iOS開發中使用頻率最高的UI控件,在前面的文章中對於UITableView的具體用法有詳細的描述,今天主要看一些UITableView開發中的常見一些坑,這些坑或許不深,但是如果開發中注意不到的話往往比較浪費時間。

神奇的section header

事情的起因是一個網友說要實現一個類似下圖界面,但是不管是設置sectionHeaderHeight還是代理方法中實現func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int)都無法調整Section Header的默認高度。而且他還試過通過func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?自定義一個header也是無濟於事。
Blog_iOS_tTps_UITableView_UITableViewDemo1

其實這個問題解決起來並不復雜,只要設置sectionFooterHeight為0即可(當然對應通過代理方法也是可以的)。默認情況下分組樣式UITableView的section header和section footer是由一個默認高度的,並不為0。

import UIKit

private let ProfileTableViewControllerCellReuseIdentifier = "ProfileTableViewCell"
class ProfileTableViewController: UIViewController {
    
    // MARK: - Nested type
    struct ProfileData {
        var title:String!
        var content:String!
    }

    // MARK: - TableView life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
        self.loadData()
    }

    // MARK: - Private method
    private func setup() {
        self.view.backgroundColor = UIColor.gray
        self.tableView.register(ProfileTableViewCell.self, forCellReuseIdentifier: ProfileTableViewControllerCellReuseIdentifier)
        self.tableView.dataSource = self
        self.tableView.delegate = self
        
        self.view.addSubview(self.tableView)
        self.tableView.snp.makeConstraints { (make) in
            make.edges.equalTo(0.0)
        }
        
    }
    
    private func loadData() {
        self.data.removeAll()
        
        let row1 = ProfileData(title: "Name", content: "Kenshin Cui")
        let row2 = ProfileData(title: "ID", content: "kenshincui")
        let section1 = [row1,row2]
        
        let row3 = ProfileData(title: "Gender", content: "Male")
        let row4 = ProfileData(title: "Region", content: "China")
        let section2 = [row3,row4]
        
        
        let row5 = ProfileData(title: "What's Up", content: "We're here to put a dent in the universe。 Otherwise why else even be here?")
        let section3 = [row5]
        
        self.data.append(section1)
        self.data.append(section2)
        self.data.append(section3)
        
        self.tableView.reloadData()
    }
    
    // MARK: - Private property
    private lazy var tableView:UITableView = {
        let temp = UITableView(frame: CGRect.zero, style: .grouped)
        temp.estimatedRowHeight = 50
        temp.sectionFooterHeight = 0
        return temp
    }()

    fileprivate var data = [[ProfileData]]()

}


extension ProfileTableViewController:UITableViewDataSource, UITableViewDelegate{
    // MARK: - Table view data source
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.data.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionData = self.data[section]
        return sectionData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewControllerCellReuseIdentifier, for: indexPath) as? ProfileTableViewCell {
            let dataItem = self.data[indexPath.section][indexPath.row]
            cell.title = dataItem.title
            cell.content = dataItem.content
            return cell
        }
        return UITableViewCell()
    }

    // MARK: - Table view delegate
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if section == 0 {
            return CGFloat.leastNormalMagnitude
        }
        return 8.0
    }
}

數據源和代理方法的自動調用

同樣是上面的代碼,如果去掉loadData()方法后面的reloadData()調用你會發現整個界面不會有任何異常現象,數據可以照樣加載,也就是說數據源方法和代理方法會正常調用加載對應的數據。這是為什么呢?
事實上類似於func numberOfSections(in tableView: UITableView) -> Int等方法並不是只有reloadData()等方法刷新的時候才會調用,而是在已經設置了dataSource和delegate后布局變化后就會調用。因此即使注釋掉上面的reloadData()方法界面仍然不會有變化。
要驗證這個結論可以延遲設置數據源和代理(注意:手動更新界面布局setNeedsLayoutlayoutIfNeeded),或者最簡單的方式就是旋轉屏幕會發現func numberOfSections(in tableView: UITableView) -> Intfunc tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat等方法都會調用。除此之外,你也會發現func numberOfSections(in tableView: UITableView) -> Int其實並不止一次調用,系統內部為了確定整個布局本身對它就存在着調用。

分割線左側對齊

貌似蘋果的設計分割就建議你將分割線左側留出一定的間距,但在實際開發過程中卻很少見設計師會這么做。要讓分割線左側對齊對於當前iOS 10來說應該是再簡單不過,只要設置UITableView的separatorInset = UIEdgeInsets.zero即可(如果僅僅想要控制器某個UITableViewCell的分割線則直接設置UITableViewCell的separatorInset,當然這種方式對於.grouped風格的UITableView而言無法修改Section頂部和底部分割線)。不過低版本的iOS就要復雜一些,例如iOS 7除了以上設置還要設置UITableViewCell的separatorInset;而iOS 8、9還要再設置UITableView的layoutMargins等。

當然,如果你希望控制右側的間距,仍然可以調整separatorInset的right即可,不過調整top、bottom應該不會生效。

移除多余的行

不妨將上面代碼修改為.plain風格的UITableView,此時會發現由於數據后面多了很多多余的空行。移除這些空行的方法也很簡單,那就是設置tableFooterView = UIView()即可(如果設置view的高度為CGFloat.leastNormalMagnitude則不顯示最后面的一條分割線)。當然,默認情況下style為.plain則每個section的上下均不存在分割線。

注意:.plain風格的UITableView設置sectionFooterHeight不起作用,必須通過func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat代理方法設置。

.plain下禁用section header懸停

有時候你不得不用.plain Style,因為這樣一來默認Section Header就有懸停效果,但是有時候你只是想要使用.plain樣式卻不想顯示懸停效果,這時你必須禁用這個效果。
當然可以使用很多黑科技來禁用懸停效果,但是最簡單的方式應該是直接將header隱藏起來:通過設置tableViewHeader高度等同於section header的高度,然后設置tableView的contentInset讓它偏移到上方,這樣一來當滾動到section header懸浮時出現的位置不是0而是被隱藏起來的偏移位置。

import UIKit

private let ProfileTableViewControllerCellReuseIdentifier = "ProfileTableViewCell"
class ProfileTableViewControllerWithPlain: UIViewController {
    
    // MARK: - Nested type
    struct ProfileData {
        var title:String!
        var content:String!
    }

    // MARK: - TableView life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
        self.loadData()
    }

    // MARK: - Private method
    private func setup() {
        self.view.backgroundColor = UIColor.gray
        self.tableView.register(ProfileTableViewCell.self, forCellReuseIdentifier: ProfileTableViewControllerCellReuseIdentifier)
        self.tableView.estimatedRowHeight = 50
        self.tableView.dataSource = self
        self.tableView.delegate = self
        
        self.view.addSubview(self.tableView)
        self.tableView.snp.makeConstraints { (make) in
            make.edges.equalTo(0.0)
        }
        
        // disable section header sticky
        let headerView = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 8.0))
        self.tableView.tableHeaderView = headerView
        self.tableView.contentInset.top = -8.0
        
    }
    
    private func loadData() {
        self.data.removeAll()
        
        let row1 = ProfileData(title: "Name", content: "Kenshin Cui")
        let row2 = ProfileData(title: "ID", content: "kenshincui")
        let section1 = [row1,row2]
        
        let row3 = ProfileData(title: "Gender", content: "Male")
        let row4 = ProfileData(title: "Region", content: "China")
        let section2 = [row3,row4]
        
        
        let row5 = ProfileData(title: "What's Up", content: "We're here to put a dent in the universe。 Otherwise why else even be here?")
        let section3 = [row5]
        
        self.data.append(section1)
        self.data.append(section2)
        self.data.append(section3)
        
//        self.tableView.reloadData()
    }
    
    // MARK: - Private property
    fileprivate lazy var tableView:UITableView = {
        let temp = UITableView(frame: CGRect.zero, style: .plain)
        temp.estimatedRowHeight = 50
        temp.tableFooterView = UIView()
        return temp
    }()

    fileprivate var data = [[ProfileData]]()

}


extension ProfileTableViewControllerWithPlain:UITableViewDataSource, UITableViewDelegate{
    // MARK: - Table view data source
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.data.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionData = self.data[section]
        return sectionData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewControllerCellReuseIdentifier, for: indexPath) as? ProfileTableViewCell {
            let dataItem = self.data[indexPath.section][indexPath.row]
            cell.title = dataItem.title
            cell.content = dataItem.content
            return cell
        }
        return UITableViewCell()
    }

    // MARK: - Table view delegate
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if section == 0 {
            return CGFloat.leastNormalMagnitude
        }
        return 8.0
    }

}

調整tableHeaderView的高度

如果你的UITableView設置了tableHeaderView的話,有事你可能喜歡動態調整tableHeaderView的高度,但是怎么修改這個view的高度都不會生效。正確的修改方法是受限修改view的高度,然后將這個view重新設置給UITableView的tableHeaderView屬性。下面的demo演示了這一過程,通過這個demo可以看到一個有趣的事實:如果你設置了tableHeaderView但是沒有指定高度的話,UITableView會自動給他提供一個默認高度。這在低版本的iOS系統中即使不指定tableHeaderView也會有這個一個默認高度,解決方式就是設置view的高度為一個極小值,當然iOS 10中如果不指定則默認沒有tableHeaderView。

import UIKit

private let ProfileTableViewControllerCellReuseIdentifier = "ProfileTableViewCell"
class ProfileTableViewControllerWithHeader: UIViewController {
    
    // MARK: - Nested type
    struct ProfileData {
        var title:String!
        var content:String!
    }
    
    // MARK: - TableView life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
        self.loadData()
        self.loadHeaderData()
    }
    
    // MARK: - Private method
    private func setup() {
        self.view.backgroundColor = UIColor.gray
        self.tableView.tableHeaderView = self.headerView
        self.tableView.register(ProfileTableViewCell.self, forCellReuseIdentifier: ProfileTableViewControllerCellReuseIdentifier)
        //        self.tableView.separatorInset = UIEdgeInsets.zero
        self.tableView.dataSource = self
        self.tableView.delegate = self
        
        self.view.addSubview(self.tableView)
        self.tableView.snp.makeConstraints { (make) in
            make.edges.equalTo(0.0)
        }
        
    }
    
    private func loadData() {
        self.data.removeAll()
        
        let row1 = ProfileData(title: "Name", content: "Kenshin Cui")
        let row2 = ProfileData(title: "ID", content: "kenshincui")
        let section1 = [row1,row2]
        
        let row3 = ProfileData(title: "Gender", content: "Male")
        let row4 = ProfileData(title: "Region", content: "China")
        let section2 = [row3,row4]
        
        
        let row5 = ProfileData(title: "What's Up", content: "We're here to put a dent in the universe。 Otherwise why else even be here?")
        let section3 = [row5]
        
        self.data.append(section1)
        self.data.append(section2)
        self.data.append(section3)
        
        
    }
    
    private func loadHeaderData() {
        DispatchQueue.main.asyncAfter(
        deadline: DispatchTime.now() + Double(Int64(2.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) {
            () -> Void in
            // set header data
            self.headerView.avatarURL = "avatar.jpg"
            self.headerView.introduction = "即使是別人看不見的地方,對其工藝也應該盡心盡力!"
            self.headerView.frame.size.height = self.headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
            self.tableView.tableHeaderView = self.headerView
        }
    }
    
    // MARK: - Private property
    private lazy var tableView:UITableView = {
        let temp = UITableView(frame: CGRect.zero, style: .grouped)
        temp.estimatedRowHeight = 50
        temp.sectionFooterHeight = 0
        return temp
    }()
    
    private lazy var headerView:ProfileHeaderView = {
        let temp = ProfileHeaderView()
        return temp
    }()
    
    fileprivate var data = [[ProfileData]]()
}


extension ProfileTableViewControllerWithHeader:UITableViewDataSource, UITableViewDelegate{
    // MARK: - Table view data source
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.data.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionData = self.data[section]
        return sectionData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewControllerCellReuseIdentifier, for: indexPath) as? ProfileTableViewCell {
            let dataItem = self.data[indexPath.section][indexPath.row]
            cell.title = dataItem.title
            cell.content = dataItem.content
            return cell
        }
        return UITableViewCell()
    }
    
    // MARK: - Table view delegate
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if section == 0 {
            return CGFloat.leastNormalMagnitude
        }
        return 8.0
    }

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM