iOS ceiling effect

In the project, when the list scrolls up, sometimes a certain control needs to be placed on top, which is our common ceiling effect.

1. UITableView ceiling effect

UITableViewIt has its own ceiling effect. We set the control that needs to be on the top to SectionHeaderView, so that when scrolling, the control will automatically be on the top.

- (UITableView *)tableView {
    
    
    if (!_tableView) {
    
    
        _tableView = [[UKNestedTableView alloc] init];
        
        _tableView.bounces = NO;
        _tableView.showsVerticalScrollIndicator = NO;

        _tableView.delegate = self;
        _tableView.dataSource = self;
        
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"CellId"];
    }
    return _tableView;
}

#pragma mark - UITableViewDataSource -
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    
    
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    
    if (section == 0) {
    
    
        return 1;
    }
    return 20;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    if (indexPath.section == 0) {
    
    
        return 150;
    }
    return 60;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    
    
    if (section == 1) {
    
    
        UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, 50)];
        headerView.backgroundColor = [UIColor blueColor];
        return headerView;
    }
    return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    
    
    if (section == 1) {
    
    
        return 50;
    }
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellId" forIndexPath:indexPath];
    
    if (indexPath.section == 0) {
    
    
        cell.backgroundColor = [UIColor yellowColor];
        cell.textLabel.text = @"section 0";
    } else {
    
    
        if (indexPath.row % 2 == 0) {
    
    
            cell.backgroundColor = [UIColor grayColor];
        } else {
    
    
            cell.backgroundColor = [UIColor whiteColor];
        }
        cell.textLabel.text = [NSString stringWithFormat:@"item - %ld", indexPath.row];
    }
    
    return cell;
}

customizeUKNestedTableView

@implementation UKNestedTableView

- (instancetype)init {
    
    
    self = [super initWithFrame:CGRectZero style:UITableViewStylePlain];

    if (self) {
    
    
        self.backgroundColor = [UIColor whiteColor];
        self.separatorColor = [UIColor clearColor];
        
        self.separatorStyle = UITableViewCellSeparatorStyleNone;
                
        if (@available(iOS 11.0, *)) {
    
    
            self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        }

        self.estimatedRowHeight = 0.000;
        self.estimatedSectionHeaderHeight = 0.000;
        self.estimatedSectionFooterHeight = 0.000;
        
        if (@available(iOS 13.0,*)) {
    
    
            self.automaticallyAdjustsScrollIndicatorInsets = NO;
        }

        if (@available(iOS 15.0,*)) {
    
     // 去除表格头留白
            self.sectionHeaderTopPadding = YES;
         }
    }
    return self;
}

@end

The effect is as follows

insert image description here

2. Ceiling effect with TabView

UITableViewThe ceiling effect can meet some of the requirements, but in practical applications, it is often some tab pages that need to be placed on the top, corresponding to multiple lists.

We use UKTabView as the top control and correspond to multiple contents.

- (UKTabView *)tabView {
    
    
    if (!_tabView) {
    
    
        _tabView = [[UKTabView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, 50)];
        [_tabView setIndicatorWidth:80 height:2 radius:1 color:[UIColor blueColor]];
        
        UKCustomTabItemView *tabItemView1 = [[UKCustomTabItemView alloc] init];
        [tabItemView1 setText:@"选项1"];
        [_tabView addItemView:tabItemView1];
        
        UKCustomTabItemView *tabItemView2 = [[UKCustomTabItemView alloc] init];
        [tabItemView2 setText:@"选项2"];
        [_tabView addItemView:tabItemView2];
        
        _tabView.delegate = self;
        
        [_tabView setSelection:0];
    }
    return _tabView;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    
    
    if (section == 1) {
    
    
        return self.tabView;
    }
    return nil;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellId" forIndexPath:indexPath];
    if (indexPath.section == 0) {
    
    
        cell.backgroundColor = [UIColor yellowColor];
        cell.textLabel.text = @"section 0";
    } else {
    
    
        if (indexPath.row % 2 == 0) {
    
    
            if (self.selection == 0) {
    
    
                cell.backgroundColor = [UIColor grayColor];
            } else {
    
    
                cell.backgroundColor = [UIColor darkGrayColor];
            }
        } else {
    
    
            cell.backgroundColor = [UIColor whiteColor];
        }
        cell.textLabel.text = [NSString stringWithFormat:@"item %ld - %ld", self.selection, indexPath.row];
    }
    
    return cell;
}

