60

I've created a UICollectionView, so that I can arrange views into neat columns. I'd like there to be a single column on devices > 500 pixels wide.

In order to achieve this, I created this function:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
    let size = collectionView.frame.width
    if (size > 500) {
        return CGSize(width: (size/2) - 8, height: (size/2) - 8)
    }
    return CGSize(width: size, height: size)
}

This works as expected on first load, however when I rotate the device, the calculation doesn't always happen again, and the views don't always redraw as expected. Here's my code for when the device is rotated:

override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
    collectionView.collectionViewLayout.invalidateLayout()
    self.view.setNeedsDisplay()
}

I'm assuming I've forgotten to redraw something, but I'm not sure what. Any ideas are very gratefully recieved!

Ben
  • 3,981
  • 4
  • 28
  • 49
  • This might help someone https://stackoverflow.com/questions/13490065/keeping-the-contentoffset-in-a-uicollectionview-while-rotating-interface-orienta/43322706#43322706 – Gurjinder Singh Oct 29 '18 at 17:42

9 Answers9

77

Perhaps the most straight way to make this is to invalidateLayout during the viewWillTransitionToSize:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
        return
    }
    flowLayout.invalidateLayout()
}
Philippe
  • 1,507
  • 16
  • 18
valvoline
  • 6,374
  • 3
  • 35
  • 45
26

To have the collection view resize its cells, and have the change animated during rotation, use the transition coordinator:

(Swift 4+)

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    // Have the collection view re-layout its cells.
    coordinator.animate(
        alongsideTransition: { _ in self.collectionView.collectionViewLayout.invalidateLayout() },
        completion: { _ in }
    )
}
Graham Perks
  • 21,623
  • 8
  • 56
  • 79
  • 1
    This is the only thing that gave me smooth transition during collection view size changes and centering its cell – Mette Jun 10 '20 at 13:25
20

You might use viewWillLayoutSubviews. This question should be helpful but this is bassically called whenever the view controller views is about to layout its subviews.

So your code will look like this:

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()

  guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
    return
  }

  if UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation) {
    //here you can do the logic for the cell size if phone is in landscape
  } else {
    //logic if not landscape 
  }

  flowLayout.invalidateLayout()
}
Community
  • 1
  • 1
adolfosrs
  • 8,626
  • 5
  • 36
  • 65
  • 21
    by using this approach you will enter an infinite loop. – valvoline Mar 15 '17 at 11:54
  • 6
    I am not sure that @valvoline is correct that this will cause an infinite loop - however it may well cause performance issues as it is invalidating the collectionview layout far more often than is necessary. It is much better to use `viewWillTransitionToSize`. – JosephH Apr 11 '17 at 10:53
  • 5
    This code causes an infinite loop. It should not be the correct answer. – Josh Jul 26 '17 at 18:56
  • 3
    The app hangs in an infinite loop when using this solution – Dmitry Klochkov Sep 04 '17 at 10:27
  • 3
    This should be unmarked as correct and replaced with the `viewWillTransitionToSize` approach. – Daniel Saidi Oct 06 '17 at 11:37
  • 1
    This does not cause an infinite loop in iOS 11. In fact, it gets called exactly twice on a screen rotation. Works fantastically to correctly resize the cells. – Chuck Krutsinger Sep 18 '18 at 00:09
  • This answer is correct and we don't need any kind of notification observers too. May be it's apple bug which makes it infinite in previous versions of iOS. Its fine in 12 and 13 – Abhishek Thapliyal Oct 11 '19 at 02:50
  • This should only cause an infinite loop if you are using UICollectionViewController. It will not cause an infinite loop otherwise. – Nicholas Feb 11 '20 at 01:20
  • This approach works for me. For the infinite loop. You could store the previous view width or something and check against the current view width. Only call invalidteLayout when both value are different. – William Tong Jun 23 '20 at 09:32
15

I've tended to use viewWillTransitionToSize with the same thing and simply made a call invalidateLayout() in there.

10

I have used following approach, which worked for me. Problem with me was that I was invalidating layout in

viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)

But at this point (as this method name suggest) Device has not been rotated yet. So this method will be called before viewWillLayoutSubviews and hence in this method we do not have right bounds and frames (Safe Area) as device will rotate afterwards.

So I used notifications

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}

@objc func rotated(){
    guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
        return
    }
    flowLayout.invalidateLayout()
}

and then in collection view flow delegate method everything works as expected.

