My layout when on split screen is not respecting the width of the split screen.
My custom view is respecting it(the black bar at the top) but anything using autolayout is not respecting the width.
I am handling rotation using
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Is there a helper method for handling split view? Do I handle it in layoutSubview? I would have expected the UICollectionView to handle this for us.
In viewWillTransition I use
guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
flowLayout.invalidateLayout()
I assume by "default" layout you mean a UICollectionViewFlowLayout. It looks like the itemSize width is bigger then the width of the view. Try adding a check in collectionView:layout:sizeForItemAtIndexPath: to make sure that the width is less than or equal to the collection's width.
Yes. That issue still exist in iOS 13 beta 7.
If you implement multiple-window and split two collection view side by side. The only left window will layout correct (rotate several times could see that).
Add that in view controller can fix it:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionViewLayout.invalidateLayout()
}
Related
Hi student learning swift here. On iPad, I have been building a custom segmented control app, where in portrait, the segmented control is at the top and in landscape, the control is on the left. The segmented control is implemented by embedding a collectionView inside a UIView. This just helps me separate the datasource/delegate methods from the viewController. The content is displayed by a standard nested collectionView. Like so:
I have managed to handle rotation within this view controller without any warnings or errors. I initiate two arrays of constraints for portrait and landscape and I deactivate/activate them in viewWillTransitionToSize as needed. The problem occurs when:
new view controller is pushed onto this one -> device is rotated -> pop back to this controller.
Rotating to portrait on a new view controller and popping back causes a "UICollectionViewFLowLayout is not defined" error. Rotating to landscape is less severe, it causes the content collection view to be the wrong size. Like this:
Below is my implementation of viewWillTransitionToSize. Thanks! I'd appreciate any ideas.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
segmentedControl.collectionView.collectionViewLayout.invalidateLayout()
segmentedControl.collectionView.reloadData()
contentScroll.collectionViewLayout.invalidateLayout()
contentScroll.reloadData()
super.viewWillTransition(to: size, with: coordinator)
NSLayoutConstraint.deactivate(p) //deactivate constraints for portrait and landscape
NSLayoutConstraint.deactivate(l)
if isPortrait {
if let layout = segmentedControl.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
}
NSLayoutConstraint.activate(p)
DispatchQueue.main.async { //scrolling content and segmentedControl to their correct places
self.jumpContentViewToIndex(tabIndex: self.targetIndex, animated: false)
self.segmentedControl.scrollAndUpdateIndex(to: self.targetIndex)
}
} else {
if let layout = segmentedControl.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .vertical
}
NSLayoutConstraint.activate(l)
DispatchQueue.main.async { //scrolling content and segmentedControl to their correct places
self.contentScroll.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
self.segmentedControl.scrollAndUpdateIndex(to: self.targetIndex)
}
}
}
After jumping through a lot of stackoverflow links, I was able to find a helpful answer here and here. My problem was because a view controller that is in the middle of a navigation stack will not recalculate its layout until the next layout pass. You could either force a layout update by calling layoutIfNeeded() during rotation or what I did was move all the rotation logic to viewDidLayoutSubviews() and just set a flag in viewWillTransition:tosize to check whether you need to perform your rotation logic.
I have a collection view that the frame appears too tall on the iPhone X. On every other device, the sizing and scrolling works properly as shown below:
However on iPhone X, it looks like this:
The top row is cut off, and it does not scroll all the way down to the last row. Somehow, the sizing correctly calculates the width but not the height, which is about 70 pixels too tall. (I'm not worried about the top and bottom bars. I'll fix those later)
I'm guessing this has something to do with the inset adjustments for the iPhone X screen, but I can't figure out how to fix it. I've tried this in where I size the collection view:
if #available(iOS 11.0, *) {
collectionView.contentInsetAdjustmentBehavior = .always
}
However, I can't seem to fix it.
Edit:
To clarify, the set up for this menu is as follows, there are actually two collection views onscreen. The first scrolls horizontally with paging enabled so that it locks onto each cell. The other collection views are the cells for the first one, and they scroll vertically. We'll call these subCollectionViews.
The subCollectionViews are receiving a size from the original collection view thats too tall. On the storyboard, the collection view's height is defined with respect to the top bar and the bottom paging bar as flush. In the story board, the height of the collection view is about 70 pixels larger than the calculated height during runtime.
So for the cell's layout guide:
override func awakeFromNib() {
cellImage = UIImageView(frame: contentView.frame)
cellImage?.contentMode = .scaleAspectFill
cellImage?.clipsToBounds = true
contentView.addSubview(cellImage!)
}
and for the collection view's layout:
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 8
layout.minimumInteritemSpacing = 8
for the storyboard presets:
I think this is what y'all asked for. If there's anything you need to see, just ask.
I also facing this issue. Need to called safeAreaLayoutGuide for control the size of item.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if #available(iOS 11.0, *) {
return view.safeAreaLayoutGuide.layoutFrame.size
} else {
// Fallback on earlier versions
return view.frame.size
}
}
Add below lines of code. I think it will solve your problem.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.aCollectionVew.setNeedsLayout()
self.aCollectionVew.layoutIfNeeded()
}
Please take all traits, spacing and contentInsetAdjustmentBehavior.
Happy Coding...:)
I calculate itemSize dependent on safe area for UICollectionView with horizontal scroll and custom layout.
But for iPhone X safe area has different size for different orientation. My question is how should I calculate safe area size for landscape orientation in viewWillTransition function? Or how is it possible to do without this calculation?
EDIT
To get safe area size without creating any additional views, use this:
view.safeAreaLayoutGuide.layoutFrame.size
If you want to use viewWillTransition method you can use this:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// Before rotation
print(view.safeAreaLayoutGuide.layoutFrame.size)
coordinator.animate(alongsideTransition: { (context) in
// During rotation
}) { (context) in
// After rotation
print(self.view.safeAreaLayoutGuide.layoutFrame.size)
}
}
In the completion block you will get your desired size, note however, that this code will be called after the rotation.
Original answer
Solution using additional UIView:
What I did was to create a UIView and pin it with constant 0 to Safe Area Guides, so that it always matches size of Safe Area:
I created an #IBOutlet of that UIView and in viewDidLayoutSubviews() check the size:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(containerView.frame.size)
}
After rotation I also get the updated size of that UIView.
Use UICollectionViewDelegateFlowLayout's collectionView(_:layout:sizeForItemAt:). Works like a charm.
Setting the size in the completion block of coordinator.animate(alongsideTransition:completion:) in viewWillTransition(to:with:) didn't work for me because the size animation occurs after the orientation animation ends, which looks weird.
When testing my app on an iPad using multitasking and I change the app size on the app, the collection view cells need to be resized or else they don't fit in the view window.
I have took some measures such as
detecting device to change the size of the cells
measure view size to make sure cells fit within the window
Add an observer if user changes device orientation
This is all great but however when actually changing the window size when using multitasking, or going back to the full screen app, the cell sizes don't change and look out of place. This is until forced by switching orientation or switching view controllers so viewWillLoad or viewDidLoad gets executed again.
So how can I set a notification if the view.frame size changes ?
I think the right function to override is viewWillTransition(to:, with:). It is called whenever the container size is about to change.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.collectionView?.collectionViewLayout.invalidateLayout()
}
I think you are over complicating the issue, just use the following so when the device is rotated or resized the collection view also changes accordingly.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let collectionLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
if UIApplication.shared.statusBarOrientation == UIInterfaceOrientation.landscapeLeft ||
UIApplication.shared.statusBarOrientation == UIInterfaceOrientation.landscapeRight {
//here you can do the logic for the cell size if phone is in landscape
} else {
//logic if not landscape
}
collectionLayout.invalidateLayout()
}
I tried RileyDev answer but the app entered in an infinite loop.
There is no delegate that is executed just when the App's window bounds change, so I've made my own version of the viewWillTransition(to size and traitCollectionDidChange.
With the code below you can detect the window bounds resizing change even if the traits don't change, and resize your collection view cells accordingly invalidating the collection view layout.
var previousWindowBounds: CGRect?
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let currentBounds = view.window?.bounds,
let previousWindowBounds = previousWindowBounds, currentBounds != previousWindowBounds {
collectionView?.collectionViewLayout.invalidateLayout()
}
previousWindowBounds = view.window?.bounds
}
Having implemented a similar method described in iOS 8 Custom Keyboard: Changing the Height, I have two main issues that I can't seem to figure out.
1) When a rotation occurs, there is a terribly noticeable jump. The keyboard rotates at its current height, and then only after the rotation has completed does it abruptly jump to the new height.
2) The contents of the keyboard don't seem to be drawn until the rotation is complete. This is causing a whole list of problems when trying to use the various available callbacks. For example, I'm trying to set the contentSize height of my scrollView, which is at the bottom of my keyboard (similar to the old emoji keyboard), to the same height as my scrollView. However,
scrollView.contentSize = scrollView.frame.size.height
does not work because the scrollView (nor its enclosing view - tried that also) hasn't reached its final bounds yet. I've verified this by checking the bounds after the keyboard is completely loaded - only then does the scrollView return the correct value for its height.
The same thing occurs when I try to set the size for the contents of my collectionView (which comprises the main body of the keyboard):
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let heightPortrait: CGFloat = 0.8
let heightLandscape: CGFloat = 0.8
var collectionViewHeight = collectionView.bounds.size.height
var portrait = collectionViewHeight * heightPortrait
var landscape = collectionViewHeight * heightLandscape
var newDimensions: CGFloat
newDimensions = UIScreen.mainScreen().bounds.size.width < UIScreen.mainScreen().bounds.size.height ? portrait : landscape
return CGSize(width: newDimensions, height: newDimensions)
}
I've implemented viewWillTransitionToSize as well so each object will update on rotation, but when it's called, the bounds are once again not yet set:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
self.setKeyboardHeight() // Change the keyboard height from its default setting.
self.buildScrollView() // Populate the scrollView and set contentSize as well as cell attributes.
println("Before: \(self.view4.frame.size.height)") // Is the same as 'After' (see a few lines below), also swapping out view4 (the scrollView's superview) for scrollView doesn't change anything.
coordinator.animateAlongsideTransition(nil, completion: { context in
self.collectionView?.collectionViewLayout.invalidateLayout()
println("After: \(self.view4.frame.size.height)")
})
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
I've been cracking my head over this for way too long, so if anyone can help me figure it out I will be eternally grateful. Thank you so much for your help and any time you may take to answer/work on this!!