I've got a UIScrollView with a child UIView (CATiledLayer) - then I have a bunch more child views of that view (some of which are UITextViews)
After zooming everything is all fuzzy.
I have read various articles on the subject and they all seem to indicate that I must handle scrollViewDidEndZooming then 'do some transform stuff, mess with the frame and tweak the content offset'. Please can someone put me out of my misery and explain how this is meant to work.
Thanks in advance...
I had a similar problem where I needed zooming with text. Mine wasn't using the CATiledLayer, so this may or may not work for you. I also wasn't using ARC, so if you are, you'll have to adjust that as well.
The solution I came up with was to set the UIScrollViewDelegate methods as follows this:
// Return the view that you want to zoom. My UIView is named contentView.
-(UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView {
return self.contentView;
}
// Recursively find all views that need scaled to prevent blurry text
-(NSArray*)findAllViewsToScale:(UIView*)parentView {
NSMutableArray* views = [[[NSMutableArray alloc] init] autorelease];
for(id view in parentView.subviews) {
// You will want to check for UITextView here. I only needed labels.
if([view isKindOfClass:[UILabel class]]) {
[views addObject:view];
} else if ([view respondsToSelector:#selector(subviews)]) {
[views addObjectsFromArray:[self findAllViewsToScale:view]];
}
}
return views;
}
// Scale views when finished zooming
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
CGFloat contentScale = scale * [UIScreen mainScreen].scale; // Handle retina
NSArray* labels = [self findAllViewsToScale:self.contentView];
for(UIView* view in labels) {
view.contentScaleFactor = contentScale;
}
}
I hit the same problem and the code above didn't work for my case. Then I followed the documentation:
If you intend to support zoom in your scroll view, the most common technique is to use a single subview that encompasses the entire contentSize of the scroll view and then add additional subviews to that view. This allows you to specify the single ‘collection’ content view as the view to zoom, and all its subviews will zoom according to its state.
"Adding Subviews" section of https://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/UIScrollView_pg/CreatingBasicScrollViews/CreatingBasicScrollViews.html
I simply created an empty view and added my CATiledLayer and all other subviews to that empty view. Then added that empty view as the only child view of the scroll view. And, it worked like a charm. Something along these lines:
- (void)viewDidLoad
{
_containerView = [[UIView alloc] init];
[_containerView addSubView:_yourTiledView];
for (UIView* subview in _yourSubviews) {
[_containerView addSubView:subview];
}
[_scrollView addSubView:_containerView];
}
-(UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView {
return _containerView;
}
Related
I am trying to implement a table view that allows the user to "show more" rows to toggle the full number of rows or a smaller number of rows. I have all the data upfront when the view is loaded, so I do not need to fetch any extra data.
I am able to get it working, but the problem I am having is that my table view does not smoothly animate when expanding or collapsing. What happens is that if the "show more" action is triggered, the size of the table is updated all at once to the full height of the table with all the data in it. Then the rows will animate.
Likewise when hiding rows, the table height will shrink all at once to the end height and then the rows will animate.
Here is a picture of what is happening. It just "jumps" to the full height and then it animates the rows. But what I would like to happen would be for it to smoothly expand the height of the table unveiling the data as it smoothly expands downward. I would like the opposite, where it smoothly slides upward when pressing "show less".
The way my app is laid out is as follows:
UIScrollView
UIStackView
UIViewController
UIViewController
UITableView <-- the section I am working on here
UIView
...
Basically, I have a scrollable list of sections of different data. Some sections are tables, some are just ad-hoc views arranged with AutoLayout, others have collection views or page view controllers or other types of views.
But this section is represented by the UITableView in the previous list.
#import "MyTableView.h"
#import "MyAutosizingTableView.h"
#interface MyTableView ()
#property (strong, nonatomic) UILabel *titleLabel;
#property (strong, nonatomic) MyAutosizingTableView *tableView;
#end
#implementation MyTableView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
[self.titleLabel setText:#"Details"];
self.numberOfRows = 3;
[self addSubview:self.titleLabel];
[self addSubview:self.tableView];
[NSLayoutConstraint activateConstraints:#[
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor],
[self.titleLabel.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor],
[self.tableView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.tableView.topAnchor constraintEqualToSystemSpacingBelowAnchor:self.titleLabel.bottomAnchor multiplier:1.0f],
[self.tableView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[self.tableView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.tableView.widthAnchor constraintEqualToAnchor:self.widthAnchor]
]];
}
- (UITableView *)tableView {
if (!self->_tableView) {
self->_tableView = [[MyAutosizingTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self->_tableView.translatesAutoresizingMaskIntoConstraints = NO;
self->_tableView.rowHeight = UITableViewAutomaticDimension;
self->_tableView.estimatedRowHeight = UITableViewAutomaticDimension;
self->_tableView.allowsSelection = NO;
self->_tableView.scrollEnabled = NO;
self->_tableView.delegate = self;
self->_tableView.dataSource = self;
[self->_tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:#"dataCell"];
}
return self->_tableView;
}
- (UILabel *)titleLabel {
if (!self->_titleLabel) {
self->_titleLabel = [[UILabel alloc] init];
self->_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self->_titleLabel.numberOfLines = 0;
self->_titleLabel.userInteractionEnabled = YES;
self->_titleLabel.textAlignment = NSTextAlignmentNatural;
self->_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self->_titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
self->_titleLabel.adjustsFontSizeToFitWidth = NO;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showMore)];
[self->_titleLabel addGestureRecognizer:tapGesture];
}
return self->_titleLabel;
}
- (void)showMore {
if (self.numberOfRows == 0) {
self.numberOfRows = 3;
} else {
self.numberOfRows = 0;
}
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:indexes withRowAnimation:UITableViewRowAnimationFade];
}
- (void)setData:(NSArray<NSString *> *)data {
self->_data = data;
[self.tableView reloadData];
}
- (void)didMoveToSuperview {
//this has no effect unless the view is already in the view hierarchy
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.numberOfRows <= 0) {
return self.data.count;
}
return MIN(self.data.count, self.numberOfRows);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *data = self.data[indexPath.row];
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"dataCell" forIndexPath:indexPath];
cell.data = data;
return cell;
}
#end
I have a UITableView subclass that I used in the previous code. The reason I have the table view subclass is to get the table to resize when the content changes. If anyone knows a better way to do this without the subclass, I would be very interested in learning that too.
#import "MyAutosizingTableView.h"
#implementation MyAutosizingTableView
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
#end
Does anyone know what I can do to get smooth expansion animations? I would really like the table to expand smoothly and reveal the data that is in the new rows.
Update - a high level overview of what I am trying to do
Here I am going to try to describe what it is I am trying to accomplish and how I am going about it, so if anyone has any alternative approaches or improvements for laying this out, I would be very open to suggestions. Thank you in advance for reading this.
So I am working on a view controller that displays data about a product, imagine a car for example. The view controller has different sections responsible for displaying different data. For example, the first section displays pictures of the item and for this section, I am using a UIPageView that displays basically a carousel of product images.
Another section displays options about the current product. For example, someone might choose a different color for the car or different wheels. For this section, I am using a table view to display the list of available attributes, where each attribute is in a different section of the table. Upon pressing the section header, the table view adds a row that displays the available options for that attribute. For instance, assume the car has different colors available (red, green, blue, and yellow). Upon pressing the section header, the table view adds a row and animates that in (using the technique discussed in chapter 8 of "Programming iOS 12"). The row that is shown then contains a UICollectionView that scrolls horizontally allowing the user to choose between the colors (or wheel options or whatever attribute is being changed).
Another section displays what other customers have said about this product. For example, if someone leaves a writeup on the car, then that would go in this section. This section is has a table showing how the product fares across different criteria. Using the car example still, it might have a row for comfort and another for gas mileage and another for performance and so on. Then there is also a horizontal UICollectionView displaying the write ups about the product.
Yet another section is a list of attributes for the product. For example, with the car it might show engine size on the left and V12 on the right.
And there are other sections as well, but then to tie them all together I have a scroll view with a vertical UIStackView inside it. And this is what I am really curious about. What is the best way to display or layout all these sections? Is a stack view the way to go or should I use a table view or should I just have a scroll view with these views hooked together just using AutoLayout directly or is there some better way to do it than all of these?
One thing that is very important is that each section can be different sizes between products. For example, some sections might be one height for one product but then a completely different height for other products. So I need it to be able to dynamically size each section and I also need to be able to animate some size changes (for example, the section where it adds a row to the table and then removes it, that needs to animate and make the whole section larger in an animated fashion).
Also, I would like each section to be modular and to not end up having a giant view controller that manages everything. I would like each section to essentially be self contained. I just pass data to that section and then it handles everything related to layout and interaction. But again, the real issue is getting those views to be able to resize and to have that update in the parent view (stack view currently).
But I am really new to iOS development, and while I have been trying to learn as fast as I can, I am still not sure if the way that I am doing this currently (the scroll view with the stack view inside it) is the best approach or if there are better approaches. I tried this initially in a table view with static cells where each row was a different section. But then getting the child view controllers to resize the row in the parent table view was not working that well. Since then I have also changed from making each section be a separate view controller and just have each section be a view (rather than view controller) to make resizing hopefully easier, but I am still running into issues. I have also considered collection views, but I also need to be able to support iOS 12 (or I could drop iOS 12 support if I really need to and just support iOS 13+).
But to restate, my overall goals are to have some way to layout this interface which consists of different views that each handle different data. I would like each view to be able to resize and for that to be able to be smooth. And I would like each section of the interface to be modular so as to avoid one large view controller.
Update 2
After following the suggestion to wrap the table view in a UIView and then set the two bottom constraints with different priorities, this is what I am getting. I also get this effect anytime I try to animate things like the height constraint of views. I think this is because the overall view here is contained inside a stack view, so I think the stack view is doing something when its arranged subview changes size.
The picture here shows both expanding the table view and collapsing it. When it is collapsed it has three rows visible and when it is expanded it has five rows.
The yellow and blue views are representative of other views on the page. They are not part of this view and neither is the stack view they are contained within. They are just other parts of the page included here to show this issue. All the views here are contained in a stack view and I think that is what is causing the issue with the animation, but I am not sure how to fix it.
You really have a bunch of questions here - but to address specifically the issue with trying to expand/collapse your table view...
I think you'll be fighting a losing battle with this approach. By trying to use an "auto-sizing" table view, you're counter-acting much of a table view's behaviors/ And, since you are not getting the benefit of memory management with reusable cells, you're probably better off formatting your "spec list" with a vertical stack view.
In either case, to create a collapse / expand animation with a "reveal / hide" effect, you may want to embed the tableView or stackView in a "container" view, and then toggle constraint priorities for the height (the bottom) of the container.
Here is a quick example...
create a "container" UIView
add a vertical stack view to the container
add labels to the stack view
add a red UIView to place above the container
add a blue UIView to place below the container
define constraints for the height of the container
On each tap, the container will expand / collapse to reveal / hide the labels.
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
#interface ViewController ()
#property (strong, nonatomic) NSLayoutConstraint *collapsedConstraint;
#property (strong, nonatomic) NSLayoutConstraint *expandedConstraint;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIStackView *labelsStackView = [UIStackView new];
labelsStackView.axis = UILayoutConstraintAxisVertical;
labelsStackView.spacing = 4;
// let's add 12 labels to the stack view (12 "rows")
for (int i = 1; i < 12; i++) {
UILabel *v = [UILabel new];
v.text = [NSString stringWithFormat:#"Label %d", i];
[labelsStackView addArrangedSubview:v];
}
// add a view to show above the stack view
UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
// add a view to show below the stack view
UIView *blueView = [UIView new];
blueView.backgroundColor = [UIColor blueColor];
// add a "container" view for the stack view
UIView *cView = [UIView new];
// clip the container's subviews
cView.clipsToBounds = YES;
for (UIView *v in #[redView, cView, blueView]) {
v.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:v];
}
// add the stackView to the container
labelsStackView.translatesAutoresizingMaskIntoConstraints = NO;
[cView addSubview:labelsStackView];
// constraints
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// when expanded, we'll use the full height of the stack view
_expandedConstraint = [cView.bottomAnchor constraintEqualToAnchor:labelsStackView.bottomAnchor];
// when collapsed, we'll use the bottom of the 3rd label in the stack view
UILabel *v = labelsStackView.arrangedSubviews[2];
_collapsedConstraint = [cView.bottomAnchor constraintEqualToAnchor:v.bottomAnchor];
// start collapsed
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:#[
// redView Top / Leading / Trailing / Height=120
[redView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
[redView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[redView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[redView.heightAnchor constraintEqualToConstant:120.0],
// container Top==redView.bottom / Leading / Trailing / no height
[cView.topAnchor constraintEqualToAnchor:redView.bottomAnchor constant:0.0],
[cView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[cView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
// blueView Top==stackView.bottom / Leading / Trailing / Height=160
[blueView.topAnchor constraintEqualToAnchor:cView.bottomAnchor constant:0.0],
[blueView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[blueView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[blueView.heightAnchor constraintEqualToConstant:160.0],
// stackView Top / Leading / Trailing
[labelsStackView.topAnchor constraintEqualToAnchor:cView.topAnchor],
[labelsStackView.leadingAnchor constraintEqualToAnchor:cView.leadingAnchor],
[labelsStackView.trailingAnchor constraintEqualToAnchor:cView.trailingAnchor],
_expandedConstraint,
_collapsedConstraint,
]];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// toggle priority between expanded / collapsed constraints
if (_expandedConstraint.priority == UILayoutPriorityDefaultHigh) {
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
} else {
_collapsedConstraint.priority = UILayoutPriorityDefaultLow;
_expandedConstraint.priority = UILayoutPriorityDefaultHigh;
}
// animate the change
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}];
}
#end
You may be able to implement that with your "auto-sizing" table view, but again, since you're not using the "normal" table view behaviors (scrolling, reusable cells, etc), this might be a better approach.
Hopefully, it can also give you some insight to sizing other views in your layout - particularly if you want to animate them in or out of view.
Edit - added a more complex example here: https://github.com/DonMag/Scratch2021
It uses a vertical stack view in a vertical scroll view as the "main view" UI, adding child view controllers as "components" in the stack view.
I am having a (UIView and UICollectionView in a UIScrollView) as 1stVC. I have to scroll both at the same time for which I already unable the collection view scrolling. So first time When I launch that screen I am able to scroll the whole view, But when I push to (next ViewController) 2ndVC and then press Back Button to 1stVC my scroll view is not preforming it got Freeze.
Tried this Method:-
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
dispatch_async(dispatch_get_main_queue(), ^ {
CGRect contentRect = CGRectZero;
for(UIView *view in self.scrollView.subviews)
contentRect = CGRectUnion(contentRect,view.frame);
self.scrollView.contentSize = contentRect.size;
});
}
Tried this Method:-
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[_scrollView setContentSize:CGSizeMake(_scrollView.frame.size.width, _scrollView.frame.size.height)];
}
And For Getting Dynamic Height of the Collection view I have Done This
CGFloat height = _collectionCompass.contentSize.height;
Which I Have provided to ScollView ContentSize.
_scrollView.contentSize = CGSizeMake(_scrollView.frame.size.width, height);
Please help me.
What I really suggest it's to remove scroll view and do all stuff in collection view, since with scroll view you're missing some of benefits (e.g. cells reusing). Also collection view in scroll view it's a bad practice.
From Apple style guide:
Don’t place a scroll view inside of another scroll view. Doing so creates an unpredictable interface that’s difficult to control.
If your UI as list, you can add view as header of collection view or as another cell. With this approach you can remove the code from viewDidLayoutSubviews
I have a viewController that holds WKWebView in his properties.
I made my viewController to implement UIScrollViewDelegate and in 'didScroll' put some code that wont let the scrollView scroll vertically.
like that:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 0);
}
The disabling part is good I tried it on other scrollView and it works just fine.
I set the webView.scrollView delegate to be the viewController.
I also made a method that loop over all of the subViews of webView and if it comes across another scrollView it set the delegate of that scrollView to be the viewController.
After all that it still not working...
-(void)setDelegateForScrollViewsInView:(UIView *)view{
if (view.subviews.count > 0) {
for (UIView *subView in view.subviews){
if ([subView isKindOfClass:[UIScrollView class]]) {
[((UIScrollView *)subView) setDelegate:self];
[((UIScrollView *)subView) setShowsHorizontalScrollIndicator:false];
[((UIScrollView *)subView) setBackgroundColor:UIColor.blueColor];
}
[self setDelegateForScrollViewsInView:subView];
}
}I }
After View Debugger i found that my webView is build up in that hirarchy:
And my method cant catches the inside scrollView so the disabling of the vertical scroll won't work.
How can I fix it?
I have a UICollectionView with a grid of images. When you tap on one, it opens up the grid and shows a subview with some details. Like this:
I open up the grid in my UICollectionViewLayout by adjusting the UICollectionViewLayoutAttributes and setting a translation on the transform3D property for all cells below the current row of the selected item. This works really nicely, and is a much better animation and a simpler approach than my first attempt at inserting another cell into the grid which is a different size to the others.
Anyway... it works most of the time, but then after continued use I see old images on the collection view. They are like ghost cells. I can't click them, it's like they haven't been removed from the collection view properly, and they sit on top of the cells preventing taps and just being a nuisance. Like this:
Any ideas why these cells are doing this?
EDIT:
I'd like to add, I think it only happens when i scroll the collection view really fast. I've written my own UICollectionViewFlowLayout replacement to test if it still happens. It does.
EDIT 2:
The 3d transforms or layout have nothing to do with this. It must be a bug in UICollectionView. I can exploit by just scrolling really fast, letting come to a standstill and then querying the views that are on screen. There are often double the number of cells, but they are hidden as they are stacked on top of each other. My implementation above reveals them because of the translation i do.
This can really hurt performance too.
See my answer for a solution.
My second edit of my question details why this is happenening, and here is my workaround. It's not bullet proof, but it works in my case, and if you experience somethign similar you could tweak my solution:
- (void) removeNaughtyLingeringCells {
// 1. Find the visible cells
NSArray *visibleCells = self.collectionView.visibleCells;
//NSLog(#"We have %i visible cells", visibleCells.count);
// 2. Find the visible rect of the collection view on screen now
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
//NSLog(#"Rect %#", NSStringFromCGRect(visibleRect));
// 3. Find the subviews that shouldn't be there and remove them
//NSLog(#"We have %i subviews", self.collectionView.subviews.count);
for (UIView *aView in [self.collectionView subviews]) {
if ([aView isKindOfClass:UICollectionViewCell.class]) {
CGPoint origin = aView.frame.origin;
if(CGRectContainsPoint(visibleRect, origin)) {
if (![visibleCells containsObject:aView]) {
[aView removeFromSuperview];
}
}
}
}
//NSLog(#"%i views shouldn't be there", viewsShouldntBeThere.count);
// 4. Refresh the collection view display
[self.collectionView setNeedsDisplay];
}
and
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self removeNaughtyLingeringCells];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self removeNaughtyLingeringCells];
}
A quick further comment to bandejapaisa's: under iOS 6 only, I found that UICollectionView also had a habit of bungling animated transitions. The original cells would remain where they were, copies would be made and then the copies would be animated. Usually on top of the originals but not always. So a simple bounds test wasn't sufficient.
I therefore wrote a custom subclass of UICollectionView that does the following:
- (void)didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
//
// iOS 6 contains a bug whereby it fails to remove subviews, ever as far as I can make out.
// This is a workaround for that. So, if this is iOS 6...
//
if(![UIViewController instancesRespondToSelector:#selector(automaticallyAdjustsScrollViewInsets)])
{
// ... then we'll want to wait until visibleCells has definitely been updated ...
dispatch_async(dispatch_get_main_queue(),
^{
// ... then we'll manually remove anything that's a sub of UICollectionViewCell
// and isn't currently listed as a visible cell
NSArray *visibleCells = self.visibleCells;
for(UIView *view in self.subviews)
{
if([view isKindOfClass:[UICollectionViewCell class]] && ![visibleCells containsObject:view])
[view removeFromSuperview];
}
});
}
}
Obviously it's a shame that 'is this iOS 6' test can't be a little more direct but it's hidden off in a category in my actual code.
A Swift UICollectionView extension version of bandejapaisa's answer:
extension UICollectionView {
func removeNaughtyLingeringCells() {
// 1. Find the visible cells
let visibleCells = self.visibleCells()
//NSLog("We have %i visible cells", visibleCells.count)
// 2. Find the visible rect of the collection view on screen now
let visibleRect = CGRectOffset(bounds, contentOffset.x, contentOffset.y)
//NSLog("Rect %#", NSStringFromCGRect(visibleRect))
// 3. Find the subviews that shouldn't be there and remove them
//NSLog("We have %i subviews", subviews.count)
for aView in subviews {
if let aCollectionViewCell = aView as? UICollectionViewCell {
let origin = aView.frame.origin
if (CGRectContainsPoint(visibleRect, origin)) {
if (!visibleCells.contains(aCollectionViewCell)) {
aView.removeFromSuperview()
}
}
}
}
// 4. Refresh the collection view display
setNeedsDisplay()
}
}
I have a fairly simple view configuration:
A UIViewController, with a child UIScrollView and a UIImageView in this UIScrollView.
I set the UIImageView with a height sufficient to break out of the visible area (ie. higher to 1024pt), and set the Bottom space to superview constraint of my UIImageView to a fixed positive value (20 for example).
The whole setup works as expected, the image scrolls nicely in its parent.
Except when the view is scrolled (the effect is more visible if you scrolled to the bottom of the view), then disappear, and appear again (you switched to another view and came back) the scrolling value is restored, but the content of the scroll view is moved to the outside top part of its parent view.
This is not simple to explain, I'll try to draw it:
If you want to test/view the source (or the storyboard, I did not edit a single line of code). I put a little demo on my github: https://github.com/guillaume-algis/iOSAutoLayoutScrollView
I did read the iOS 6 changelog and the explanation on this particular topic, and think this is the correct implementation of the second option (pure auto layout), but in this case why is the UIScrollView behaving so erratically ? Am I missing something ?
EDIT: This is the exact same issue as #12580434 uiscrollview-autolayout-issue. The answers are just workarounds, as anyone found a proper way to fix this or is this a iOS bug ?
EDIT 2: I found another workaround, which keep the scroll position in the same state the user left it (this is an improvement over 12580434's accepted answer):
#interface GAViewController ()
#property CGPoint tempContentOffset;
#end
#implementation GAViewController
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.tempContentOffset = self.mainScrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.scrollView.contentOffset = self.tempContentOffset;
}
This basically save the offset in viewWillAppear, reset it to the origin, and then restore the value in viewDidAppear. The problem seems to occur between these two calls, but I can't find its origin.
Yeah, something strange happened with UIScrollView in pure autolayout environment. Re-reading the iOS SDK 6.0 release notes for the twentieth time I found that:
Note that you can make a subview of the scroll view appear to float (not scroll) over the other scrolling content by creating constraints between the view and a view outside the scroll view’s subtree, such as the scroll view’s superview.
Solution
Connect your subview to the outer view. In another words, to the view in which scrollview is embedded.
As IB does not allow us set up constraints between the imageView and a view outside the scroll view’s subtree, such as the scroll view’s superview then I've done it in code.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.view removeConstraints:[self.view constraints]];
[self.scrollView removeConstraints:[self.scrollView constraints]];
[self.imageView removeConstraints:[self.imageView constraints]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_scrollView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_scrollView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[_imageView(700)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_imageView(1500)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_imageView)]];
}
And vau! It works!
The edit didn't work for me. But this worked:
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.tempContentOffset = self.scrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.scrollView.contentOffset = self.tempContentOffset;
}
For me I went to the IB clicked my view controller that contains the scroll view. Then I went to Attribute Inspector -> View Controller -> Extend Edges -> Uncheck "Under Top Bars" and "Under Bottom Bars".
Simple solution found, Just put
[self setAutomaticallyAdjustsScrollViewInsets:NO];
in your ViewControllers viewDidLoad method
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[self setAutomaticallyAdjustsScrollViewInsets:NO];
}
I had a similar problem using a UIScrollView in a UIViewController with a top extra space that was not visible in Storyboard. The solution for me was to uncheck the "Adjust Scroll View Insets" on the ViewController storyboard properties : see answer Extra Space (inset) in Scroll View at run time
Add a global property contentOffset and save the current contentOffset in viewDidDisappear.
Once you return the method viewDidLayoutSubviews will be called and you can set your original contentOffset.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.scrollView setContentOffset:self.contentOffset animated:FALSE];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.contentOffset = self.scrollView.contentOffset;
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:FALSE];
}
Looks like the problem solved with the dispatch_async during the viewWillAppear:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGPoint originalContentOffset = self.scrollView.contentOffset;
self.scrollView.contentOffset = CGPointZero;
dispatch_async(dispatch_get_main_queue(), ^{
self.scrollView.contentOffset = originalContentOffset;
});
}