靈活輕便的Table控件,適合複雜樣式的內容排版

github倉庫地址 https://github.com/gaoyangclub/GYTableViewController

前言

TableView是在項目開發的時候經常用到的組件,幾乎百分之八十以上的頁面都需要使用,一個最基本的Table需要實現DataSource的協議才能成功展示;那麼當頁面越來越多,根據不同的業務場景就需要實現不同的協議,將會出現不少相似冗餘代碼;當需求改變後,某些複雜頁面的排版和業務需求耦合在一起,後期將變得越來越難以維護;大部分情況下,Table需要和下拉刷新上拉加載控件配合使用模擬網絡請求;爲了滿足項目的可維護性,將UI排版和業務需求剝離出來,且自帶一些常用的功能,GYTableViewController系列應運而生。

技術特點

  • 無需繼承自定義類,引入頭文件UIViewController+GYTableView.h即可使用
  • 支持OC和Swift(混合)
  • Section和Cell層次更加清晰,根據傳入的Section數據結構內部已經全部實現Section和Cell相關delegate方法
  • Cell實例可獲得外部動態數據,索引位置,上下關係,選中狀態等,隨時更換樣式
  • 自帶MJRefresh框架,提供下拉刷新和上拉加載功能
  • 提供Section,Cell間距設置,提供選中行高亮、選中行自動居中,提供設置Cell動態高度設置等API
  • 框架中的元素全部繼承於原生的tableView,除部分代理方法外,其他原生方法扔然可以使用

安裝方法

  • pod安裝: pod ‘GYTableViewController’
  • 手動安裝:手動安裝需要添加兩個庫,將GYTableViewController項目文件中Framework文件下的文件導入自身項目,同時此框架基於MJRefresh,所以也需要導入MJRefresh框架文件,手動或者pod都可以,MJRefresh安裝方法請戳
  • demo項目圖標基於iconfont技術棧,請戳這裏

框架用法

請使用該框架中的元素來代替原生Table控件,對應關係如下:

UIViewController+GYTableView -> UIViewController
GYTableBaseView -> UITableView
GYTableViewCell -> UITableViewCell
GYTableViewSection 原生使用UIView展示section內容,這裏使用GYTableViewSection
SectionNode 用來設置Section樣式與GYTableViewSection實例綁定
CellNode 用來設置Cell樣式與GYTableViewCell實例綁定

使用時有Table控件的界面直接引入頭文件UIViewController+GYTableView.h即可,.h示例如下

  • Objective-C
#import "UIViewController+GYTableView.h"
@interface YourViewController : UIViewController

swift項目在Bridging_Header橋接文件中引入UIViewController+GYTableView.h,參照demo示例

.m或swift實現文件必須開啓gy_useTableView開關來使用Table控件GYTableView

  • Objective-C
- (BOOL)gy_useTableView {
    return YES;
}
  • Swift
override func gy_useTableView() -> Bool {
    return true
}

.m或swift文件中重寫headerRefresh添加元素,當自帶的下拉刷新控件下拉時調用;從而開始Table內容層次搭建,以及各種類型的Cell位置如何擺放等

- (void)headerRefresh:(GYTableBaseView *)tableView {
    //下拉刷新後開始請求後臺提供數據,請求到數據後根據解析的內容展開cell實例和位置等操作,代碼結構如下(僞代碼)
    request {
        tableView {
            sectionNode {
                cellNode,
                cellNode,
                ...
            }
            sectionNode {
                cellNode,
                ...
            }
            ...
        }
        [tableView headerEndRefresh:YES];//界面搭建完畢後停止刷新
    }
}

Cell控件直接繼承GYTableViewCell,.h示例如下

  • Objective-C
#import "GYTableViewCell.h"
@interface YourViewCell : GYTableViewCell
  • Swift
class YourViewCell: GYTableViewCell

.m文件中重寫showSubviews方法進行佈局,利用getCellData獲取Table控件中傳入的數據

  • Objective-C
- (void)showSubviews {
    id yourData = [self getCellData];//先獲取外部傳入的數據
    //開始界面佈局...
}
  • Swift
override func showSubviews() {
    let yourData = self.getData()
    //開始界面佈局...
}

添加Cell

Table控制器內部實現

  • Objective-C
