Dynamically setting layout on UICollectionView causes inexplicable contentOffset change - iphone

According to Apple's documentation (and touted at WWDC 2012), it is possible to set the layout on UICollectionView dynamically and even animate the changes:
You normally specify a layout object when creating a collection view but you can also change the layout of a collection view dynamically. The layout object is stored in the collectionViewLayout property. Setting this property directly updates the layout immediately, without animating the changes. If you want to animate the changes, you must call the setCollectionViewLayout:animated: method instead.
However, in practice, I've found that UICollectionView makes inexplicable and even invalid changes to the contentOffset, causing cells to move incorrectly, making the feature virtually unusable. To illustrate the problem, I put together the following sample code that can be attached to a default collection view controller dropped into a storyboard:
#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
The controller sets a default UICollectionViewFlowLayout in viewDidLoad and displays a single cell on-screen. When the cells is selected, the controller creates another default UICollectionViewFlowLayout and sets it on the collection view with the animated:YES flag. The expected behavior is that the cell does not move. The actual behavior, however, is that the cell scroll off-screen, at which point it is not even possible to scroll the cell back on-screen.
Looking at the console log reveals that the contentOffset has inexplicably changed (in my project, from (0, 0) to (0, 205)). I posted a solution for the solution for the non-animated case (i.e. animated:NO), but since I need animation, I'm very interested to know if anyone has a solution or workaround for the animated case.
As a side-note, I've tested custom layouts and get the same behavior.

UICollectionViewLayout contains the overridable method targetContentOffsetForProposedContentOffset: which allows you to provide the proper content offset during a change of layout, and this will animate correctly. This is available in iOS 7.0 and above

I have been pulling my hair out over this for days and have found a solution for my situation that may help.
In my case I have a collapsing photo layout like in the photos app on the ipad. It shows albums with the photos on top of each other and when you tap an album it expands the photos. So what I have is two separate UICollectionViewLayouts and am toggling between them with [self.collectionView setCollectionViewLayout:myLayout animated:YES] I was having your exact problem with the cells jumping before animation and realized it was the contentOffset. I tried everything with the contentOffset but it still jumped during animation. tyler's solution above worked but it was still messing with the animation.
Then I noticed that it happens only when there were a few albums on the screen, not enough to fill the screen. My layout overrides -(CGSize)collectionViewContentSize as recommended. When there are only a few albums the collection view content size is less than the views content size. That's causing the jump when I toggle between the collection layouts.
So I set a property on my layouts called minHeight and set it to the collection views parent's height. Then I check the height before I return in -(CGSize)collectionViewContentSize I ensure the height is >= the minimum height.
Not a true solution but it's working fine now. I would try setting the contentSize of your collection view to be at least the length of it's containing view.
edit:
Manicaesar added an easy workaround if you inherit from 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)));
}

2019 actual solution
Say you have a number of layouts for your "Cars" view.
Let's say you have three.
CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...
It will jump when you animate between layouts.
It's just an undeniable mistake by Apple. It jumps when you animate, without question.
The fix is this:
You must have a global float, and, the following base class:
var avoidAppleMessupCarsLayouts: CGPoint? = nil
class FixerForCarsLayouts: UICollectionViewLayout {
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
avoidAppleMessupCarsLayouts = collectionView?.contentOffset
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
if avoidAppleMessupCarsLayouts != nil {
return avoidAppleMessupCarsLayouts!
}
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
So here are the three layouts for your "Cars" screen:
CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...
That's it. It now works.
Incredibly obscurely, you could have different "sets" of layouts (for Cars, Dogs, Houses, etc.), which could (conceivably) collide. For this reason, have a global and a base class as above for each "set".
This was invented by passing user #Isaacliu, above, many years ago.
A detail, FWIW in Isaacliu's code fragment, finalizeLayoutTransition is added. In fact it's not necessary logically.
The fact is, until Apple change how it works, every time you animate between collection view layouts, you do have to do this. That's life!

This issue bit me as well and it seems to be a bug in the transition code. From what I can tell it tries to focus on the cell that was closest to the center of the pre-transition view layout. However, if there doesn't happen to be a cell at the center of the view pre-transition then it still tries to center where the cell would be post-transition. This is very clear if you set alwaysBounceVertical/Horizontal to YES, load the view with a single cell and then perform a layout transition.
I was able to get around this by explicitly telling the collection to focus on a specific cell (the first cell visible cell, in this example) after triggering the layout update.
[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];
}

