Make UICollectionView scroll right to left? - objective-c

The natural direction for a UICollectionView to scroll when set horizontally is from left to right. Is there any way to reverse this? The simpler the better.

I'm not sure exactly what you mean -- if you set the scrolling to horizontal, it scrolls equally well, left and right. If you want it to start it from the right side, you can use this method:
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.theData.count - 1 inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
This assumes that you have 1 section, and the array populating the collection view is called theData.

Swift4 solution
in cellForItemAt collectionView function
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = categoryBook.dequeueReusableCell(withReuseIdentifier: "HomeCategoryCell", for: indexPath) as! HomeCategoryCell
collectionView.transform = CGAffineTransform(scaleX:-1,y: 1);
cell.transform = CGAffineTransform(scaleX:-1,y: 1);
}
but this solution in some cases did not work properly if it dose not you can use ColletctionView scrollToItem method and you can implement it after you reload the data .
self.YourCollectionView.reloadData()
self.YourCollectionView.scrollToItem(at: NSIndexPath(item: self.YourObjectListData.count - 1, section: 0) as IndexPath, at: .right, animated: false)

Same thing for swift:
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: theData.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)

Use This Extention
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true //RETURN true if collection view needs to enable RTL
}
}

I have found using xCode 12.4 with an app that targets iOS 12 that this there seems to be no need to load the items in a different order or do any transforms. The only issue has to do with the initial scroll position. So all I need to do to get things working in both RTL and LTR is the following:
collectionView.reloadData {
if self.collectionView.effectiveUserInterfaceLayoutDirection == .rightToLeft {
self.collectionView?.scrollToItem(at: IndexPath(row:0, section:0), at: .right, animated: false)
}
}

Related

How to figure out if tableViews footer is displayed?

I have a tableView and I want to know when the footer will be displayed
I used the scrollView to figure out the end of tableView.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let height = scrollView.frame.size.height
let contentYoffset = scrollView.contentOffset.y
let distanceFromBottom = scrollView.contentSize.height - contentYoffset
if distanceFromBottom < height {
// end of tableView
}
}
and I also used Bool to understand when was the first time the footer was displayed
UITableViewDelegate has
#available(iOS 6.0, *)
optional func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int)
method to get this

UICollectionView deselectItem when cell pre-fetching is enabled

On iOS 10.0, UICollectionView pre-fetches cells by default. This leads to cells that are prepared for being shown on screen, but are hidden. This question describes it really well.
The following code will successfully deselect an index path when its cell is either visible or does not exist at all. If the cell exists and is hidden, the index path will be deselected, but the cell becomes stuck in the selected state until it is reused.
collectionView!.deselectItem(at: indexPath, animated: false)
This problem does not exits on iOS 9 or when pre-fetching is disabled with isPrefetchingEnabled = false on iOS 10.0.
Is this a bug in UICollectionView or am I misunderstanding how deselectItem is supposed to work?
Here is the full code of a UICollectionViewController subclass that demonstrates this behaviour with the following steps:
Tap on a cell, so that it becomes selected (red)
Scroll the cell slightly off-screen
Tap the "Deselect Cell" button
Scroll the cell back on screen
Observe how it still looks selected
Tap on another cell
Observe how both cells look selected
Scroll the first cell far off-screen and back again
Observe how the first cell finally does not look selected
import UIKit
private let reuseIdentifier = "Cell"
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
let button = UIButton(frame: CGRect(x: 10, y: 30, width: 360, height: 44))
button.backgroundColor = #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)
button.setTitleColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1), for: .normal)
button.setTitleColor(#colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1), for: .highlighted)
button.setTitle("Deselect Cell", for: .normal)
button.addTarget(self, action: #selector(CollectionViewController.buttonPress), for: .touchUpInside)
view.addSubview(button)
}
func buttonPress() {
for indexPath in collectionView!.indexPathsForSelectedItems ?? [IndexPath]() {
let cell = collectionView!.cellForItem(at: indexPath)
NSLog("Deselecting indexPath: %#, cell: %#", indexPath.description, cell?.frame.debugDescription ?? "not visible")
collectionView!.deselectItem(at: indexPath, animated: false)
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 300
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
cell.selectedBackgroundView = UIView(frame: cell.bounds)
cell.selectedBackgroundView!.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
return cell
}
}
As far as I can tell this is a bug in UICollectionView, and I've opened a Radar myself. You might want to do so as well to apply more pressure.
Even before prefetching, collection view didn't bother deselecting cells that weren't visible. Even cellForItemAtIndexPath: states that it returns nil for cells that are not visible.
But before prefetching, as soon as a cell left the content view it was added to the reuse pool, and when you scrolled back you got a reused cell that had its selected state reset.
Now, that cell remains loaded and wont reset until it's reused, by scrolling further away, beyond the prefetching area.
To fix this you can either set prefetchingEnabled to NO and lose its benefits, or update selection state whenever a cell appears -
- (void)collectionView:(UICollectionView *)collectionView
willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
if (!cell.isSelected) {
return;
}
if ([[collectionView indexPathsForSelectedItems] containsObject:indexPath]) {
return;
}
cell.selected = NO;
}
This doesn't seem to diminish performance.