- (void)headerRefresh:(GYTableBaseView *)tableView {
    [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
        //添加一個高度爲230,類型爲BannerViewCell,展示banner圖片序列的Cell
        [sNode addCellNode:[CellNode initWithParams:230 cellClass:RefreshBannerViewCell.class cellData:Mock.bannerUrlGroup]];
    }]];
    [tableView headerEndRefresh:YES];//不要忘了結束刷新,否則刷新動畫會停留原地
}
  • Swift
override func headerRefresh(_ tableView: GYTableBaseView!) {
    tableView.add(SectionNode.initWithParams({ sNode in
        //添加一個高度爲230,類型爲BannerViewCell,展示banner圖片序列的Cell
        sNode?.add(CellNode.initWithParams(230, cellClass: RefreshBannerViewCell.self, cellData: Mock.bannerUrlGroup))
    }))
    tableView.headerEndRefresh(true)
}

案例1-1

批量添加Cell

Table控制器內部實現(暴力添加,Swift略過…)

- (void)headerRefresh:(GYTableBaseView *)tableView {
    [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
        //添加一個高度爲230,類型爲BannerViewCell,展示banner圖片序列的Cell
        [sNode addCellNode:[CellNode initWithParams:230 cellClass:RefreshBannerViewCell.class cellData:self.bannerUrlGroup]];
    }]];
    //注意banner和基金產品列表屬於不同區域,應存放到各自section中添加,管理section視圖會比較方便
    [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
        //添加多個高度爲80,類型爲RefreshFundViewCell,展示基金信息的Cell
        [sNode addCellNode:[CellNode initWithParams:80 cellClass:RefreshFundViewCell.class cellData:self.fundModels[0]]];
        [sNode addCellNode:[CellNode initWithParams:80 cellClass:RefreshFundViewCell.class cellData:self.fundModels[1]]];
        [sNode addCellNode:[CellNode initWithParams:80 cellClass:RefreshFundViewCell.class cellData:self.fundModels[2]]];
        //...
    }]];
    tableView.headerEndRefresh(true);//不要忘了結束刷新,否則刷新動畫會停留原地
}

相同類型的Cell添加可以修改成通過原數組批量添加

  • Objective-C
[tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
    //添加多個高度爲80,類型爲RefreshFundViewCell,展示基金信息的Cell
    [sNode addCellNodeByList:[CellNode dividingCellNodeBySourceArray:80 cellClass:RefreshFundViewCell.class sourceArray:Mock.fundModels]];
}]];
  • Swift
tableView.add(SectionNode.initWithParams({ sNode in
    sNode?.addCellNode(byList: CellNode.dividingCellNode(bySourceArray: 80, cellClass: RefreshFundViewCell.self, sourceArray: Mock.fundNewModels))
}))

案例1-2

添加Section

如果一節內容需要添加section頁眉視圖,只要在sectionNode實例設置sectionHeaderClass即可,同理section頁腳設置sectionFooterClass

  • Objective-C
- (void)headerRefresh:(GYTableBaseView *)tableView {
    [tableView addSectionNode:[SectionNode initWithParams:36 sectionHeaderClass:RefreshFundViewSection.class sectionHeaderData:@"精品專區" nextBlock:^(SectionNode *sNode) {
        //添加section內的cell...
    }]];
    [tableView headerEndRefresh:YES];
}
  • Swift
override func headerRefresh(_ tableView: GYTableBaseView!) {
    tableView.add(SectionNode.initWithParams(36, sectionHeaderClass: RefreshFundViewSection.self, sectionHeaderData: "精品專區", nextBlock: { sNode in
        //添加section內的cell...
    }))
}

分類結構如下

靜態圖

案例1-3

isUnique唯一性

默認所有相同Class的Cell實例都是相互複用,每次下拉刷新或者table設置reloadData,被複用的Cell實例都會重新觸發刷新調用showSubviews,從而根據傳遞的data展開;然而,一些特殊的Cell不需要複用或只實例化一次,比如標籤按鈕區域的Cell或者banner區域的Cell,每次下拉都是隻用這個實例,可以設置爲isUnique作爲唯一Cell實例優化提高性能

  • Objective-C
- (void)headerRefresh:(GYTableBaseView *)tableView endRefreshHandler:(HeaderRefreshHandler)endRefreshHandler {
    [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
       //添加一個高度爲230,類型爲BannerViewCell,展示banner圖片序列的Cell
        [sNode addCellNode:[CellNode initWithParams:230 cellClass:RefreshBannerViewCell.class cellData:self.bannerUrlGroup isUnique:YES]];
        //添加一個高度爲90,類型爲RefreshHotViewCell,展示banner圖片序列的Cell
        [sNode addCellNode:[CellNode initWithParams:90 cellClass:RefreshHotViewCell.class cellData:self.hotModels isUnique:YES]];
    }]];
    [tableView headerEndRefresh:YES];
}
  • Swift
override func headerRefresh(_ tableView: GYTableBaseView!) {
    tableView.add(SectionNode.initWithParams({ sNode in
        //添加一個高度爲230,類型爲BannerViewCell,展示banner圖片序列的Cell
        sNode?.add(CellNode.initWithParams(230, cellClass: RefreshBannerViewCell.self, cellData: Mock.bannerUrlGroup, isUnique:true))
        //添加一個高度爲90,類型爲RefreshHotViewCell,展示標籤按鈕區域的Cell
        sNode?.add(CellNode.initWithParams(90, cellClass: RefreshHotViewCell.self, cellData:Mock.hotModels , isUnique:true))
    }))
    tableView.headerEndRefresh(true)
}

上拉加載更多

Table控制器內部設置顯示上拉加載控制器

  • Objective-C
- (BOOL)gy_useLoadMoreFooter {
    return YES;
}
  • Swift
override func gy_useLoadMoreFooter() -> Bool {
    return true
}

Table控制器內部重寫footerLoadMore

  • Objective-C
//lastSectionNode:上一節sectionNode數據,即當前Table頁最後一節
- (void)footerLoadMore:(GYTableBaseView *)tableView lastSectionNode:(Section *)lastSectionNode {
    [lastSectionNode addCellNodeByList:[CellNode dividingCellNodeBySourceArray:80 cellClass:RefreshFundViewCell.class sourceArray:self.fundNewModels]];//將新增的CellNode實例繼續添加到上一節SectionNode實例中
    [tableView footerEndLoadMore:YES];//不要忘了結束上拉加載刷新
}
  • Swift
override func footerLoadMore(_ tableView: GYTableBaseView!, last lastSectionNode: SectionNode!) {
    lastSectionNode.addCellNode(byList: CellNode.dividingCellNode(bySourceArray: 80, cellClass: RefreshFundViewCell.self, sourceArray: Mock.fundNewModels))//將新增的CellNode實例繼續添加到上一節SectionNode實例中
    tableView.footerEndLoadMore(true)//不要忘了結束上拉加載刷新
}

根據需求添加到Table頁最後一節,或者添加到新的一節數據中,並設置添加上限,業務相關代碼swift示例略…

if ([tableView getTotalCellNodeCount] > 30) {//總共超出30條數據不添加數據
    [tableView footerEndLoadMore:NO];//直接結束上拉加載刷新,並顯示"已經全部加載完畢"
    return;
}
//根據業務需求的不同,可以繼續添加到上一節sectionNode,也可以添加到新的一節sectionNode中
if ([lastSectionNode getCellNodeCount] < 15) {//上一節少於15條繼續添加到上一節sectionNode
    [lastSectionNode addCellNodeByList:[CellNode dividingCellNodeBySourceArray:80 cellClass:RefreshFundViewCell.class sourceArray:self.fundNewModels]];
} else {//上一節超了 添加到新的一節sectionNode
    [tableView addSectionNode:[SectionNode initWithParams:36 sectionHeaderClass:RefreshFundViewSection.class sectionHeaderData:@"推薦專區" nextBlock:^(SectionNode *sNode) {
        [sNode addCellNodeByList:[CellNode dividingCellNodeBySourceArray:80 cellClass:RefreshFundViewCell.class sourceArray:self.fundNewModels]];
    }]];
}
[tableView footerEndLoadMore:YES];//不要忘了結束上拉加載刷新

案例1-4

更改UITableView的frame

Table控制器內部重寫getTableViewFrame

如存在和容器底部對齊的元素,請在此方法對齊底部位置(默認佔滿controller邊界);autoLayerout無需重寫此方法,自行設置tableView和其他元素佈局關係

  • Objective-C