Jumping in with a late answer to my own question.
The TLLayoutTransitioning library provides a great solution to this problem by re-tasking iOS7s interactive transitioning APIs to do non-interactive, layout to layout transitions. It effectively provides an alternative to setCollectionViewLayout, solving the content offset issue and adding several features:
Animation duration
30+ easing curves (courtesy of Warren Moore's AHEasing library)
Multiple content offset modes
Custom easing curves can be defined as AHEasingFunction functions. The final content offset can be specified in terms of one or more index paths with Minimal, Center, Top, Left, Bottom or Right placement options.
To see what I mean, try running the Resize demo in the Examples workspace and playing around with the options.
The usage is like this. First, configure your view controller to return an instance of TLTransitionLayout:
- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}
Then, instead of calling setCollectionViewLayout, call transitionToCollectionViewLayout:toLayout defined in the UICollectionView-TLLayoutTransitioning category:
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];
This call initiates an interactive transition and, internally, a CADisplayLink callback that drives the transition progress with the specified duration and easing function.
The next step is to specify a final content offset. You can specify any arbitrary value, but the toContentOffsetForLayout method defined in UICollectionView-TLLayoutTransitioning provides an elegant way to calculate content offsets relative to one or more index paths. For example, in order to have a specific cell to end up as close to the center of the collection view as possible, make the following call immediately after 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;

Easy.
Animate your new layout and collectionView's contentOffset in the same animation block.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
[self.collectionView setContentOffset:CGPointMake(0, -64)];
} completion:nil];
It will keep self.collectionView.contentOffset constant.

If you are simply looking for the content offset to not change when transition from layouts, you can creating a custom layout and override a couple methods to keep track of the old contentOffset and reuse it:
#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
All this is doing is saving the previous content offset before the layout transition in prepareForTransitionFromLayout, overwriting the new content offset in targetContentOffsetForProposedContentOffset, and clearing it in finalizeLayoutTransition. Pretty straightforward

If it helps add to the body of experience: I encountered this problem persistently regardless of the size of my content, whether I had set a content inset, or any other obvious factor. So my workaround was somewhat drastic. First I subclassed UICollectionView and added to combat inappropriate content offset setting:
- (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 --;
}
I'm not proud of it but the only workable solution seems to be completely to reject any attempt by the collection view to set its own content offset resulting from a call to setCollectionViewLayout:animated:. Empirically it looks like this change occurs directly in the immediate call, which obviously isn't guaranteed by the interface or the documentation but makes sense from a Core Animation point of view so I'm perhaps only 50% uncomfortable with the assumption.
However there was a second issue: UICollectionView was now adding a little jump to those views that were staying in the same place upon a new collection view layout — pushing them down about 240 points and then animating them back to the original position. I'm unclear why but I modified my code to deal with it nevertheless by severing the CAAnimations that had been added to any cells that, actually, weren't moving:
- (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"];
}
}
}
This appears not to inhibit views that actually do move, or to cause them to move incorrectly (as if they were expecting everything around them to be down about 240 points and to animate to the correct position with them).
So this is my current solution.

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)

Related

Action while scroll up from UITableview

I want to write an action when user scroll up from UITableView,How can I do this ?
Try this
Step 1:
yourUITableView.delegate = self;
Step 2:
CGFloat yOffset = 0.0;
Step 3:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < yOffset) {
// scrolls down.
yOffset = scrollView.contentOffset.y;
}
else
{
// scrolls up.
yOffset = scrollView.contentOffset.y;
// Your Action goes here...
}
}
There are several ways I can think of that can help you with this. For once, if you don't need to have a precise recognition of the upward swiping you can use the UITableView behaviour to do this.
Each time a new row appears, tableView:cellForRowAtIndexPath: selector is called. You can use this method to know de direction the user is scrolling by comparing the previously inserted row (indexPath.row) to see if the newer is lower. If it is, then the user is scrolling up.
For more precision you can try using the Swipe Gesture Recognizer. I've personally never used it, but I can't imagine it being hard to use.
UITableView inherits from UIScrollView, so you can use the delegate from UIScrollView for your tableView: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIScrollViewDelegate_Protocol/Reference/UIScrollViewDelegate.html#//apple_ref/occ/intf/UIScrollViewDelegate
tableView.delegate = self;
..
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
NSLog(#"Content offset: %f", scrollView.contentOffset.y);
// do something like
// if firstOffset < secondOffset {
// [self yourAction];
//}
}