Change insets for each section in a UICollectionView

I have a simple UICollectionView written in Swift. My sectionInsets used to be 0, 35, 0, 35 which gave the entire collectionView margins left end right. My minimumLineSpacing is set to default 10, which means all of my cells have a margin of 10 between each other. All good.
Then I divided the UICollectionView into two sections. The first section serves to hold a cell that acts as a prompt to create a new cell (which will end up in section 2). The second section holds all other cells (the actual content).
My problem is that ever since I added the second section, the spacing between the one static cell in section 1 and the first cell in section 2 isn't 10 anymore but 35+10. I'm now trying to get this particular spacing back to 10.
My thought was to add an if condition to identify whether it's section 1 or 2 and set the sectionInsets accordingly. I'm stuck however as to where and how exactly I should do this. I don't seem to be able to call sectionInsets or FlowLayout from my View Controller at e.g. cellForItemAtIndexPath. Should I do it in my CustomCollectionViewFlowLayout subclass? Here is the code that I have in there.
- (void)awakeFromNib
{
self.itemSize = CGSizeMake(305.0, 407.0);
self.minimumInteritemSpacing = 20.0;
self.minimumLineSpacing = 10.0;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(0, 35, 0, 35);
}
And this is the code for my sections in my view controller:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let firstCell = collectionView.dequeueReusableCellWithReuseIdentifier("createCell", forIndexPath: indexPath) as! CreateCollectionViewCell
firstCell.imageView.image = UIImage(named: "puppy3")
return firstCell
} else {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("mainCell", forIndexPath: indexPath) as! MainCollectionViewCell
cell.imageView?.image = self.imageArray[indexPath.row]
return cell
}
}
UPDATE:
I figured it out. It works for me by adding two variables for sectionInsets of type UIEdgeInsets in my view controller and then adding this function:
//Set custom insets per section
func collectionView(collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
if section == 0 {
return sectionInsets1
} else {
return sectionInsets2
}
}

the behavior of the UICollectionViewFlowLayout is not defined, because the cell width is greater than collectionView width