- (CGRect)gy_getTableViewFrame {
    self.noticeBack.frame = CGRectMake(0, 0, self.view.width, 30);
    self.submitButton.maxY = self.view.height;//底部按鈕對齊容器底部
    //返回設置好的tableView位置frame 高度=總高度-公告區高-底部按鈕高
    return CGRectMake(0, self.noticeBack.height, self.view.width, self.view.height - self.noticeBack.height - self.submitButton.height);
}
  • Swift
override func gy_getTableViewFrame() -> CGRect {
    //...業務代碼同上
    //返回設置好的tableView位置frame 高度=總高度-公告區高-底部按鈕高
    return CGRect.init(x: 0, y: self.noticeBack.height, width: self.view.width, height:  self.view.height - self.noticeBack.height - self.submitButton.height);
}

案例2-1

自定義下拉刷新控件

Table控制器內部重寫gy_getRefreshHeader

  • Objective-C
- (MJRefreshHeader *)gy_getRefreshHeader {
    return [[DiyRotateRefreshHeader alloc] init];
}
  • Swift
override func gy_getRefreshHeader() -> MJRefreshHeader! {
    return DiyRotateRefreshHeader()
}

案例2-2

偵聽選中的Cell

Table控制器內部實現代理 (tableView:didSelectRowAtIndexPath:已廢棄)

  • Objective-C
- (void)didSelectRow:(GYTableBaseView *)tableView indexPath:(NSIndexPath *)indexPath {
    CellNode* cNode = [tableView getCellNodeByIndexPath:indexPath];//獲取到綁定的CellNode
    YourClass* yourData = cNode.cellData;//獲得cell的原始數據
    //根據數據添加業務邏輯...
}
  • Swift
override func didSelectRow(_ tableView: GYTableBaseView!, indexPath: IndexPath!) {
    let cNode:CellNode? = tableView.getCellNode(by: indexPath)
    let yourData:YourClass? = cNode?.cellData as? YourClass
    //根據數據添加業務邏輯...
}

設置cell點擊效果,cell實例內部重寫showSelectionStyle

  • Objective-C
- (BOOL)showSelectionStyle {
    return YES;
}
  • Swift
override func showSelectionStyle() -> Bool {
    return true
}

案例2-3

設置Cell或Section元素間距

Table控制器內部設置tableView屬性cellGap或sectionGap

  • Objective-C
- (void)viewDidLoad {
    self.tableView.sectionGap = 6;//設置每一節區域之間間距
    self.tableView.cellGap = 3;//設置每個Cell之間間距(包含每一節區域)
}
  • Swift
override func viewDidLoad() {
    self.tableView.sectionGap = 6//設置每一節區域之間間距
    self.tableView.cellGap = 3//設置每個Cell之間間距(包含每一節區域)
}

案例3-1

設置選中某個位置的Cell

當刷新完成後設置,Table控制器內部設置tableView屬性selectedIndexPath

  • Objective-C
- (void)headerRefresh:(GYTableBaseView *)tableView {
     [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
        //..添加cell數據
     }]];
     tableView.selectedIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];//設置選中某個indexPath
     [tableView headerEndRefresh:YES];
}
  • Swift
override func headerRefresh(_ tableView: GYTableBaseView!) {
    tableView.add(SectionNode.initWithParams({ sNode in
        //..添加cell數據
    }))
    tableView.selectedIndexPath = IndexPath.init(item: 0, section: 0)
    tableView.headerEndRefresh(true)
}

Cell實例設置選中效果,重寫setSelected方法,選中樣式請根據需求自行添加

  • Objective-C
- (void)setSelected:(BOOL)selected {
    [super setSelected:selected];
    [self checkCellRelate];//自定義選中樣式方法,非框架內部方法,實現如下
}
  • Swift
override var isSelected:Bool {
    set {
        super.isSelected = newValue
        self.checkCellRelate()//自定義選中樣式方法,非框架內部方法,業務實現略過...
    }
    get{
        return super.isSelected
    }
}

Cell實例位置關係isFirst,isLast,位於第一個或最後一個和中間段的Cell樣式不同

- (void)checkCellRelate {
    if (self.isFirst) {
        [self drawFirstStyle:nodeColor];
    }else if(self.isLast){
        [self drawLastStyle:nodeColor];
    }else{
        [self drawNormalStyle:nodeColor];
    }
}

案例4-1

設置交互點擊某個位置Cell並高亮

  • Objective-C