#pragma mark - UKTabViewDelegate -
- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position {
    
    
    self.selection = position;
    [self.tableView reloadData];
}

The effect is as follows
insert image description here

The above method simply implements the functions of tab topping and tab switching, but since we can only share one list, both tab pages will scroll.

To do this, we need to optimize the scrolling offset, first record the offset at the end of the scrolling, and then set the original offset when switching tabs.

@property(nonatomic, assign) NSInteger selection;
@property(nonatomic, assign) CGFloat tab1Offset;
@property(nonatomic, assign) CGFloat tab2Offset;

// 拖动结束
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    
    
    NSLog(@"scrollViewDidEndDragging");

    [self recordOffset:scrollView];
}

// 滚动结束
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    
    NSLog(@"scrollViewDidEndDecelerating");

    [self recordOffset:scrollView];
}

- (void)recordOffset:(UIScrollView *)scrollView {
    
    
    if (self.selection == 0) {
    
    
        self.tab1Offset = scrollView.contentOffset.y;
        NSLog(@"tab1Offset = %.2f", self.tab1Offset);
    } else if (self.selection == 1) {
    
    
        self.tab2Offset = scrollView.contentOffset.y;
        NSLog(@"tab2Offset = %.2f", self.tab2Offset);
    }
}

When switching tabs, set the actual offset

- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position {
    
    
    self.selection = position;
    [self.tableView reloadData];

    // 有时设置tableView.contentOffset无效,需要提前刷新
    [self.tableView layoutIfNeeded];
    if (position == 0) {
    
    
        self.tableView.contentOffset = CGPointMake(0, self.tab1Offset);
    } else if (position == 1) {
    
    
        self.tableView.contentOffset = CGPointMake(0, self.tab2Offset);
    }
}

The effect is as follows
insert image description here

Although we recorded the original offset, from the actual effect, TabViewit will be at the same position when switching, and the flickering is serious. For this, we need to try to maintain TabViewthe position.

- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position {
    
    
    self.selection = position;
    [self.tableView reloadData];

    [self.tableView layoutIfNeeded];
    if (position == 0) {
    
    
        self.tab1Offset = [self getDestOffset:self.tab1Offset originOffset:self.tab2Offset];
        self.tableView.contentOffset = CGPointMake(0, self.tab1Offset);
    } else if (position == 1) {
    
    
        self.tab2Offset = [self getDestOffset:self.tab2Offset originOffset:self.tab1Offset];
        self.tableView.contentOffset = CGPointMake(0, self.tab2Offset);
    }
}

// 如果TabView已经置顶,切换时保持置顶。
// 1、如果切换后的内容已经置顶,保持原有效果
// 2、如果切换后的内容没有置顶,修改切换后的内容为置顶
// 如果TabView没有制度,切换后保持一致
- (CGFloat)getDestOffset:(CGFloat)destOffset originOffset:(CGFloat)originOffset {
    
    
    if (originOffset >= 150) {
    
    
        if (destOffset >= 150) {
    
    
            return destOffset;
        } else {
    
    
            return 150;
        }
    } else {
    
    
        return originOffset;
    }
}

The effect is as follows
insert image description here

Although the current solution has solved most of the requirements, there are still some flaws left.

  1. Content can only be UIScrollViewdisplayed with
  2. In order to maintain UKTableViewthe same position, the offset position of the content is not fully guaranteed.
  3. If a content is short, there will still be an offset problem. Although we can improve this problem by filling in blank content, it adds a lot of work.
  4. There is no smooth effect when switching content.

3. UITableView+UICollectionView nesting