Can I override scroll view's automatic behavior to scroll to the first responder?

I have a UITextField inside a UIScrollView (a few levels deep). I am watching UIKeyboardDidShowNotification, and also calling the same code when I manually change the first responder (I might change to a different text field without momentarily hiding the keyboard). In that code I use scrollRectToVisible:animated: to make sure the UITextField is visible.
I was having a huge headache debugging why that was acting funny, but I realized now that UIScrollView automatically ensures that the first responder is within its bounds. I am changing the frame of the UIScrollView so that none of it is hidden behind the keyboard.
However, my code can be slightly smarter than their code, because I want to show not only the UITextField, but some nearby related views as well. I try to show those views if they will fit; if not whatever, I try to show as much of them as I can but at least ensure that the UITextField is visible. So I want to keep my custom code.
The automatic behavior interferes with my code. What I see is the scroll view gently scroll up so that the bottom edge of my content is visible, then it snaps down to where my code told it to position.
Is there anyway to stop the UIScrollView from doing its default capability of scrolling the first responder into view?
More Info
On reviewing the documentation I read that they advise to change the scroll view's contentInset instead of frame. I changed that and eliminated some unpredictable behavior, but it didn't fix this particular problem.
I don't think posting all the code would necessarily be that useful. But here is the critical call and the values of important properties at that time. I will just write 4-tuples for CGRects; I mean (x, y, width, height).
[scrollView scrollRectToVisible:(116.2, 71.2, 60, 243) animated:YES];
scrollView.bounds == (0, 12, 320, 361)
scrollView.contentInset == UIEdgeInsetsMake(0, 0, 118, 0)
textField.frame == (112.2, 222.6, 24, 24)
converted to coordinates of the immediate subview of scrollView == (134.2, 244.6, 24, 24)
converted to coordinates of scrollView == (134.2, 244.6, 24, 24)
So the scroll view bottom edge is really at y == 243 because of the inset.
The requested rectangle extends to y == 314.2.
The text field extends to y == 268.6.
Both are out of bounds. scrollRectToVisible is trying to fix one of those problems. The standard UIScrollView / UITextField behavior is trying to fix the other. They don't come up with quite the same solution.
I didn't test this particular situation, but I've managed to prevent a scrollview from bouncing at the top and bottom by subclassing the scrollview and overriding setContentOffset: and setContentOffset:animated:. The scrollview calls this at every scroll movement, so I'm fairly certain they will be called when scrolling to the textfield.
You can use the delegate method textFieldDidBeginEditing: to determine when the scroll is allowed.
In code:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.blockingTextViewScroll = YES;
}
-(void)setContentOffset:(CGPoint)contentOffset
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset];
}
}
-(void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(self.blockingTextViewScroll)
{
self.blockingTextViewScroll = NO;
}
else
{
[super setContentOffset:contentOffset animated:animated];
}
}
If your current scroll behaviour works with a setContentOffset: override, just place it inside the else blocks (or preferably, in a method you call from the else blocks).
In my project I have succeeded to achieve this by performing my scroll only after some delay.
- (void)keyboardWillShow:(NSNotification *)note
{
NSDictionary *userInfo = note.userInfo;
CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets contentInsets = self.tableView.contentInset;
contentInsets.bottom += keyboardFrame.size.height;
[self.tableView setContentInset:contentInsets];
[self performSelector:#selector(scrollToEditableCell) withObject:nil afterDelay:0];
}
Also there is other possibility to make your view with additional views to be first responder and fool scroll view where to scroll. Haven't tested this yet.
This may turn out to be useless, but have you tried setting scrollView.userInteractionEnabled to NO before calling scrollrectToVisible: & then setting it back to YES? It may prevent the automatic scrolling behavior.
Try changing the view autoresizing to UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin. The default is FlexibleTopMargin so maybe thats the reason. btw scrollRectToVisible: is using the scrollView.contentSize.
The other thing you can try to change the scrollView size first and then apply the scrollRectToVisible: change. First frame change, then content change. (Maybe observe the keyboard did appear event)
The automatic scrolling behavior seems to be especially buggy starting in iOS 14. I alleviated the problem by subclassing UIScrollView and overriding setContentOffset to do nothing. Here is the bases of my code.
class ManualScrollView: UIScrollView {
/// Use this function to set the content offset. This will forward the call to
/// super.setContentOffset(:animated:)
/// - Parameters:
/// - contentOffset: A point (expressed in points) that is offset from the content view’s origin.
/// - animated: true to animate the transition at a constant velocity to the new offset, false to make the transition immediate.
func forceContentOffset(_ contentOffset: CGPoint, animated: Bool) {
super.setContentOffset(contentOffset, animated: animated)
}
/// This function has be overriden to do nothing to block system calls from changing the
/// content offset at undesireable times.
///
/// Instead call forceContentOffset(:animated:)
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
}
}
This works but you have to deal with reimplementing many of the scroll views behaviors and methods that you normally get for free. Since scrollRectToView and scrollToView both use setContentOffset you also have to reimplement these if you want them to work.

