如何同步 UICollectionViewCell 内的 UITableView 之间的滚动(在 UICollectionViewController 内)

Posted

技术标签:

【中文标题】如何同步 UICollectionViewCell 内的 UITableView 之间的滚动(在 UICollectionViewController 内)【英文标题】:How can I synchronise the scrolling between a UITableView inside of a UICollectionViewCell (Inside a UICollectionViewController) 【发布时间】:2018-08-03 13:56:59 【问题描述】:

目前,我正在尝试同步嵌套在 UICollectionViewCell 内的 UITableView 之间的滚动

我的视图层次结构类似于:

UICollectionViewController UICollectionView UICollectionReusableView(标题视图) UICollectionViewCell UITableView UITableViewCell

我想要实现的是一个类似于视图寻呼机的效果,您可以在页面之间滑动,当您垂直滚动时,视图的其余部分。即导航栏会优雅地响应它。

我现在正在做的是获取UITableViewcontentOffset.y 并相应地更新UICollectionView 内容偏移量。

我正在使用 NSNotificationCenter 在 tableview 滚动时通知父 ViewController。

@objc private func didScroll(_ notification: NSNotification) 
   guard let userInfo = notification.userInfo as? [String: CGPoint] else  return 
   guard let offset = userInfo["offset"] else  return 

   let y = offset.y
   collectionView?.setContentOffset(CGPoint(x: 0, y: y), animated: false)

结果如下:

主要问题是:

    您无法向左或向右滑动,因为内容偏移 x 是静态的并设置为 0

    视图会自动卷起,看起来很糟糕

    非常有问题

在我尝试之前,我的视图最终看起来像这样:

在这里你可以看到我描述的问题,视图没有响应 UITableView 滚动。

所以简而言之,我需要:

了解如何使 UICollectionView 与 UICollectionViewCell 内的 UITableView 同步滚动(并允许更改 内容偏移量.x)

【问题讨论】:

您是否尝试过在到达末尾时禁用滚动? 我想我找不到一个行之有效的解决方案。 【参考方案1】:

我没有使用嵌套 UICollectionView/UITableView 的方法,认为很难解决两个 UIScrollView 之间的滚动同步问题。这是另一种方法:保留一个 ListView(UICollectionView 或 UITableView)并添加滑动手势以实现水平滚动以与另一个显示部分列表数据的 ListView 进行分页。分页后,刷新主 ListView 以显示新的类别数据。

1。自定义 UICollectionViewLayout 以偏移单元格视图进行分页:

// CollectionViewPagingLayout.h
@interface UICollectionViewPagingLayout : UICollectionViewFlowLayout

@property (nonatomic, assign) NSInteger pagingSection;
@property (nonatomic, assign) CGPoint pagingOffset;

@end

// CollectionViewPagingLayout.m
// The new InvalidationContext contains new flag, invalidatedOffset, to indicates that it is just offset change and doesn't need full layout 
@interface UICollectionViewPagingLayoutInvalidationContext : UICollectionViewFlowLayoutInvalidationContext
@property (nonatomic, assign) BOOL invalidateOffset; // Paging Or Sticky

@end

@interface UICollectionViewPagingLayout()

    BOOL m_layoutInvalidated; // Should be initialized to NO


@end

@implementation UICollectionViewPagingLayoutInvalidationContext

@end
@implementation UICollectionViewPagingLayout
@synthesize pagingOffset = m_pagingOffset;
@synthesize pagingSection = m_pagingSection;

// Skip initialization here...

+ (Class)invalidationContextClass

    return [UICollectionViewPagingLayoutInvalidationContext class];


- (void)setPagingOffset:(CGPoint)pagingOffset

    m_pagingOffset = pagingOffset;
    [self invalidateOffset];


- (void)setPagingSection:(NSInteger)pagingSection

    m_pagingSection = pagingSection;
    [self invalidateOffset];


- (void)invalidateOffset

    UICollectionViewPagingLayoutInvalidationContext *context = (UICollectionViewPagingLayoutInvalidationContext *)[[[UICollectionViewPagingLayout invalidationContextClass] alloc] init];
    context.invalidateOffset = YES;
    [self invalidateLayoutWithContext:context];


- (void)prepareLayout

    if (m_layoutInvalidated)
    
        [super prepareLayout];
        m_layoutInvalidated = NO;
    