2015-08-18 16:07:51.523 Example[16070:269647] the behavior of the
UICollectionViewFlowLayout is not defined because: 2015-08-18
16:07:51.523
Example[16070:269647] the item width must be less than
the width of the UICollectionView minus the section insets left and
right values, minus the content insets left and right values.
2015-08-18 16:07:51.524 Example[16070:269647] The relevant
UICollectionViewFlowLayout instance is <UICollectionViewFlowLayout:
0x7f24d670>, and it is attached to <UICollectionView: 0x7b377200;
frame = (0 0; 768 1024); clipsToBounds = YES; autoresize = W+H;
gestureRecognizers = <NSArray: 0x7f24ce30>; animations = {
position=<CABasicAnimation: 0x7f279d60>;
bounds.origin=<CABasicAnimation: 0x7f279430>;
bounds.size=<CABasicAnimation: 0x7f279480>; }; layer = <CALayer:
0x7aec9cf0>; contentOffset: {0, 0}; contentSize: {1024, 770}>
collection view layout: <UICollectionViewFlowLayout: 0x7f24d670>.
2015-08-18 16:07:51.524 Example[16070:269647] Make a symbolic
breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch
this in the debugger.
That is what I get and what I do
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(self.collectionView!.frame.size.width - 20, 66)
}
I rotate from landscape to portrait then the console shows this error message only in iOS 9 if anyone knows what happens and if there is a fix for this?
select collectionview in storyboard go to size inspector change estimate size to none
This happens when your collection view resizes to something less wide (go from landscape to portrait mode, for example), and the cell becomes too large to fit.
Why is the cell becoming too large, as the collection view flow layout should be called and return a suitable size ?
collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath)
Update to include Swift 4
#objc override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{ ... }
This is because this function is not called, or at least not straight away.
What happens is that your collection view flow layout subclass does not override the shouldInvalidateLayoutForBoundsChange function, which returns false by default.
When this method returns false, the collection view first tries to go with the current cell size, detects a problem (which logs the warning) and then calls the flow layout to resize the cell.
This means 2 things :
1 - The warning in itself is not harmful
2 - You can get rid of it by simply overriding the shouldInvalidateLayoutForBoundsChange function to return true.
In that case, the flow layout will always be called when the collection view bounds change.
XCode 11
For me, the reason was that the CollectionView's Estimate option size was set to Automatic.
It seems it is the default behavior in XCode 11.
Hence for me which I loaded the images in the cells, because of the size of the images and the automatic option (which wants to adapt the size of the cell with the size of the image) the cell's width became greater than the CollectionView's width.
By setting this option to None the problem solved.
I've also seen this occur when we set the estimatedItemSize to automaticSize and the computed size of the cells is less than 50x50 (Note: This answer was valid at the time of iOS 12. Maybe later versions have it fixed).
i.e. if you are declaring support for self-sizing cells by setting the estimated item size to the automaticSize constant:
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
and your cells are actually smaller than 50x50 points, then UIKit prints out these warnings. FWIW, the cells are eventually sized appropriately and the warnings seem to be harmless.
One fix workaround is to replace the constant with a 1x1 estimated item size:
flowLayout.estimatedItemSize = CGSize(width: 1, height: 1)
This does not impact the self-sizing support, because as the documentation for estimatedItemSize mentions:
Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method.
However, it might/might-not have performance implications.
I've solve this problem by using safeAreaLayoutGuide.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (view.safeAreaLayoutGuide.layoutFrame.width), height: 80);
}
and you also have to override this function to support portrait and landscape mode correctly.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout();
}
I am using a subclass of UICollectionViewFlowLayout and using its itemSize property to specify the cell size (instead of the collectionView:sizeForItemAtIndexPath: delegate method). Every time I rotate the screen to a shorter width one (e.g. landscape -> portrait) I get this huge warning in question.
I was able to fix it by doing 2 steps.
Step 1: In UICollectionViewFlowLayout subclass's prepareLayout() function, move super.prepareLayout() to after where self.itemSize is set. I think this makes the super class to use the correct itemSize value.
import UIKit
extension UICollectionViewFlowLayout {
var collectionViewWidthWithoutInsets: CGFloat {
get {
guard let collectionView = self.collectionView else { return 0 }
let collectionViewSize = collectionView.bounds.size
let widthWithoutInsets = collectionViewSize.width
- self.sectionInset.left - self.sectionInset.right
- collectionView.contentInset.left - collectionView.contentInset.right
return widthWithoutInsets
}
}
}
class StickyHeaderCollectionViewLayout: UICollectionViewFlowLayout {
// MARK: - Variables
let cellAspectRatio: CGFloat = 3/1
// MARK: - Layout
override func prepareLayout() {
self.scrollDirection = .Vertical
self.minimumInteritemSpacing = 1
self.minimumLineSpacing = 1
self.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: self.minimumLineSpacing, right: 0)
let collectionViewWidth = self.collectionView?.bounds.size.width ?? 0
self.headerReferenceSize = CGSize(width: collectionViewWidth, height: 40)
// cell size
let itemWidth = collectionViewWidthWithoutInsets
self.itemSize = CGSize(width: itemWidth, height: itemWidth/cellAspectRatio)
// Note: call super last if we set itemSize
super.prepareLayout()
}
// ...
}
Note that the above change will somehow make the layout size change when screen rotates stops working. This is where step 2 comes in.
Step 2: Put this in the view controller that holds the collection view.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
collectionView.collectionViewLayout.invalidateLayout()
}
Now the warning is gone :)
Some Notes:
Make sure you are adding constraints to the collectionView and not using collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
Make sure you are not calling invalidateLayout in viewWillTransitionToSize() because the width of an edge-to-edge cell in landscape is larger than the collection view’s frame width in portrait. See below references.
References
Making Adaptive Forms using UICollectionView
Collection view with self-sizing cells
For Xcode 11, none of the above methods worked for me.
It turned out one need to set the Estimate Size to None in the collection view size panel.
see: https://forums.developer.apple.com/thread/123625
Its happens because your collection view cell's width is bigger than collection view width after rotation.suppose that you have a 1024x768 screen and your collection view fills the screen.When your device is landscape,your cell's width will be self.collectionView.frame.size.width - 20 =1004 and its greater than your collection view's width in portrait = 768.The debugger says that "the item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values".
This works for me:
Invalidate the layout on rotation:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
Have a separate function for calculating the available width:
Note: taking in to account both section inset and content inset.
// MARK: - Helpers
func calculateCellWidth(for collectionView: UICollectionView, section: Int) -> CGFloat {
var width = collectionView.frame.width
let contentInset = collectionView.contentInset
width = width - contentInset.left - contentInset.right
// Uncomment the following two lines if you're adjusting section insets
// let sectionInset = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, insetForSectionAt: section)
// width = width - sectionInset.left - sectionInset.right
// Uncomment if you are using the sectionInset property on flow layouts
// let sectionInset = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? UIEdgeInsets.zero
// width = width - sectionInset.left - sectionInset.right
return width
}
And then of course finally returning the item size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: calculateCellWidth(for: collectionView, section: indexPath.section), height: 60)
}
I think it is important to note that this works because invalidating a layout wont trigger a recalculation of the cell size immediately, but during the next layout update cycle. This means that once the item size callback is eventually called, the collection view has the correct frame, thus allowing accurate sizing.
Voila!
Turn your collection view's estimate size 'None' value from automatic.
You can do this by the storyboard or programatically.
But you have to calculate it manually with custom layout class or 'UICollectionViewDelegateFlowLayout' protocol APIs
After doing some experimenting, this seems to also be tied to how you layout your collectionView.
The tl;dr is: Use AutoLayout, not autoresizingMask.
So for the core of the problem the best solutions I've found to handle orientation change use the following code, which all makes sense:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
However, there are still situations where you can get the item size warning. For me it's if I am in landscape in one tab, switch to another tab, rotate to portrait, then return to the previous tab. I tried invalidating layout in willAppear, willLayout, all the usual suspects, but no luck. In fact, even if you call invalidateLayout before super.willAppear() you still get the warning.
And ultimately, this problem is tied to the size of the collectionView bounds updating before the delegate is asked for the itemSize.
So with that in mind I tried using AutoLayout instead of collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight], and with that, problem solved! (I use SnapKit to do AutoLayout so that I don't pull my hair out constantly). You also just need to invalidateLayout in viewWillTransition (without the coordinator.animate, so just as you have in your example), and also invalidateLayout at the bottom of viewWillAppear(:). That seems to cover all situations.
I dont know if you are using autoresizing or not - it would be interesting to know if my theory/solution works for everyone.
You can check in debugger if collectionView.contentOffset is changed to negative in my case it changes from (0,0) to (0,-40). You can solve this issue by using this method
if ([self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
self.automaticallyAdjustsScrollViewInsets = NO;
}
This solution definitely works.. Try this code
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
collectionView.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
I had a similar issue.
In my case, I had a collection view and when you tapped on one of the cells, a popover with a UITextField opened, to edit the item. After that popover disappeared, the self.collectionView.contentInset.bottom was set to 55 (originally 0).
To fix my issue, after the popover view disappears, I’m manually setting contentInset to UIEdgeInsetsZero.
The original issue seems to be related to the contextual prediction bar that shows up on top of the keyboard. When the keyboard is hidden, the bar disappears, but the contentInset.bottom value is not restored to the original value.
Since your issue seems to be related to the width and not to the height of the cell, check if any of the contentInset or layout.sectionInset values are the same as the one set by you.
I had the same issue. I fixed this by clearing my collection view of it's constraints, and resetting them in Storyboard.
I could solve it by putting
super.prepare()
at end of
override func prepare() { }
I've been struggling with this problem for some hours, too.
My UICollectionViewController is embedded in a container view, when rotating the iPad from landscape to portrait it showed the error. The cells did resize though.
I solved the error by calling the following in the UICollectionViewController:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
self.collectionView.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
The "super" has to be called after invalidating the layout.
Additionally I needed to call the following in the view where the container view is embedded in:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
DispatchQueue.main.async {
self.collectionViewController.collectionView.reloadData()
}
}
This way, the cells are updated after the screen rotation is done.
It is my solution:
Remove all UICollectionViewFlowDelegate methods from your view controller.
Create your own flowLayout class as a subclass UICollectionViewFlowLayout.
class ImageCollectionViewFlowLayout: UICollectionViewFlowLayout {}
Configure all sizes and other parameters into prepare() method. See my example below.
private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
self.sectionInset = sectionInsets
self.minimumLineSpacing = sectionInsets.bottom
self.minimumInteritemSpacing = sectionInsets.bottom
let width = collectionView.bounds
.inset(by: collectionView.layoutMargins)
.inset(by: self.sectionInset)
.width
self.estimatedItemSize = CGSize(width: width, height: width + 16)
//self.itemSize - I do not set itemsize because my cell has dynamical height
}
Set your new class for collection view on your storyboard(or xib).
That's all. Keep your code clean.
if you're a using Dynamic Types for fonts, just reload a collectionView when font size is changing:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
collectionView?.reloadData()
}
I found that this worked quite well for UICollectionViewController and animated correctly:
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
// Ensure the layout is within the allowed size before animating below, or
// `UICollectionViewFlowLayoutBreakForInvalidSizes` breakpoint will trigger
// and the logging will occur.
if ([self.collectionView.collectionViewLayout isKindOfClass:[YourLayoutSubclass class]]) {
[(YourLayoutSubclass *)self.collectionView.collectionViewLayout updateForWidth:size.width];
}
// Then, animate alongside the transition, as you would:
[coordinator animateAlongsideTransition:^
(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self.collectionView.collectionViewLayout invalidateLayout];
}
completion:nil];
[super viewWillTransitionToSize:size
withTransitionCoordinator:coordinator];
}
///////////////
#implementation YourLayoutSubclass
- (void)prepareLayout {
[super prepareLayout];
[self updateForWidth:self.collectionView.bounds.size.width];
}
- (void)updateForWidth:(CGFloat)width {
// Update layout as you wish...
}
#end
... See inline code comments above for explanation. 👍🏻
Any Changes in UIView must be done from main thread. Put your code of collectionView addition to views hierarchy into:
DispatchQueue.main.async {
...
}
In my case, for a vertical UICollectionView I had:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.bounds.width, height: padding(scale: 37))
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: padding(scale: 10), left: padding(scale: 4), bottom: padding(scale: 3), right: padding(scale: 4))
}
I fixed it by resting the horizontal insets to the size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (collectionView.bounds.width - (2 * padding(scale: 4))), height: padding(scale: 37))
}
In case you're setting the itemSize of collectionViewLayout in code, then make sure that the size is valid. Set a breakpoint on the code line where you set the itemSize, and check if the size complies to the instruction of the error warning:
The item width must be less than the width of the UICollectionView minus the section insets left and right values, minus the content insets left and right values.
This is what you need:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
//Get frame width
let width = self.view.frame.width
//I want a width of 418 and height of 274 (Aspect ratio 209:137) with a margin of 24 on each side of the cell (See insetForSectionAt 24 + 24 = 48). So, check if a have that much screen real estate.
if width > (418 + 48) {
//If I do return the size I want
return CGSize(width: 418, height: 274)
}else{
//Get new width. Frame width minus the margins I want to maintain
let newWidth = (width - 48)
//If not calculate the new height that maintains the aspect ratio I want. NewHeight = (originalHeight / originalWidth) * newWidth
let height = (274 / 418) * newWidth
//Return the new size that is Aspect ratio 209:137
return CGSize(width: newWidth, height: height)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 33
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 33
}