In order to perfect our ceiling effect as much as possible, we try to use UITableView+UICollectionViewthe combination to achieve two effects of ceiling and left and right sliding.

we customizeUKNestedScrollView

@interface UKNestedScrollView()

@property(nonatomic, strong) NSMutableArray <UITableView *> *contentViewArray;
@property(nonatomic, assign) BOOL dragging;

@end

@implementation UKNestedScrollView

- (instancetype)initWithFrame:(CGRect)frame {
    
    
    self = [super initWithFrame:frame];
    if (self) {
    
    
        [self setupInitialUI];
    }
    return self;
}

// 设置表头
- (void)setHeaderView:(UIView *)headerView {
    
    
    self.tableView.tableHeaderView = headerView;
    self.headerHeight = headerView.frame.size.height;
}

// 添加标签页和内容
- (void)addTabView:(UKTabItemView *)itemView contentView:(UITableView *)contentView {
    
    
    [self.tabView addItemView:itemView];
    
    [self.contentViewArray addObject:contentView];
    
    [self.collectionView reloadData];
}


- (void)setupInitialUI {
    
    
    // UKNestedScrollView包含一个UITableView
    [self addSubview:self.tableView];
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
    
    
        make.left.right.top.bottom.equalTo(self);
    }];
}

- (UITableView *)tableView {
    
    
    if (!_tableView) {
    
    
        _tableView = [[UKNestedTableView alloc] init];
        
        _tableView.bounces = NO;
        _tableView.showsVerticalScrollIndicator = NO;

        _tableView.delegate = self;
        _tableView.dataSource = self;
        
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"CellId"];
    }
    return _tableView;
}

- (UITableView *)tableView {
    
    
    if (!_tableView) {
    
    
        _tableView = [[UKNestedTableView alloc] init];
        
        _tableView.bounces = NO;
        _tableView.showsVerticalScrollIndicator = NO;

        _tableView.delegate = self;
        _tableView.dataSource = self;
        
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"CellId"];
    }
    return _tableView;
}

// SectionHeaderView包含UKTabView和UICollectionView
- (UIView *)sectionHeaderView {
    
    
    if (!_sectionHeaderView) {
    
    
        _sectionHeaderView = [[UIView alloc] initWithFrame:self.frame];
        
        [_sectionHeaderView addSubview:self.tabView];
        
        [_sectionHeaderView addSubview:self.collectionView];
    }
    return _sectionHeaderView;
}

- (UKTabView *)tabView {
    
    
    if (!_tabView) {
    
    
        _tabView = [[UKTabView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, 50)];
        [_tabView setIndicatorWidth:80 height:2 radius:1 color:[UIColor blueColor]];

        _tabView.delegate = self;
    }
    return _tabView;
}

- (UICollectionView *)collectionView {
    
    
    if (!_collectionView) {
    
    
        UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
        layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        layout.itemSize = CGSizeMake(self.frame.size.width, self.frame.size.height - 50);
        layout.minimumLineSpacing = CGFLOAT_MIN;
        layout.minimumInteritemSpacing = CGFLOAT_MIN;
        
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 50, self.frame.size.width, self.frame.size.height - 50) collectionViewLayout:layout];
        _collectionView.pagingEnabled = YES;
        _collectionView.bounces = NO;
        _collectionView.showsHorizontalScrollIndicator = NO;

        _collectionView.dataSource = self;
        _collectionView.delegate = self;
        
        [_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CellId"];
    }
    return _collectionView;
}

#pragma mark - UITableViewDataSource -
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    
    
    return self.frame.size.height;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    
    
    return self.sectionHeaderView;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    return [[UITableViewCell alloc] init];
}

#pragma mark - UICollectionViewDataSource -
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    
    
    return self.contentViewArray.count;
}

- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CellId" forIndexPath:indexPath];
    
    UITableView *contentView = self.contentViewArray[indexPath.row];
    [contentView removeFromSuperview];
    
    [cell.contentView addSubview:contentView];
    [contentView mas_makeConstraints:^(MASConstraintMaker *make) {
    
    
        make.left.right.top.bottom.equalTo(cell.contentView);
    }];
    
    return cell;
}


