Nice programing

UICollectionView에서 레이아웃을 동적으로 설정하면 설명 할 수없는 contentOffset 변경이 발생합니다.

nicepro 2020. 11. 8. 11:06
반응형

UICollectionView에서 레이아웃을 동적으로 설정하면 설명 할 수없는 contentOffset 변경이 발생합니다.


Apple의 문서 (그리고 WWDC 2012에서 선전 됨)에 따르면 레이아웃을 UICollectionView동적으로 설정 하고 변경 사항을 애니메이션화 할 수도 있습니다.

일반적으로 컬렉션보기를 만들 때 레이아웃 개체를 지정하지만 컬렉션보기의 레이아웃을 동적으로 변경할 수도 있습니다. 레이아웃 개체는 collectionViewLayout 속성에 저장됩니다. 이 속성을 설정하면 변경 사항에 애니메이션을 적용하지 않고 레이아웃이 즉시 업데이트됩니다. 변경 사항에 애니메이션을 적용하려면 대신 setCollectionViewLayout : animated : 메소드를 호출해야합니다.

그러나 실제로, 나는 것으로 나타났습니다 UICollectionView받는 설명 할 수없는, 심지어 유효하지 않은 변화를 만드는 contentOffset기능이 거의 사용할 수 없게 만드는 세포가 잘못 이동하는 원인. 문제를 설명하기 위해 스토리 보드에 놓인 기본 컬렉션 뷰 컨트롤러에 연결할 수있는 다음 샘플 코드를 모았습니다.

#import <UIKit/UIKit.h>

@interface MyCollectionViewController : UICollectionViewController
@end

@implementation MyCollectionViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 1;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor whiteColor];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
    [self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}

@end

컨트롤러는 기본값 UICollectionViewFlowLayout설정 viewDidLoad하고 화면에 단일 셀을 표시합니다. 셀이 선택되면 컨트롤러는 또 다른 기본값을 만들고 플래그 UICollectionViewFlowLayout와 함께 컬렉션보기에 설정합니다 animated:YES. 예상되는 동작은 셀이 이동하지 않는 것입니다. 그러나 실제 동작은 셀이 화면 밖으로 스크롤되므로 화면에서 다시 셀을 스크롤 할 수 없습니다.

콘솔 로그를 보면 contentOffset이 설명 할 수없이 변경되었음을 알 수 있습니다 (내 프로젝트에서는 (0, 0)에서 (0, 205)로). 애니메이션이 아닌 사례 ( i.e. animated:NO) 에 대한 솔루션에 대한 솔루션을 게시 했지만 애니메이션이 필요하기 때문에 애니메이션 사례에 대한 솔루션이나 해결 방법이 있는지 알고 싶습니다.

참고로 사용자 지정 레이아웃을 테스트했으며 동일한 동작을 얻었습니다.


나는 며칠 동안 머리카락을 뽑아 왔으며 도움이 될 수있는 내 상황에 대한 해결책을 찾았습니다. 제 경우에는 ipad의 사진 앱 에서처럼 축소되는 사진 레이아웃이 있습니다. 사진이 서로 겹쳐있는 앨범을 표시하며 앨범을 탭하면 사진이 확장됩니다. 그래서 내가 가진 것은 두 개의 별도 UICollectionViewLayouts이며 [self.collectionView setCollectionViewLayout:myLayout animated:YES]애니메이션 이전에 셀 점프에 대한 정확한 문제가 있었고 그것이 contentOffset. 나는 모든 것을 시도했지만 contentOffset애니메이션 중에 여전히 뛰었습니다. 위의 타일러의 솔루션은 효과가 있었지만 여전히 애니메이션을 엉망으로 만들었습니다.

그런 다음 화면에 앨범이 몇 개 있고 화면을 채울만큼 충분하지 않을 때만 발생한다는 것을 알았습니다. 내 레이아웃 -(CGSize)collectionViewContentSize이 권장대로 재정의 됩니다. 앨범이 몇 개만있는 경우 컬렉션보기 콘텐츠 크기가보기 콘텐츠 크기보다 작습니다. 이것이 컬렉션 레이아웃 사이를 전환 할 때 점프를 유발합니다.