UIPickerView is not scrolling when added to UIScrollView!

I have added UIPickerView to the UIScrollView but now UPickerView is not scrolling. When I add it to the self.view it scrolls smoothly.
Here i my code
monthsArray = [[NSArray alloc] initWithObjects:#"Jan",#"Feb",#"Mar",#"Apr",#"May",#"Jun",#"Jul",#"Aug",#"Sep",#"Oct",#"Nov",#"Dec",nil];
UIPickerView *objPickerView = [[UIPickerView alloc] initWithFrame:CGRectMake(185,350,100,100)];
objPickerView.userInteractionEnabled=YES;
objPickerView.delegate = self;
objPickerView.showsSelectionIndicator = YES;
[objScrollView addSubView:objPickerView];
I have included the delegete and its methods. have a look on this issue. Thanks in advance.
If I am not clear please tell me.
I am using a subclass of UIPickerView for the same purpose, but mine is much simpler:
#implementation ScrollablePickerView
- (UIScrollView *)findScrollableSuperview {
UIView *parent = self.superview;
while ((nil != parent) && (![parent isKindOfClass:[UIScrollView class]])) {
parent = parent.superview;
}
UIScrollView* scrollView = (UIScrollView *)parent;
return scrollView;
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
UIScrollView* scrollView = [self findScrollableSuperview];
if (CGRectContainsPoint(self.bounds, point)) {
scrollView.canCancelContentTouches = NO;
scrollView.delaysContentTouches = NO;
} else {
scrollView.canCancelContentTouches = YES;
scrollView.delaysContentTouches = YES;
}
return [super hitTest:point withEvent:event];
}
#end
Works like a charm - at least for me.
From the UIScrollView class documentation:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result because touch events for the two objects can be mixed up and wrongly handled.
They don't mention UIPickerView there, but I wonder if it should have been added to that list. It shares in common with the others the characteristic of using touches to scroll things.
I guess this might solve your problem (be sure to check the comments too):
http://www.alexc.me/uiscrollview-and-uidatepicker/153/
Basically you have to set DelaysContentTouches and CanCancelContentTouches to NO on the scroll view, as it steals the touch events from the picker.
UIPickerView is not designed to be scrolled or moved at all. You should place some text field instead and show UIPickerView when user taps on it.
I had a similar issue with a UIPickerView nested within a UIScrollView and solved it by subclassing UIPickerView. Changes to DelaysContentTouches/CanCancelContentTouches didn't help, and the other "answers" here that basically say "don't do it" -- well, that's no answer at all! It can be done and you can get a picker to behave within a scrollView.
I answered the following question on subclassing UIPickerView, including some code at the end which may help you:
Responding to touchesBegan in UIPickerView instead of UIView
I've been running into a similar problem. I've got a UIPickerView that I turned horizontal by applying a transform ( which works pretty well), however inside of a scrollview, scrolling only works at the left hand side.
What I think is happening is that the UIPicker looks up it's parent chain to see if there are any gesture recognisers, and sets itself as a delegate, so it can disable gesture recognition further up the chain for touches within it's boundaries, and while the transform changes the visual boundaries, it doesn't change the frame, is it is looking for touches within it's original bounds.
If this isn't what is happening, you could use this to prevent the scroll view stealing your touches.
I'm think I'm going to switch to a custom UISCrollView instead of my transformed UIPickerView.
On iOS 11 and iOS 12 changing canCancelContentTouches and delaysContentTouches when UIScrollView is already been dragging doesn't help. So I did the hack by intercepting some touches before scrollView.panGestureRecognizer receives them.
Here is Swift 4 code. Maybe we can check if touch is inside picker more elegantly. But it works good.
public class TableView: UITableView {
/// Check if view is a picker's subview
private func isInPickerView(_ view: UIView) -> Bool {
var prev: UIView? = view
while prev != nil {
if prev is UIPickerView || prev is UIDatePicker {
return true
}
prev = prev?.superview
}
return false
}
// UITableView is already UIGestureRecognizerDelegate internally,
// so we just need to overwrite 1 method here
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
// we don't care about any recognizers except self.panGestureRecognizer
guard gestureRecognizer == panGestureRecognizer else {
return true
}
// if touch is inside picker - we don't pass this touch
// to panGestureRecognizer
let location = touch.location(in: self)
if let view = self.hitTest(location, with: nil), isInPickerView(view) {
return false
}
return true
}
}