UITextView text content doesn't start from the top

I have a long text coming from my JSON file but when I click the link from my UITableViewCell to go to my UIViewController page, the UITextView text loads the string content but it does not show the content from the beginning and I have to scroll up all the time.
What I need to do?
I had the same problem, and turns out I had to set the content offset in viewDidLayoutSubviews for it to take effect. I'm using this code to display attributed static text.
- (void)viewDidLayoutSubviews {
[self.yourTextView setContentOffset:CGPointZero animated:NO];
}
SWIFT 3:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.textView.setContentOffset(CGPoint.zero, animated: false)
}
This is the only way that worked for me. I disable the scroll of the UITextView before the view is loaded and then i enable it again:
override func viewWillAppear(_ animated: Bool) {
yourTextView.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
yourTextView.isScrollEnabled = true
}
[self.textView scrollRangeToVisible:NSMakeRange(0, 1)];
in viewDidLoad
By Programmatically before loading the content disable the scrolling property of textview
textview.scrollenabled = NO;
And after loading enable the scrolling of textview textview.scrollenabled = YES;
As well check the XIB, always non-check the scrolling enabled of Textview.
The answers for the question Blank space at top of UITextView in iOS 10 provide a much cleaner end user experience.
In viewDidLoad of the view controller containing the text view:
self.automaticallyAdjustsScrollViewInsets = false
Setting textView.setContentOffset(CGPointMake(0,0), animated: false) and some of these other suggestions do work when called in the viewDidLayoutSubviews() but on older devices like iPad 2 and older you will actually see the text get scrolled when the screen is displayed. That is not something you want the end user to see.
I was still having problems after using these solutions. The problem definitely seems to relate to having transparent navigation bars and selecting to automatically adjust content insets on the view controller. If you don't care about your text scrolling underneath the navigation bar then it's best to leave these settings off and constrain the top of your textview to the bottom of the navigation bar, rather than to the top of the viewcontroller.
If like me you wanted it to appear underneath your navigation bar when you scroll down; then the solution that worked for me was to add this.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGFloat offset = self.navigationController.navigationBar.frame.size.height+[UIApplication sharedApplication].statusBarFrame.size.height;
[self.textView setContentOffset:CGPointMake(0, -offset) animated:NO];
}
This just looks for the height of the navigation bar and status bar and adjusts the content offset accordingly.
Note that one downside of this approach is that when the device rotates you'll end up scrolling back to the top.
For me fine works this code:
textView.attributedText = newText //or textView.text = ...
//this part of code scrolls to top
textView.contentOffset.y = -64 //or = 0 if no Navigation Bar
textView.scrollEnabled = false
textView.layoutIfNeeded()
textView.scrollEnabled = true
For scroll to exact position and show it on top of screen I use this code:
var scrollToLocation = 50 //<needed position>
textView.contentOffset.y = textView.contentSize.height
textView.scrollRangeToVisible(NSRange.init(location: scrollToLocation, length: 1))
Setting contentOffset.y scrolls to the end of text, and then scrollRangeToVisible scrolls up to value of scrollToLocation. Thereby, needed position appears in first line of scrollView.
Similar to some other answers, but with the added benefit that you won't cause a scroll to top on subsequent device rotations. Works well in Swift 2.2
/// Whether view has laid out subviews at least once before.
var viewDidLayoutSubviewsAtLeastOnce = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !viewDidLayoutSubviewsAtLeastOnce {
textView.setContentOffset(CGPoint(x: 0, y: -textView.contentInset.top), animated: false)
}
viewDidLayoutSubviewsAtLeastOnce = true
}
Swift Version
A combination of things will be needed:
1.) Set your outlet
#IBOutlet var textView: UITextView!
2.) In storyboard on View Controller turn off "Adjust Scroll View Insets"
3.) Set content to zero top by adding below to your view controller
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myUITextView.setContentOffset(CGPointZero, animated: false)
}
Instead of setting content offset from viewDidLayoutSubviews you can write layoutIfNeeded from viewDidLoad to set the proper position of textview as below:
self.textView.layoutIfNeeded()
self.textView.setContentOffset(CGPoint.zero, animated: false)
Cheers !!
In Swift 2, Xcode 7 solution, to leave scroll Enabled as well as have the text start at the top, this is all you need:
#IBOutlet weak var myUITextView: UITextView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//start scroll at top of text
myUITextView.scrollRangeToVisible(NSMakeRange(0, 0))
}
Swift 3.0
override func viewWillAppear(_ animated: Bool) {
privacyText.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
privacyText.isScrollEnabled = true
}
This worked the best for me! I placed this within viewDidLoad().
//TextView Scroll starts from the top
myTextView.contentOffset.y = 0
Here's another way to do it that always works for me. Objective-C:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.textView setContentOffset:CGPointZero animated:NO];
}
And in Swift:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
textView.setContentOffset(CGPointZero, animated: false)
}
Swift version:
override func viewDidLayoutSubviews() {
yourTextView.setContentOffset(CGPointZero, animated: false)
}
add the following function to your view controller class...
Swift 3
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(.zero, animated: false)
}
Swift 2.1
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(CGPointZero, animated: false)
}
Objective C
- (void)viewDidLayoutSubviews {
[self.mainTextView setContentOffset:CGPointZero animated:NO];
}
或者 你在ViewDidAppear 里面加上滚动,这样用户会看到他往上滚动到第一行
in swift 4 with attributed text any of answer does not help me and i combine some answers in topic.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
uiTextview.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
uiTextview.isScrollEnabled = true
uiTextview.setContentOffset(CGPoint.zero, animated: false)
}
Swift 3, 4, 5 solution:
Steps to solve the issue:
Disable the UITextView scroll
set scrollRectToVisible
enable UITextView scroll
Code:
yourTextView.isScrollEnabled = false
let rect:CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
yourTextView.scrollRectToVisible(rect, animated: false)
yourTextView.isScrollEnabled = true
This Worked for me. Hope that will help!
This is how i did it. I subclassed textview, and:
override func willMoveToSuperview(newSuperview: UIView?) {
self.scrollEnabled = false
}
override func layoutSubviews() {
super.layoutSubviews()
self.scrollEnabled = true
}
From storyboard, select the view controller on which you have you text view placed. In the attributes inspector, uncheck "Adjust Scroll View Insets". That's it.
Put this code on your class
override func viewDidLayoutSubviews() {
self.About_TV.setContentOffset(.zero, animated: false) // About_TV : your text view name)
}
Add code to the viewdidload
self.automaticallyAdjustsScrollViewInsets = NO;