그래서 minHeight라는 레이아웃에 속성을 설정하고 컬렉션 뷰 부모의 높이로 설정합니다. 그런 다음 돌아 오기 전에 높이를 -(CGSize)collectionViewContentSize확인하여 높이가 최소 높이보다 큰지 확인합니다.

진정한 해결책은 아니지만 지금은 잘 작동합니다. contentSize컬렉션 뷰의을 적어도 포함하는 뷰의 길이로 설정하려고합니다 .

편집 : Manicaesar는 UICollectionViewFlowLayout에서 상속하는 경우 쉬운 해결 방법을 추가했습니다.

-(CGSize)collectionViewContentSize { //Workaround
    CGSize superSize = [super collectionViewContentSize];
    CGRect frame = self.collectionView.frame;
    return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}

UICollectionViewLayouttargetContentOffsetForProposedContentOffset:레이아웃 변경 중에 적절한 콘텐츠 오프셋을 제공 할 수 있는 재정의 가능한 메서드 포함되어 있으며 이는 올바르게 애니메이션됩니다. iOS 7.0 이상에서 사용할 수 있습니다.


이 문제도 저를 물었고 전환 코드의 버그 인 것 같습니다. 내가 알 수 있듯이 전환 전 뷰 레이아웃의 중앙에 가장 가까운 셀에 초점을 맞추려고합니다. 그러나 뷰 사전 전환의 중심에 셀이없는 경우에도 전환 후 셀이 위치 할 위치를 중심으로 설정하려고합니다. alwaysBounceVertical / Horizontal을 YES로 설정하고 단일 셀로 뷰를로드 한 다음 레이아웃 전환을 수행하면 이는 매우 분명합니다.

레이아웃 업데이트를 트리거 한 후 컬렉션에 특정 셀 (이 예에서는 첫 번째 셀 표시 셀)에 초점을 맞추도록 명시 적으로 지시하여이 문제를 해결할 수있었습니다.

[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];

// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
    NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
    [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}

내 질문에 늦게 대답했습니다.

TLLayoutTransitioning의 라이브러리는 배치 전환에 비 대화 형, 배치를 할 수있는 재 태스킹 iOS7s 대화 형 천이 API에 의해이 문제에 대한 훌륭한 솔루션을 제공합니다. setCollectionViewLayout콘텐츠 오프셋 문제를 해결하고 여러 기능을 추가하여에 대한 대안을 효과적으로 제공 합니다.

  1. 애니메이션 기간
  2. 30 개 이상의 여유 곡선 (Warren Moore의 AHEasing 라이브러리 제공 )
  3. 다중 콘텐츠 오프셋 모드

사용자 정의 여유 곡선을 AHEasingFunction함수 로 정의 할 수 있습니다 . 최종 콘텐츠 오프셋은 최소, 중앙, 위쪽, 왼쪽, 아래쪽 또는 오른쪽 배치 옵션을 사용하여 하나 이상의 인덱스 경로로 지정할 수 있습니다.

무슨 뜻인지 확인하려면 예제 작업 공간에서 크기 조정 데모실행 하고 옵션을 사용해보십시오.

사용법은 다음과 같습니다. 먼저 TLTransitionLayout다음 인스턴스를 반환하도록 뷰 컨트롤러를 구성합니다 .

- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
    return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}

그런 다음, 호출하는 대신 setCollectionViewLayout, 호출 transitionToCollectionViewLayout:toLayout에 정의 UICollectionView-TLLayoutTransitioning카테고리 :

UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];

이 호출은 대화 형 전환을 시작하고 내부적 CADisplayLink으로는 지정된 기간 및 여유 함수로 전환 진행을 유도 하는 콜백을 시작합니다.