UITableViewCell custom reorder control

Is there any way to change the image of the reorder control that is displayed when the UITableView is in edit mode? I have a UIImage that I’d like to display instead of the usual grey bars.
Do I have to subclass UITableViewCell to accomplish this?
I guess you're a long way past this by now, but this has come up in a new question.
See my answer here:
Change default icon for moving cells in UITableView
I recently ran across the need to change the image for the reorder control, because I subclassed UITableViewCell to provide my own custom table cell. As part of this effort, I changed the background of the cell to something other than the default color.
Everything works correctly, but when I put the UITableView into editing mode, the reorder control would appear, with a white background - instead of the background color I was using for the cell. This didn't look good, and I wanted the background to match.
During the course of various versions of iOS, the hierarchy of views in a UITableViewCell has changed. I've taken an approach that will traverse the entire set of views until it finds the UITableViewCellReorderControl private class. I believe this will work for iOS 5 and all subsequent iOS versions at the time of this answer. Please note that while the UITableViewCellReorderControl class itself is private, I am not using any private API's to find it.
First, here's the code to scan for the reorder control; I'm assuming that the text "Reorder" will be in the class name - which Apple could change in the future:
-(UIView *) findReorderView:(UIView *) view
{
UIView *reorderView = nil;
for (UIView *subview in view.subviews)
{
if ([[[subview class] description] rangeOfString:#"Reorder"].location != NSNotFound)
{
reorderView = subview;
break;
}
else
{
reorderView = [self findReorderView:subview];
if (reorderView != nil)
{
break;
}
}
}
return reorderView;
}
In your custom UITableViewCell subclass, you will override -(void) setEditing:animated: and find the reorder control here. If you try to find this control when the table is not in editing mode, the reorder control will not be in the view hierarchy for the cell:
-(void) setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (editing)
{
// find the reorder view here
// place the previous method either directly in your
// subclassed UITableViewCell, or in a category
// defined on UIView
UIView *reorderView = [self findReorderView:self];
if (reorderView)
{
// here, I am changing the background color to match my custom cell
// you may not want or need to do this
reorderView.backgroundColor = self.contentView.backgroundColor;
// now scan the reorder control's subviews for the reorder image
for (UIView *sv in reorderView.subviews)
{
if ([sv isKindOfClass:[UIImageView class]])
{
// and replace the image with one that you want
((UIImageView *)sv).image = [UIImage imageNamed:#"yourImage.png"];
// it may be necessary to properly size the image's frame
// for your new image - in my experience, this was necessary
// the upper left position of the UIImageView's frame
// does not seem to matter - the parent reorder control
// will center it properly for you
sv.frame = CGRectMake(0, 0, 48.0, 48.0);
}
}
}
}
}
Your mileage may vary; I hope this works for you.
Here is my Swift solution based on Rick Morgan's answer:
func adjustSize() {
// we're trying to leverage the existing reordering controls, however that means the table must be kept in editing mode,
// which shrinks the content area to less than full width to make room for editing controls
let cellBounds = bounds
let contentFrame = contentView.convert(contentView.bounds, to: self)
let leftPadding = contentFrame.minX - cellBounds.minX
let rightPadding = cellBounds.maxX - contentFrame.maxX
// adjust actual content so that it still covers the full length of the cell
contentLeadingEdge.constant = -leftPadding
// this should pull our custom reorder button in line with the system button
contentTrailingEdge.constant = -rightPadding
// make sure we can still see and interact with the content that overhangs
contentView.clipsToBounds = false
// recursive search of the view tree for a reorder control
func findReorderControl(_ view: UIView) -> UIView? {
// this is depending on a private API, retest on every new iPad OS version
if String(describing: type(of: view)).contains("Reorder") {
return view
}
for subview in view.subviews {
if let v = findReorderControl(subview) {
return v
}
}
return nil
}
// hunt down the system reorder button and make it invisible but still operable
findReorderControl(self)?.alpha = 0.05 // don't go too close to alpha 0, or it will be considered hidden
}
This worked pretty well. contentLeadingEdge and contentTrailingEdge are layout constraints I set up in Interface Builder between the contentView and the actual content. My code calls this adjustSize method from the tableView(_:, willDisplay:, forRowAt:) delegate method.
Ultimately, however, I went with Clifton's suggestion of just covering the reorder control. I added a UIImageView directly to the cell (not contentView) in awakeFromNib, positioned it, and when adjustSize is called I simply bring the image view to the front, and it covers the reorder control without having to depend on any private APIs.
I put a little work into this recently, but came up short. I tried setting my own editingAccesoryView but couldn't get the reorder control to change. Odd.
My guess is that it has something to do with the following comment in the UITableviewCell docs re: showsReorderControl:
If the value is YES , the reordering
control temporarily replaces any
accessory view.
In other words, the editingAccessoryView is being replaced by the reordering control view, which might be why we cannot override the reordering control. Hoping someone can find a workaround.
You set the cell's editingAccessoryView property to an image view containing the image you want.
As an aside, I would caution you to be careful when doing this. When you substitute a custom graphic for a system standard such as the reorder graphic, you run a serious risk of confusing the user. The standard UI grammar has told them to expect the standard graphic when reordering and they may not understand the significance of your custom graphic.
Maybe we're all overthinking this. :)
Just put a custom UIImageView over the top of the default move accessory so it covers it up. Done.

UIScrollView in UITableCellView, cell doesn't update

I have a custom UITableViewCell with a UIScrollView in it that is wired to the cell controller. When I assign text to the scrollview, some cells get the correct text, some are blank, some redisplay old text and others have the scrollview clipped around the 2nd or 3rd line of text. It seems random on what will happen. I followed the suggestion here of using a timer to fix blank cells, http://www.bdunagan.com/2008/12/08/uitextview-in-a-uitableview-on-the-iphone/, but that didn't help. I placed the timer code in the cellForRowAtIndexPath method.
I've also tried calling
[cell.textview setNeedsDisplay];
after text is assigned to the textview but it doesn't have any affect.
When I use a textfield or label, everything looks fine. However, I need something that can scroll text. Any suggestions on a fix or better way?
Update: Found this on the dev forums (specifically mentions your problem):
https://devforums.apple.com/message/38944#38944
I would follow the link it has some more detailed info.
// view controller
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSArray* visibleCells = [self.tableView visibleCells];
for (UITableViewCell* cell in visibleCells)
{
if ([cell.reuseIdentifier isEqualToString:kTextViewCellID])
{
[(MTextViewCell*)cell refresh];
}
}
}
// MTextViewCell
- (void)refresh
{
// mucking with the contentOffset causes the textView to redraw itself
CGPoint contentOffset = mTextView.contentOffset;
CGPoint contentOffset1 = { contentOffset.x, contentOffset.y + 1.0f };
mTextView.contentOffset = contentOffset1;
mTextView.contentOffset = contentOffset;
}
Try calling:
[tableView reloadData];
After you update all the textViews.