extension ViewController: UICollectionViewDelegateFlowLayout{
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
        if #available(iOS 11.0, *) {
            return CGSize(width: view.safeAreaLayoutGuide.layoutFrame.width, height: 70)
        } else {
            return CGSize(width: view.frame.width, height: 70)
        }
    }
}
Nikunj Acharya
  • 643
  • 6
  • 17
7

traitCollectionDidChange method might be used as well instead of viewWillLayoutSubviews:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    guard let previousTraitCollection = previousTraitCollection, traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
        traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass else {
            return
    }

    if traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular {
        // iPad portrait and landscape
        // do something here...
    }
    if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular {
        // iPhone portrait
        // do something here...
    }
    if traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .compact {
        // iPhone landscape
        // do something here...
    }
    collectionView?.collectionViewLayout.invalidateLayout()
    collectionView?.reloadData()
}
Beau Nouvelle
  • 6,331
  • 3
  • 34
  • 49
birdy
  • 845
  • 13
  • 22
1

You can create a mask content and move it to the collectionView. After the landscape/portrait animation is finished, you must remove it ASAP.

Here is an example:

@property (strong, nonatomic) UIImageView *maskImage;

.........

- (UIImageView *) imageForCellAtIndex: (NSInteger) index {
    UICollectionView *collectionView = self.pagerView.test;
    FSPagerViewCell *cell = nil;
    NSArray *indexPaths = [collectionView indexPathsForVisibleItems];
    for (NSIndexPath *indexPath in indexPaths) {
        if (indexPath.item == index) {
            cell = (FSPagerViewCell *)[collectionView cellForItemAtIndexPath: indexPath];
            break;
        }
    }
    if (cell) {
        return cell.imageView;
    }
    return nil;
}


- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator

{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
         UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
         [self test_didRotateFromInterfaceOrientation: orientation];

         UIImageView *imageView = [self imageForCellAtIndex: self.pagerView.currentIndex];
         if (imageView) {
                 UIImageView *imageView = [self imageForCellAtIndex: self.pagerView.currentIndex];
                 CGSize itemSize = self.pagerView.itemSize;
                 UIImageView *newImage = [[UIImageView alloc] initWithImage: imageView.image];
                 [newImage setFrame: CGRectMake((_contentView.bounds.size.width - itemSize.width)/2.0f, 0, itemSize.width, itemSize.height)];
                 newImage.contentMode = imageView.contentMode;
                 newImage.clipsToBounds = imageView.clipsToBounds;
                 newImage.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
                 [self.pagerView addSubview: newImage];

                 self.maskImage = newImage;
         }

         [self.pagerView.test performBatchUpdates:^{
             [self.pagerView.test setCollectionViewLayout:self.pagerView.test.collectionViewLayout animated:YES];
         } completion:nil];
         // do whatever
     } completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
         [self.maskImage removeFromSuperview];
     }];
}
KuTeo
  • 11
  • 2
1

This is for others with maybe same special case. Situation: I included a UICollectionView as a UITableViewCell in a table view controller. The row height was set to UITableViewAutomaticDimension to adapt on number of cells in the collection view. It didn't layout correctly on device rotation although other cells with dynamic content in the same table view behaved correctly. After a long research I found a solution that worked:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    [self reloadTableViewSilently];
}

- (void) reloadTableViewSilently {
    dispatch_async(dispatch_get_main_queue(), ^{
// optional: [UIView setAnimationsEnabled:false];
        [self.tableView beginUpdates];
        [self.tableView endUpdates];
// optional: [UIView setAnimationsEnabled:true];
    });
}
Herbert Bay
  • 216
  • 2
  • 14
0

I had a task to adjust items size in UICollectionView subclass. The best method for this is setFrame. Property collectionViewFlowLayout I passed from ViewController (in my case it was outlet from default flow layout).

// .h
@property (nonatomic, weak) UICollectionViewFlowLayout *collectionViewFlowLayout;

// .m
- (void)setFrame:(CGRect)frame {
    if (!CGSizeEqualToSize(frame.size, self.frame.size)) {
        collectionViewFlowLayout.itemSize =
        CGSizeMake((UIDeviceOrientationIsLandscape(UIDevice.currentDevice.orientation) ? (frame.size.width - 10) / 2 : frame.size.width), collectionViewFlowLayout.itemSize.height);
    }
    [super setFrame:frame];
}
Igor
  • 11,227
  • 4
  • 49
  • 69