一、案例演示
IOS中提供的UITableView功能非常強大,section提供分組,cell提供顯示,幾乎可以應付絕大部分場景。最近想模仿舊版的淘寶的商品詳情頁(最新的淘寶詳情頁商品詳情和圖文詳情是兩個頁面)寫一個Demo,後來發現單純使用UITableView來佈局是比較困難的。因為舊版的淘寶詳情頁中,最外層的View肯定是一個UITableView,但是內層的Tab中,圖文介紹、商品詳情和評價三個Tab對應的內容非常豐富,如果你把這三塊內容放在一個section中的話,將導致資料組織非常困難,並且UI的靈活度也大大降低。所以最後準備嘗試使用UITableView巢狀UITableView的方式來組織UI,最外層是一個UITableView,三個Tab其實是一個橫向ScrollView,這個ScrollView裡面包含三個UITableView。並且Tab中的內容採用動態可配置話的方式生成(下面詳解)。實現的效果如下:
二、專案詳解
2.1、大體思路
使內層的UITableView(TAB欄裡面)和外層的UITableView同時響應使用者的手勢滑動事件。當使用者從頁面頂端從下往上滑動到TAB欄的過程中,使外層的UITableView跟隨使用者手勢滑動,內層的UITableView不跟隨手勢滑動。當使用者繼續往上滑動的時候,讓外層的UITableView不跟隨手勢滑動,讓內層的UITableView跟隨手勢滑動。反之從下往上滑動也一樣。
如上圖所示,外層的section0為價格區,可以自定義。section1為sku區,也可以自定義。section2為TAB區域,該區域採用Runtime反射機制,動態配置完成。
2.2、具體實現
2.2.1、YXIgnoreHeaderTouchTableView
我們頂部的圖片其實是覆蓋在外層UITableView的tableHeaderView的下面,我們把tableHeaderView設定為透明。這樣實現是為了方便我們在滑動的時候,動態的改變圖片的寬高,實現列表頭部能夠動態拉伸的效果。但是我們對於UITableView不做處理的時候,該圖片是無法響應點選事件的,因為被tableHeaderView提前消費了。所以我們要不讓tableHeaderView不響應點選事件。我們在YXIgnoreHeaderTouchTableView的實現檔案中重寫以下方法。
1 2 3 4 5 6 |
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (self.tableHeaderView && CGRectContainsPoint(self.tableHeaderView.frame, point)) { return NO; } return [super pointInside:point withEvent:event]; } |
2.2.2、 YXIgnoreHeaderTouchAndRecognizeSimultaneousTableView
該檔案繼承於YXIgnoreHeaderTouchTableView,除此之外,主要是為了讓外層的UITableView能夠顯示外層UITableView的滑動事件。我們需要實現以下代理方法。
1 2 3 |
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; } |
2.2.3、YXTabView
該檔案是TAB區域主檔案,顯示的標題的內容都是通過以下字典動態生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if(section==2){ NSArray *tabConfigArray = @[@{ @"title":@"圖文介紹", @"view":@"PicAndTextIntroduceView", @"data":@"圖文介紹的資料", @"position":@0 },@{ @"title":@"商品詳情", @"view":@"ItemDetailView", @"data":@"商品詳情的資料", @"position":@1 },@{ @"title":@"評價(273)", @"view":@"CommentView", @"data":@"評價的資料", @"position":@2 }]; YXTabView *tabView = [[YXTabView alloc] initWithTabConfigArray:tabConfigArray]; [cell.contentView addSubview:tabView]; } |
title:TAB每個Item的標題。
view:TAB每個Item的內容。
data:TAB每個Item內容渲染需要的資料。
position:TAB的位置。從0開始。
該TAB其實是有YXTabTitleView(標題欄)和一個橫向的ScrollView(內層多個UITableView的容器)構成。內層多個UITableView通過以上配置檔案動態生成。如下如示:
1 2 3 4 5 6 7 8 |
for (int i=0; i<tabConfigArray.count; i++) { NSDictionary *info = tabConfigArray[i]; NSString *clazzName = info[@"view"]; Class clazz = NSClassFromString(clazzName); YXTabItemBaseView *itemBaseView = [[clazz alloc] init]; [itemBaseView renderUIWithInfo:tabConfigArray[i]]; [_tabContentView addSubview:itemBaseView]; } |
2.2.4、YXTabItemBaseView
該檔案是內層UITableView都應該繼承的BaseView,在該View中我們設定了內層UITableView具體在什麼時機不響應使用者滑動事件,什麼時機應該響應使用者滑動事件,什麼時間通知外層UITableView響應滑動事件等等功能。
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ if (!self.canScroll) { [scrollView setContentOffset:CGPointZero]; } CGFloat offsetY = scrollView.contentOffset.y; if (offsetY<0) { [[NSNotificationCenter defaultCenter] postNotificationName:kLeaveTopNotificationName object:nil userInfo:@{@"canScroll":@"1"}]; [scrollView setContentOffset:CGPointZero]; self.canScroll = NO; self.tableView.showsVerticalScrollIndicator = NO; } } |
2.2.5、PicAndTextIntroduceView、ItemDetailView、CommentView
這三個檔案都繼承於YXTabItemBaseView,但是在該檔案中我們只需要注意UI的渲染就可以了。響應事件的管理都在YXTabItemBaseView做好了。
就拿PicAndTextIntroduceView.m來看,基本上都是UI程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@implementation PicAndTextIntroduceView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return 30; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 50.; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *cellId = @"cellId"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId]; } cell.textLabel.text = self.info[@"data"]; return cell; } @end |
2.2.6、內外層滑動事件的響應和傳遞
外層UITableView在初始化的時候 需要監聽一個NSNotification,該通知是內層UITableView傳遞給外層的,傳遞時機為從上往下活動,當TAB欄取消置頂的時候。通知外層UITableView可以開始滾動了。
1 2 3 4 5 6 7 8 9 10 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(acceptMsg:) name:kLeaveTopNotificationName object:nil]; -(void)acceptMsg : (NSNotification *)notification{ //NSLog(@"%@",notification); NSDictionary *userInfo = notification.userInfo; NSString *canScroll = userInfo[@"canScroll"]; if ([canScroll isEqualToString:@"1"]) { _canScroll = YES; } } |
在scrollViewDidScroll方法中,需要實時監控外層UItableView的滑動時機。也要在適當時機傳送NSNotification給內層UItableView,通知內層UITableView是否可以滑動。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{ CGFloat tabOffsetY = [_tableView rectForSection:2].origin.y-kTopBarHeight; CGFloat offsetY = scrollView.contentOffset.y; _isTopIsCanNotMoveTabViewPre = _isTopIsCanNotMoveTabView; if (offsetY>=tabOffsetY) { scrollView.contentOffset = CGPointMake(0, tabOffsetY); _isTopIsCanNotMoveTabView = YES; }else{ _isTopIsCanNotMoveTabView = NO; } if (_isTopIsCanNotMoveTabView != _isTopIsCanNotMoveTabViewPre) { if (!_isTopIsCanNotMoveTabViewPre && _isTopIsCanNotMoveTabView) { //NSLog(@"滑動到頂端"); [[NSNotificationCenter defaultCenter] postNotificationName:kGoTopNotificationName object:nil userInfo:@{@"canScroll":@"1"}]; _canScroll = NO; } if(_isTopIsCanNotMoveTabViewPre && !_isTopIsCanNotMoveTabView){ //NSLog(@"離開頂端"); if (!_canScroll) { scrollView.contentOffset = CGPointMake(0, tabOffsetY); } } } } |
三、完整原始碼下載地址
github下載地址:https://github.com/yixiangboy/YX_UITableView_IN_UITableView
如果對你有用,star一下吧。
四、聯絡方式
歡迎加好友、一起交流。