- (void)invalidateLayout

    m_layoutInvalidated = YES;
    [super invalidateLayout];   


- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context

    if ([context isKindOfClass:[UICollectionViewPagingLayout invalidationContextClass]])
    
        UICollectionViewPagingLayoutInvalidationContext *pagingInvalidationContext = (UICollectionViewPagingLayoutInvalidationContext *)context;
        if (!pagingInvalidationContext.invalidateOffset)
        
            // It is not caused by internal offset change, should call prepareLayout
            m_layoutInvalidated = YES;
        
    
    else
    
        // It is not caused by offset change, should call prepareLayout
        m_layoutInvalidated = YES;
    
    [super invalidateLayoutWithContext:context];


- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect

    NSArray<UICollectionViewLayoutAttributes *> *layoutAttributesArray = [super layoutAttributesForElementsInRect:rect];

    // PagingOffset
    if (m_pagingSection != NSNotFound && !CGPointEqualToPoint(m_pagingOffset, CGPointZero))
    
        for (UICollectionViewLayoutAttributes *layoutAttributes in layoutAttributesArray)
        
            if (layoutAttributes.indexPath.section >= m_pagingSection)
            
                layoutAttributes.frame = CGRectOffset(layoutAttributes.frame, m_pagingOffset.x, m_pagingOffset.y);
            
        
    
    
    return layoutAttributesArray;

2。 PageingCollectionView 类实现手势:

// PagingCollectionView.h
// Define a protocol so that subclass can provide necessary information, such as current page, page size, the views for next page,
@protocol UIPagingCollectionViewDelegate<NSObject>
@optional

- (BOOL)collectionView:(nonnull UICollectionView *)collectionView pagingShouldBeginAtLocation:(CGPoint)location withTranslation:(CGPoint)translation andVelocity:(CGPoint)velocity onSection:(out NSInteger *_Nullable)section;
- (NSInteger)pageForSection:(NSInteger)section inPagingCollectionView:(nonnull UIPagingCollectionView *)pagingCollectionView;
- (NSInteger)pageSizeForSection:(NSInteger)section inPagingCollectionView:(nonnull UIPagingCollectionView *)pagingCollectionView;
- (nullable __kindof UIView *)pagingCollectionView:(nonnull UIPagingCollectionView *)pagingCollectionView viewForPage:(NSInteger)page;
- (void)collectionView:(nonnull UIPagingCollectionView *)pagingCollectionView pagingWithOffset:(CGPoint) offset decelerating:(BOOL)decelerating;
/ Notify subclass the paging action is completed, subclass should check if page is changed with necessary actions, for example, switch page on main collectionview, remove the collections on the left/right side, etc..
- (void)collectionView:(nonnull UIPagingCollectionView *)pagingCollectionView pagingEnded toNewPage:(NSInteger)page;

@end


@interface UIPagingCollectionView : UICollectionView

@property (nonatomic, weak, nullable) id <UIPagingCollectionViewDelegate> pagingDelegate;

- (void)enablePagingWithDirection:(UICollectionViewScrollDirection)direction;

@end

// PagingCollectionView.m
@interface UIPagingCollectionView() <UIGestureRecognizerDelegate>

    UIPanGestureRecognizer          *m_swipeGuestureRecognizer;
    UICollectionViewScrollDirection m_swipeDirection;   // Should provide a public property to update for subclass

    // subclass should has a strong reference and destroy it once pagination is completed and main collection finishes to load the data of new page
    __weak UIView                   *m_leftView;
    __weak UIView                   *m_rightView;
    CGRect                          m_originalLeftFrame;
    CGRect                          m_originalRightFrame;


@end

@implementation UIPagingCollectionView
@synthesize pagingDelegate = m_pagingDelegate;

- (void)enablePagingWithDirection:(UICollectionViewScrollDirection)direction

    if (nil == m_swipeGuestureRecognizer)
    
        m_swipeGuestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
        m_swipeGuestureRecognizer.delegate = self;
        [self addGestureRecognizer:m_swipeGuestureRecognizer];
    
    m_swipeDirection = direction;