다음 단계는 최종 콘텐츠 오프셋을 지정하는 것입니다. 임의의 값을 지정할 수 있지만에 toContentOffsetForLayout정의 된 방법 UICollectionView-TLLayoutTransitioning은 하나 이상의 인덱스 경로를 기준으로 콘텐츠 오프셋을 계산하는 우아한 방법을 제공합니다. 예를 들어 특정 셀이 컬렉션 뷰의 중앙에 최대한 가깝게되도록하려면 바로 뒤에 다음 호출을 수행합니다 transitionToCollectionViewLayout.

NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement];
layout.toContentOffset = toOffset;

쉬운.

동일한 애니메이션 블록에서 새 레이아웃과 collectionView의 contentOffset을 애니메이션하십시오.

[UIView animateWithDuration:0.3 animations:^{
                                 [self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
                                 [self.collectionView setContentOffset:CGPointMake(0, -64)];
                             } completion:nil];

self.collectionView.contentOffset일정 하게 유지 됩니다.


레이아웃에서 전환 할 때 변경되지 않는 콘텐츠 오프셋을 찾고있는 경우 사용자 지정 레이아웃을 만들고 몇 가지 메서드를 재정 의하여 이전 contentOffset을 추적하고 다시 사용할 수 있습니다.

@interface CustomLayout ()

@property (nonatomic) NSValue *previousContentOffset;

@end

@implementation CustomLayout

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
    CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
    return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}

- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
    self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
    return [super prepareForTransitionFromLayout:oldLayout];
}

- (void)finalizeLayoutTransition
{
    self.previousContentOffset = nil;
    return [super finalizeLayoutTransition];
}
@end

이 작업은에서 레이아웃 전환 전에 이전 콘텐츠 오프셋을 저장하고 prepareForTransitionFromLayout에서 새 콘텐츠 오프셋을 덮어 쓴 다음에서 targetContentOffsetForProposedContentOffset지우는 것입니다 finalizeLayoutTransition. 꽤 직설적 인


경험을 더하는 데 도움이되는 경우 : 콘텐츠의 크기, 콘텐츠 삽입 여부 또는 기타 명백한 요소에 관계없이이 문제가 지속적으로 발생했습니다. 그래서 내 해결 방법은 다소 과감했습니다. 먼저 UICollectionView를 서브 클래 싱하고 부적절한 콘텐츠 오프셋 설정을 방지하기 위해 추가했습니다.

- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
    if(_declineContentOffset) return;
    [super setContentOffset:contentOffset];
}

- (void)setContentOffset:(CGPoint)contentOffset
{
    if(_declineContentOffset) return;
    [super setContentOffset:contentOffset];
}

- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
    _declineContentOffset ++;
    [super setCollectionViewLayout:layout animated:animated];
    _declineContentOffset --;
}

- (void)setContentSize:(CGSize)contentSize
{
    _declineContentOffset ++;
    [super setContentSize:contentSize];
    _declineContentOffset --;
}

나는 그것을 자랑스럽게 생각하지 않지만 유일한 실행 가능한 해결책은 .NET에 대한 호출로 인한 자체 콘텐츠 오프셋을 설정하려는 컬렉션 뷰의 시도를 완전히 거부하는 것 같습니다 setCollectionViewLayout:animated:. 경험적으로 이러한 변경은 즉각적인 호출에서 직접 발생하는 것처럼 보입니다. 분명히 인터페이스 나 문서에 의해 보장되지는 않지만 Core Animation 관점에서는 의미가 있으므로 가정에 50 % 만 불편할 것입니다.

그러나 두 번째 문제가있었습니다. UICollectionView는 새로운 컬렉션 뷰 레이아웃에서 같은 위치에있는 뷰에 약간의 점프를 추가했습니다.이를 약 240 포인트 아래로 밀고 다시 원래 위치로 애니메이션화했습니다. 이유는 확실하지 않지만 CAAnimation실제로 움직이지 않는 셀에 추가 된 s를 분리 하여 처리하도록 코드를 수정했습니다 .

- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
    // collect up the positions of all existing subviews
    NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
    for(UIView *view in [self subviews])
    {
        positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
    }

    // apply the new layout, declining to allow the content offset to change
    _declineContentOffset ++;
    [super setCollectionViewLayout:layout animated:animated];
    _declineContentOffset --;

    // run through the subviews again...
    for(UIView *view in [self subviews])
    {
        // if UIKit has inexplicably applied animations to these views to move them back to where
        // they were in the first place, remove those animations
        CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"];
        NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
        if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
        {
            NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];

            if([targetValue isEqualToValue:sourceValue])
                [[view layer] removeAnimationForKey:@"position"];
        }
    }
}

이것은 실제로 움직이는 뷰를 방해하지 않거나 잘못 움직이게하는 것으로 보이지 않습니다 (주변의 모든 것이 약 240 포인트 아래로 내려 가서 올바른 위치로 애니메이션 될 것으로 예상하는 것처럼).

그래서 이것이 나의 현재 해결책입니다.


2019 실제 솔루션

"자동차"보기에 대한 여러 레이아웃이 있다고 가정합니다.

애플은 망했어, 레이아웃 사이에 애니메이션을 적용 할 때 점프 할 것이다.

var avoidAppleFuckupCarsLayouts: CGPoint? = nil

class FixerForCarsLayouts: UICollectionViewLayout {

    override func prepareForTransition(to newLayout: UICollectionViewLayout) {
        avoidAppleFuckupCarsLayouts = collectionView?.contentOffset
    }

    override func targetContentOffset(
        forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        if avoidAppleFuckupCarsLayouts != nil {
            return avoidAppleFuckupCarsLayouts!
        }
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
}

.

Say you have various layouts for your "Cars" screen:

CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...

That's it. It works.

Apple, thanks for creating another mystery problem that should never have existed.

Footnotes

  1. Incredibly obscurely, you could have different "sets" of layouts (for Cars, Dogs, etc.), which could (conceivably) collide. For this reason, have a global and a base class (as above) for each "set".

  2. This was invented by passing user @Isaacliu, above, many years ago. Thanks.

  3. A detail, FWIW in Isaacliu's code fragment, finalizeLayoutTransition is added. It's not necessary logically.


I've probably spent about two weeks now trying to get various layout to transition between one another smoothly. I've found that override the proposed offset is working in iOS 10.2, but in version prior to that I still get the issue. The thing that makes my situation a bit worse is I need to transition into another layout as a result of a scroll, so the view is both scrolling and transitioning at the same time.

Tommy's answer was the only thing that worked for me in pre 10.2 versions. I'm doing the following thing now.

class HackedCollectionView: UICollectionView {

    var ignoreContentOffsetChanges = false

    override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
        guard ignoreContentOffsetChanges == false else { return }
        super.setContentOffset(contentOffset, animated: animated)
    }

    override var contentOffset: CGPoint {
        get {
            return super.contentOffset
        }
        set {
            guard ignoreContentOffsetChanges == false else { return }
            super.contentOffset = newValue
        }
    }

    override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) {
        guard ignoreContentOffsetChanges == false else { return }
        super.setCollectionViewLayout(layout, animated: animated)
    }

    override var contentSize: CGSize {
        get {
            return super.contentSize
        }
        set {
            guard ignoreContentOffsetChanges == false else { return }
            super.contentSize = newValue
        }
    }

}

Then when I set the layout I do this...

let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100)
UIView.animate(withDuration: animationDuration,
               delay: 0, options: animationOptions,
               animations: {
                collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in
                    // I'm also doing something in my layout, but this may be redundant now
                    layout.overriddenContentOffset = nil
                })
                collectionView.ignoreContentOffsetChanges = true
}, completion: { _ in
    collectionView.ignoreContentOffsetChanges = false
    collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false)
})

This finally worked for me (Swift 3)

self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)

참고URL : https://stackoverflow.com/questions/13780138/dynamically-setting-layout-on-uicollectionview-causes-inexplicable-contentoffset

반응형