- (void)viewDidLoad {
    self.tableView.clickCellHighlight = YES;
}
  • Swift
override func viewDidLoad() {
    self.tableView.clickCellHighlight = true
}

案例4-2

設置點擊Cell自動居中

  • Objective-C
- (void)viewDidLoad {
    self.tableView.clickCellMoveToCenter = YES;
}
  • Swift
override func viewDidLoad() {
    self.tableView.clickCellMoveToCenter = true
}

案例4-3

Cell自動調整高度

Table控制器內部設置CellNode傳入高度CELL_AUTO_HEIGHT

  • Objective-C
- (void)headerRefresh:(GYTableBaseView *)tableView {
    [tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
        [sNode addCellNodeByList:[CellNode dividingCellNodeBySourceArray:CELL_AUTO_HEIGHT cellClass:AutoHeightWeiboCell.class sourceArray:Mock.weiboModels]];
    }]];
    [tableView headerEndRefresh:YES];
}
  • Swift
override func headerRefresh(_ tableView: GYTableBaseView!) {
    tableView.add(SectionNode.initWithParams({ sNode in
        sNode?.addCellNode(byList: CellNode.dividingCellNode(bySourceArray: CELL_AUTO_HEIGHT, cellClass: AutoHeightWeiboCell.self, sourceArray: Mock.weiboModels))
    tableView.headerEndRefresh(true)
}

Cell實例重寫getCellHeight方法獲取動態高度,獲取高度內容會被緩存不會二次計算

  • Objective-C
- (CGFloat)getCellHeight:(CGFloat)cellWidth {
    WeiboModel *weiboModel = [self getCellData];//獲取Model
    NSString *content = weiboModel.content;//獲取動態內容字符串
    CGRect contentSize = [content boundingRectWithSize:CGSizeMake(cellWidth - LEFT_PADDING - RIGHT_PADDING, FLT_MAX)
    options:NSStringDrawingUsesLineFragmentOrigin
    attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:TVStyle.sizeTextSecondary]}
    context:nil];//計算給定範圍內最佳尺寸
    return TOPIC_AREA_HEIGHT + contentSize.size.height + IMAGE_AREA_HEIGHT + BOTTOM_PADDING * 2;//返回計算後的最終高度
}
  • Swift
override func getHeight(_ cellWidth: CGFloat) -> CGFloat {
    let weiboModel:WeiboModel? = getData() as? WeiboModel//獲取Model
    let content:String  = weiboModel!.content //獲取動態內容字符串
    let contentSize:CGRect = content.boundingRect(with: CGSize.init(width: cellWidth - AutoHeightWeiboCell.LEFT_PADDING - AutoHeightWeiboCell.RIGHT_PADDING, height: CGFloat(Float.greatestFiniteMagnitude)), options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font:UIFont.systemFont(ofSize: TVStyle.sizeTextSecondary)], context: nil)
    //計算給定範圍內最佳尺寸
    return AutoHeightWeiboCell.TOPIC_AREA_HEIGHT + contentSize.size.height + AutoHeightWeiboCell.IMAGE_AREA_HEIGHT + AutoHeightWeiboCell.BOTTOM_PADDING * 2;//返回計算後的最終高度
}

案例5-1

自定義創建TableView

無上拉加載和下拉刷新控件的乾淨TableView實例

  • Objective-C
self.tableView = [GYTableBaseView table:self];//創建並設置delegate
[self.tableView addSectionNode:[SectionNode initWithParams:^(SectionNode *sNode) {
    //添加元素...
}]];
[self.tableView gy_reloadData];//不要忘了刷新Table
  • Swift
self.tableView = GYTableBaseView.table(self);//創建並設置delegate
self.tableView?.add(SectionNode.initWithParams({ sNode in
    //添加元素...
}))
self.tableView?.gy_reloadData();//不要忘了刷新Table

案例6-1

ChangeLog

1.1.0 當前版本,框架結構整體修改,兼容swift混編,底層delegate優化爲動態綁定

歷史版本

1.0.0 初版
1.0.1 iOS11.0 Table自動上移bug修復
1.0.2 自定義上拉加載控件支持
1.0.3 增加autolayout支持,修改TableBaseView參數傳入方式
1.0.6 添加UIViewController+GYTableView分類
1.0.7 delegate添加prepareCell用來自定義對cell進行操作
1.0.8 部分bug修復
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章