- (void)offsetPagingViews:(CGPoint)offset

    if (nil != m_leftView)
    
        m_leftView.frame = CGRectOffset(m_originalLeftFrame, offset.x, offset.y);
    
    if (nil != m_rightView)
    
        m_rightView.frame = CGRectOffset(m_originalRightFrame, offset.x, offset.y);
    


- (void)handleSwipe:(UIGestureRecognizer *)gestureRecognizer

    if (gestureRecognizer == m_swipeGuestureRecognizer)
    
        switch(m_swipeGuestureRecognizer.state)
        
            case UIGestureRecognizerStateBegan:
            
                CGPoint location =[m_swipeGuestureRecognizer locationInView:self];
                CGPoint translation =[m_swipeGuestureRecognizer translationInView:self];
                CGPoint velocity =[m_swipeGuestureRecognizer velocityInView:self];
                
                BOOL shouldBegin = NO;
                NSInteger section = NSNotFound;
                if ([self.pagingDelegate collectionView:self pagingShouldBeginAtLocation:location withTranslation:translation andVelocity:velocity onSection:&section])
                
                    shouldBegin = YES;
                

                if (shouldBegin)
                
                    NSInteger page = [m_pagingDelegate pageForSection:section inPagingCollectionView:self];
                    NSInteger pageSize = [m_pagingDelegate pageSizeForSection:section inPagingCollectionView:self];
                    
                    // m_swipingContext.leftOrRight = (translation.x < 0);
                    BOOL leftOrRight = ((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (velocity.x < 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (velocity.y < 0));
                    
                    // Check validation of newPage... 
                    
                    [self buildViewForNewPage:leftOrRight ? (page + 1) : (page - 1) withCurrentPage:page];
                    
                    CGPoint offset = (m_swipeDirection == UICollectionViewScrollDirectionHorizontal) ? CGPointMake(translation.x, 0) : CGPointMake(0, translation.y);
                    [self pagingWithOffset:offset decelerating:NO andBindingPagingViews:YES];
                    
                    // Add a flag to tell UIGestureRecognizerStateChanged and UIGestureRecognizerStateEnded that paging gesture is valid.
                
            
                break;
            case UIGestureRecognizerStateChanged:
            
                if (/*the flag of paging gesture is valid*/)
                
                    CGPoint translation = [m_swipeGuestureRecognizer translationInView:self];
                    
                    if (((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (translation.x == 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (translation.y == 0)))
                    
                        // NO Movement
                    
                    else
                    
                        BOOL leftOrRight = ((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (translation.x < 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (translation.y < 0));
                        
                        [self buildViewForNewPage:leftOrRight ? (page + 1) : (page - 1) withCurrentPage:page];
                    
                    
                    CGPoint offset = (m_swipeDirection == UICollectionViewScrollDirectionHorizontal) ? CGPointMake(translation.x, 0) : CGPointMake(0, translation.y);
                    [self pagingWithOffset:offset decelerating:NO andBindingPagingViews:YES];
                
            
                break;
            case UIGestureRecognizerStateEnded:
            
                if (nil != m_pagingContext)
                
                    CGPoint translation = [m_swipeGuestureRecognizer translationInView:self];
                    
                    CGPoint offset = (m_swipeDirection == UICollectionViewScrollDirectionHorizontal) ? CGPointMake(translation.x, 0) : CGPointMake(0, translation.y);
                    [self pagingWithOffset:offset decelerating:NO andBindingPagingViews:YES];
                    
                    if (((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (translation.x == 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (translation.y == 0)))
                    
                        // NO Movement, everything is as same as old, just notify subclass
                        [m_pagingDelegate collectionView:self pagingEnded toNewPage:page];
                        break;
                    
                    
                    BOOL leftOrRight = ((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (translation.x < 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (translation.y < 0));
                    
                    // Check (translation + sliding distance) is greater than half of size, if it is, start a sliding animation to complete the pagination and show new page
                    [m_pagingDelegate collectionView:self pagingEnded toNewPage:newPage];
                    // Otherwise, also start a sliding animation to move the current page to original position
                    [m_pagingDelegate collectionView:self pagingEnded toNewPage:page];
                
            
                break;
            default:
                break;
        
    


- (void)pagingWithOffset:(CGPoint)offset decelerating:(BOOL)decelerating andBindingPagingViews:(BOOL)bindingPagingViews
   
    if ([m_pagingDelegate conformsToProtocol:@protocol(UIPagingCollectionViewDelegate)] && [m_pagingDelegate respondsToSelector:@selector(collectionView:pagingWithOffset:decelerating:)])
    
        [self.pagingDelegate collectionView:self pagingWithOffset:offset decelerating:decelerating];
    
    
    if (bindingPagingViews)
    
        [UIView performWithoutAnimation:^
            if (nil != self->m_leftView)
            
                self->m_leftView.frame = CGRectOffset(self->m_originalLeftFrame, offset.x, offset.y);
            
            if (nil != self->m_rightView)
            
                self->m_rightView.frame = CGRectOffset(self->m_originalRightFrame, offset.x, offset.y);
            
        ];
    


- (void)buildViewForNewPage:(UIPagingContext *)newPage withCurrentPage:(NSInteger)page

    // newPage < page: draging dreiction is right
    // newPage > page: draging dreiction is left
    BOOL leftOrRight = newPage > page;
    UIView * __strong *pView = leftOrRight ? (&m_leftView) : (&m_rightView);
    if (nil != *pView)
    
        return;
    
    CGRect * pOriginalFrame = leftOrRight ? (&m_originalLeftFrame) : (&m_originalRightFrame);

    UIView *view = [self.pagingDelegate pagingCollectionView:self viewForPage:newPage];
    CGFloat xOffset = (m_swipeDirection == UICollectionViewScrollDirectionHorizontal) ? (pageContext.leftOrRight ? self.bounds.size.width : -self.bounds.size.width) : 0;
    CGFloat yOffset = (m_swipeDirection == UICollectionViewScrollDirectionHorizontal) ? 0 : (pageContext.leftOrRight ? self.bounds.size.height : -self.bounds.size.height);
    view.frame = CGRectOffset(view.frame, xOffset, yOffset);
    *pOriginalFrame = view.frame;

    *pView = view;
    
    [self addSubview:view];


- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer

    if (gestureRecognizer == m_swipeGuestureRecognizer)
    
        CGPoint location =[m_swipeGuestureRecognizer locationInView:self];
        CGPoint velocity =[m_swipeGuestureRecognizer velocityInView:self];
        CGPoint translation =[m_swipeGuestureRecognizer translationInView:self];
        
        BOOL shouldBegin = NO;
        NSInteger section = NSNotFound;
        // Check direction first
        if (((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (fabs(velocity.x) > fabs(velocity.y))) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (fabs(velocity.y) > fabs(velocity.x))))
        
            if ([self.pagingDelegate collectionView:self pagingShouldBeginAtLocation:location withTranslation:translation andVelocity:velocity onSection:&section])
            
                shouldBegin = YES;
            
        
        
        if (shouldBegin)
        
            NSInteger page = [m_pagingDelegate pageForSection:section inPagingCollectionView:self];
            NSInteger pageSize = [m_pagingDelegate pageSizeForSection:section inPagingCollectionView:self];
            
            
            BOOL leftOrRight = ((m_swipeDirection == UICollectionViewScrollDirectionHorizontal) && (velocity.x < 0)) || ((m_swipeDirection == UICollectionViewScrollDirectionVertical) && (velocity.y < 0));
            
            [self buildViewForNewPage:leftOrRight ? (page + 1) : (page - 1) withCurrentPage:page];
        
        
        return shouldBegin;
    
    
    return [super gestureRecognizerShouldBegin:gestureRecognizer];


- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(nonnull UIGestureRecognizer *)otherGestureRecognizer

    if ((gestureRecognizer == m_swipeGuestureRecognizer) &&
        (otherGestureRecognizer == self.panGestureRecognizer))
    
        return YES;
    
    
    return NO;


@end

3。实现子类

然后我们可以绕过嵌套 UIScrollView 之间的滚动问题。完成的示例代码为here

【讨论】:

以上是关于如何同步 UICollectionViewCell 内的 UITableView 之间的滚动(在 UICollectionViewController 内)的主要内容,如果未能解决你的问题,请参考以下文章

如何设置 UICollectionViewCell 的位置?

如何在 UICollectionViewCell 上设置 UILabel?

如何保存 UICollectionViewCell 的状态

如何使文本字段与 UICollectionViewCell 一样大?

如何在 UICollectionViewCell 中添加 UICollectionView?

如何创建自定义 UICollectionViewCell