#pragma mark - UIScrollViewDelegate -
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    
    
    if (scrollView == self.collectionView) {
    
    
        self.dragging = YES;
    }
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
    
    if (scrollView == self.collectionView) {
    
    
        if (self.dragging) {
    
    
            CGFloat width = scrollView.contentOffset.x;
            NSInteger page = width/self.frame.size.width + 0.5;
            
            [self.tabView setSelection:page offsetRatio:(width/self.frame.size.width - page)];
        }
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    
    
    if (scrollView == self.collectionView) {
    
    
        CGFloat width = scrollView.contentOffset.x;
        NSInteger page = width/self.frame.size.width + 0.5;

        [self.tabView setSelection:page];
        self.dragging = NO;
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    
    
    if (scrollView == self.collectionView && !decelerate) {
    
    
        CGFloat width = scrollView.contentOffset.x;
        NSInteger page = width/self.frame.size.width + 0.5;

        [self.tabView setSelection:page];
        self.dragging = NO;
    }
}
 
#pragma mark - UKTabViewDelegate -
- (void)onTabViewSelected:(UKTabView *)tabView position:(NSInteger)position {
    
    
    [self collectionViewScrollToPosition:position];
}

In order to allow UICollectionViewgestures inside to be UITableViewreceived, it is necessary UKNestedTableViewto add

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    
    
    return YES;
}

show as below
insert image description here

We can see that when the list slides, both lists are sliding, and the content inside slides faster. This is mainly due to the exception that both lists are sliding, so the inner list is actually the sum of the two sliding distances, so when we need to slide the outer list, the inner list is prohibited from sliding.

if (scrollView == self.tableView) {
    
    
    self.offset = self.tableView.contentOffset.y;
    // changed表示外面列表在滑动
    self.changed = YES;
} else {
    
    
    NSInteger position = 0;
    for (UIScrollView *contentView in self.contentViewArray) {
    
    
        if (contentView == scrollView) {
    
    
            // 如果外面列表滑动,禁止里面列表滑动事件
            if (self.changed) {
    
    
                scrollView.contentOffset = CGPointMake(0, [self.offsetArray[position] floatValue]);
                self.changed = NO;
            } else {
    
    
                // 记录当前页面偏移量,方便后面禁止事件
                self.offsetArray[position] = [NSNumber numberWithFloat:scrollView.contentOffset.y];
            }           
            break;
        }
        position++;
    }
}

The effect is as follows
insert image description here

The current effect has basically met our needs. It has a ceiling effect, can slide left and right, can record the offset of the list, and the content slides more smoothly.

Finally, we tried to pull down the control content first, maybe it will be useful later

if (scrollView == self.tableView) {
    
    
    self.originOffset = self.offset;
    self.offset = self.tableView.contentOffset.y;
    self.changed = YES;
} else {
    
    
    NSInteger position = 0;
    for (UIScrollView *contentView in self.contentViewArray) {
    
    
        if (contentView == scrollView) {
    
                    
            CGFloat scrollViewOffset = scrollView.contentOffset.y - [self.offsetArray[position] floatValue];

            if (scrollViewOffset > 0) {
    
    
                if (self.changed) {
    
    
                    scrollView.contentOffset = CGPointMake(0, [self.offsetArray[position] floatValue]);
                    self.changed = NO;
                } else {
    
    
                    self.offsetArray[position] = [NSNumber numberWithFloat:scrollView.contentOffset.y];
                }
            } else if (scrollViewOffset < 0) {
    
    
                if (self.changed) {
    
    
                    self.offset = self.originOffset;

                    self.tableView.delegate = nil;
                    self.tableView.contentOffset = CGPointMake(0, self.offset);
                    self.tableView.delegate = self;

                    self.changed = NO;
                }
                self.offsetArray[position] = [NSNumber numberWithFloat:scrollView.contentOffset.y];
            }
            break;
        }
        position++;
    }
}

Guess you like

Origin blog.csdn.net/chennai1101/article/details/130131507