Monday, May 29, 2017

Scrolling gets “stuck” when using nested scroll views

Leave a Comment

Problem description:

I have one iOS project for browsing images with nested UIScrollViews which is inspired by famous Apple's PhotoScroller. The problem is what sometimes scrolling just "stuck" when image is zoomed width- or height-wise. Here is an example of how it looks on iPhone 4s for image of size 935x1400 zoomed height-wise:

(I start dragging to left, but scroll view immediatly discard this action and image get "stuck")

Scroll problem

Workaround:

I found kind of workaround by adjusting content size of inner scroll view to nearest integer after zooming:

// Inside ImageScrollView.m  - (void)setZoomScale:(CGFloat)zoomScale {     [super setZoomScale:zoomScale];     [self fixContentSizeForScrollingIfNecessary]; }  - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated {     [super zoomToRect:rect animated:animated];     [self fixContentSizeForScrollingIfNecessary]; }  - (void)fixContentSizeForScrollingIfNecessary {     if (SYSTEM_VERSION_LESS_THAN(@"10.2"))     {         CGSize content = self.contentSize;         content.width = rint(content.width);         content.height = rint(content.height);         self.contentSize = content;     } } 

But this fix not perfect - some images now are shown with one-pixel wide stripes on sides. For example, on iPhone 6 for image of size 690x14300 it shows this at the bottom:

iPhone 6

Also, oddly enough, I'm able to reproduce this problem on iOS 7.0 - 10.1, but everything works correctly on iOS 10.2 and greater.

Question:

So, what I am doing wrong? Can my fix be improved?

Test Project:

I created simple test project to illustrate described problem - NestedScrollingProblems. Please note what my version of ImageScrollView is slightly different from Apple's one because I applied another rules for zooming. Also, workaround is commented out by default. (project code is a bit messy, sorry about that)

1 Answers

Answers 1

Can't comment on posts (not enough reps yet).

But by the looks of it (Apple's Docs) this project deinits images on scroll, then re-inits them when they are going to be loaded (see line 350 in UIScrollView.m). And also I have noticed a comment inside of the ImageScrollView.m (line 346) that explicitly says that this class is designed to avoid caching. Which is a practical way for a demo, but not for production, or real-world application that have ui-loading speed in mind like what you want to.

I also noticed that your app has to scroll much further to engage the pagination.. which is either some error in the code, or it might be the lag itself that hangs the main thread from running the pagination fluidly. Or if you intended to have such a wide threshold for pagination.. i'd recomend reducing it for better user experience since modern smartphones has screens much wider than that of the iPhone 4S.

To address this,

I found this post (bellow) on SO that seems to have a pretty decent obj-c method for caching, and fetching image data from such a cache post app-launch. You should be able to work it into post-launch methods pretty simply as well, or even use it with networking to download images from the web. You'd just have to make sure that your UIImage views are properly linked to the url strings you use, either through a set of custom string variables for each image view, or by subclassing UImageView into a custom class, and adding the cache method into it to make your code look simpler. Here's the method and NSCahe class from that post from iOSfleer

NSCache Class:

@interface Sample : NSObject  + (Sample*)sharedInstance;  // set - (void)cacheImage:(UIImage*)image forKey:(NSString*)key; // get - (UIImage*)getCachedImageForKey:(NSString*)key;  @end  #import "Sample.h"  static Sample *sharedInstance;  @interface Sample () @property (nonatomic, strong) NSCache *imageCache; @end  @implementation Sample  + (Sample*)sharedInstance {     static dispatch_once_t onceToken;     dispatch_once(&onceToken, ^{         sharedInstance = [[Sample alloc] init];     });     return sharedInstance; } - (instancetype)init {     self = [super init];     if (self) {         self.imageCache = [[NSCache alloc] init];     }     return self; }  - (void)cacheImage:(UIImage*)image forKey:(NSString*)key {     [self.imageCache setObject:image forKey:key]; }  - (UIImage*)getCachedImageForKey:(NSString*)key {     return [self.imageCache objectForKey:key]; } 

And so as to not change too much of what you've made, it seems that by changing the displayImageWithInfo method inside of ImageScrollview.m to the following one (using the caching method), it seems to work better after initial load. I'd also go a step further if I were you, and implement a loop-style method in the controller's viewDidLoad method to cache those images right away for faster loading at launch. But that's up to you.

- (void)displayImageWithInfo:(ImageItem*)imageInfo {     CGSize imageSize = (CGSize){.width = imageInfo.width, .height = imageInfo.height};      // clear the previous imageView     [self.imageView removeFromSuperview];     self.imageView = nil;      // reset our zoomScale to 1.0 before doing any further calculations     self.zoomScale = 1.0;      self.imageView = [[UIImageView alloc] initWithFrame:(CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size = imageSize}];      UIImage *image = [[Sample sharedInstance] getCachedImageForKey:imageInfo.path];     if(image)     {         NSLog(@"This is cached");         ((UIImageView*)self.imageView).image = image;     }     else{          NSURL *imageURL = [NSURL URLWithString:imageInfo.path];         UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];          if(image)         {             NSLog(@"Caching ....");             [[Sample sharedInstance] cacheImage:image forKey:imageInfo.path];             ((UIImageView*)self.imageView).image = image;         }      }       [self addSubview:self.imageView];      [self configureForImageSize:imageSize]; } 

I would also recomend working around this without removing views from their superview on scroll.. the adding of views is a very heavy task. And coupled with image loading, can be horrendously heavy for a small cpu like the ones on smartphones (since they don't have GPU's.. yet). To emphasize this, Apple even mentions that it does not re-render UIImages once they are displayed, the wording is subtle here, but it clearly does not mention optimized removing then re-adding and rendering views after they have been displayed once (such as is it in this case). I think the intended use here is to display the ImageView, and simply change it's image element afterwards after the controller is displayed.

Although image objects support all platform-native image formats, it is recommended that you use PNG or JPEG files for most images in your app. Image objects are optimized for reading and displaying both formats, and those formats offer better performance than most other image formats.

This is why views are usually added/initialized on their super view before any of the visible loading methods like viewWillAppear and viewDidAppear, or if it is done post-initial load they are rarely de-initialized, their content is often the only thing altered and even then it is usually done asynchronously (if downloading from the web), or it is done from a cache which can also be done automatically with some initializers (you can add this to what I am recommending):

Use the imageNamed:inBundle:compatibleWithTraitCollection: method (or the imageNamed: method) to create an image from an image asset or image file located in your app’s main bundle (or some other known bundle). Because these methods cache the image data automatically, they are especially recommended for images that you use frequently.

On a personnal note, I would try to take the approach of UICollectionViews. Notably, they have delegates which handle the caching of content automatically when views scroll out of the window (which is exactly what this demo is). You can add custom code to those methods too to better control the scrolling effect on those views as well. They might be a bit tricky to understand at first, but I can attest that what you are trying to accomplish here can be replicated with a fraction of the code this demo uses. I'd also take the fact that this demo was built in 2012 as a hint.. it is a very old demo and UICollectionViews appeared at the time this demo was last updated. So i'd say that this is what Apple is has been aiming for ever since because all content-oriented UIView subclasses have some kind of inheritance from UIScrollView anyways (UICollectionView, UITableView, UITextView, etc.). Worth a look! UICollectionViews.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment