UICollectionView scroll lag - performance

I have setup a collection view with 10 subviews in a cell.
The subviews are
-imageview with label on it
-text view
-imageview
-uilabel
-imageview
-uilabel
Initially the collection view have 15 cells displayed at the time on an iPad. No when I scroll the scroll pauses when it is time to replace the cells at the bottom or top(reusing cells). I removed the shadow but still the same issue.
So the problem happens when the old cell is reused causing a lag.
Btw, no images loaded via the network.

I had the answer to this long time ago but for the benefit of others and who may be in the same issue.
Apart from removing shadows, you also need to remove "clear color" backgrounds. Any additional drawing that will require additional processing should be removed or replaced with an alternative. Any heavy lifting, text formatting, date formatting should be done before even showing the collection or table views. Make sure you cell only does the presenting and not processing. If you can't avoid it do the processing at another thread.
To measure the rate of the scroll you will need to use the instruments > graphics > core animation tool to measure the frame rate.
Try it and you will notice a difference.

EDIT: No need to experiment with autoresizing masks, just read this short article about UICollectionView performance boost http://noxytrux.github.io/blog/2014/09/25/ios8-weirdness-part3-laggy-uicollectionview/
It is probably an autolayout overhead. Consider trying autoresizing masks instead.
You can just commit everything and make an experiment:
turn off autolayout on your cell xib file
run an app to test performance (don't worry about messed up layout)
setup autoresizing masks (and do layout in code if needed) instead of autolayout if the effect is noticeable
I fixed my UICollectionView performance problems this way. It helps most when you have a lot of visible cells at a time.
Also, if you have image views, please see this answer Setting image property of UIImageView causes major lag
Don't forget about Instruments: run Time Profiler and see what eats your main thread time most.

I guess the issue can be also because of ImageView.
Using Kingfisher 5 for prefetching, loading, showing and caching images will solve the issue.
let url = URL(fileURLWithPath: path)
let provider = LocalFileImageDataProvider(fileURL: url)
imageView.kf.setImage(with: provider)
https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#image-from-local-file

fileprivate func downloadForPerformance()->void{
for i in urlToFetchImageArray {
var image: UIImage = dowloadImageFunc(link: i.urlString() );
imageArray.append( image );
}
}
fileprivate func dowloadImageFunc(link: String)->UIImage {
let url = URL(string: link)
let data = try? Data(contentsOf: url!)
return UIImage(data: data!) ?? UIImage(named: "default.png")!;
}
cell?.mainImage.image = self.imageArrayPerformance[indexPath.row];
Usually the problem is slow download as each cell is dequeue.
Call downloadForPerformance func before the view has appeared.
This will fill an array called imageArray as a global variable
Then use that array in cellForItem func
basically have an array of already downloaded images you will need, make this a [UIImage] array, then use theImageArray[indexPath.row]

Related

SCNView overlay causes tearing on resize

I'm using SceneKit to display a 3D scene (so far, a single quad), and the overlaySKScene to display a 2D overlay (which so far is just a SKNode with no geometry, though I had previously used a single SKLabelNode). It's a pretty simple view inside a bunch of nested NSSplitView. And during normal use, it works brilliantly. The problem comes when I try to resize the window or split view — I get areas of red leaking through my nice background, which disappear shortly after.
I'm running this on a 2016 MBP with a Radeon Pro 460, and captured this frame using Quicktime's screen capture:
Disabling the overlay removes the red areas, which makes me think that it's the problem. Disabling the statistics bar or the scroller (a child view of the SCNView) do not have any impact. My most minimal SKScene subclass is defined as
#implementation TestOverlay
- (instancetype) initWithSize: (CGSize) size
{
if( self = [super initWithSize: size] )
{
// Setup the default state
self.scaleMode = SKSceneScaleModeResizeFill;
self.userInteractionEnabled = NO;
self.backgroundColor = [NSColor blackColor];
}
return self;
}
#end
Has anybody run into similar issues before? Annoyingly, the apple sample Fox2 doesn't have similar problems...
For true enlightenment, one needs to read the documentation carefully, then comment everything out and restore functionality one step at a time. And then read the documentation again.
In the discussion section of -[SCNSceneRendererDelegate renderer:willRenderScene:atTime:], the solution is obvious (emphasis mine):
You should only execute Metal or OpenGL drawing commands (and any setup required to perform them) in this method—the results of modifying SceneKit objects during this method are undefined.
Which is exactly what I was doing. I had misread this as modifying geometry, so thought that assigning textures would be reasonable to do here (after all, "will" render means it hadn't started rendering yet, right?), and would therefore pick the most recently created texture. And unfortunately, before I decided that I needed an overlay, this actually works perfectly well! As soon as the overlay was added, however, the tearing appeared.
The correct place to update material properties seems to be -[SCNSceneRendererDelegate renderer:updateAtTime]. Use that to avoid silly bugs like this one folks!
Try to reset SMC (System Management Controller). It helped me for solving a similar problem but with Autodesk Maya 2018 on MBP 2017 (Radeon 560).
So, shut down and unplug your MBP.
On the built-in keyboard, press and hold the Shift-Option-Control keys on the left side and press the Power Button and hold all of these down for 10 seconds, then release the keys.
Connect the power adapter and then turn the Mac on normally.
Hope this helps.
P.S. In case it doesn't help, try to check/uncheck Automatic graphics switching option in System Preferences–Energy Saver to see if there's a difference.

How do I optimise Cocoa drawing to ensure smooth scrolling

I am drawing graphs on NSView using standard Cocoa drawing APIs. See image below. There are multiple graphs on the NSView which is in a scrollView. Each graph has around 1440 data points and scrolling performance struggles a bit because of redrawing that is being done.
Is there any way to ensure the graphics are only drawn once such that the image can be scrolled up and down smoothly ?
This same view is used to generate a PDF output file.
Given the drawing does not actually need to change unless the view is resized, and this does not happen, is there any way to prevent the view from redrawing itself during scrolling. Hopefully there is a simple switch to ensure the view draw itself once and keeps that in memory!?
The basic code is in the NSView subclass draw() function.
override func draw(_ dirtyRect: NSRect) {
drawAxis()
// Only draw the graph axis during live resize
if self.inLiveResize {
return
}
plot1()
plot2()
plot3()
...
}
func plot1(){
for value in plot1Data {
path = NSBezierPath()
if isFirst {
path?.move(to: value)
} else {
path?.line(to: value)
}
}
}
Apple has pretty comprehensive advice in their View Programming Guide: Optimizing View Drawing article.
It doesn't appear that you're taking even the minimal steps to avoid drawing what you don't have to. For example, what code you've shown doesn't check the dirtyRect to see if a given plot falls entirely outside of it and therefore doesn't need to be drawn.
As described in that article, though, you can often do even better using the getRectsBeingDrawn(_:count:) and/or needsToDraw(_:) methods.
The scroll view can, under some circumstances, save what's already been drawn so your view doesn't need to redraw it. See the release notes for Responsive Scrolling. One requirement of this, though, is that your view needs to be opaque. It needs to override isOpaque to return true. It's not enough to just claim to be opaque, though. Your view actually has to be opaque by drawing the entirety of the dirty rect on every call to draw(). You can fill the dirty rect with a background color before doing other drawing to satisfy this requirement.
Be sure the clip view's copiesOnScroll property is set to true, too. This can be done in IB (although it's presented as an attribute of the scroll view, there) or in code. It should be true by default.
Note that the overdraw that's part of Responsive Scrolling will happen incrementally during idle time. That will involve repeated calls to your view's draw() method. If you haven't optimized that to only draw the things that intersect with the dirty rect(s), then those calls are going to be slow/expensive. So, be sure to do that optimization.

NSScrollView Zooming of subviews

Apologies for the noob question - coming from an iOS background I'm struggling a little with OSX.
The good news - I have an NSScrollView with a large NSView as it's documentView. I have been adjusting the bounds of the contentView to effectively zoom in on the documentView - and all works well with respect to anything I do in drawRect (of the documentView)
The not so good news - I have now added another NSView as a child of the large documentView and expected it to simply zoom just like it would in iOS land - but it doesn't. If anyone can help fill in the rather large gap in my understanding of all this, I'd be extremely grateful
Thanks.
[UPDATE] Fixed it myself - 'problem' was that autolayout (layout constraints) were enabled. Once I disabled them and set the autosizing appropriately then everything was ok. I guess I should learn about layout constraints...
I know this is very old but I just implemented mouse scroll zooming using the following after spending days trying to figure it out using various solutions posted by others, all of which had fundamental issues. As background I and using a CALayers in a NSView subclass with a large PDF building layout in the background and 100+ draggable CALayer objects overplayed on top of that.
The zooming is instant and smooth and everything scales perfectly with no pixellation that I was expecting from something called 'magnification'. I wasted many days on this.
override func scrollWheel(with event: NSEvent) {
guard event.modifierFlags.contains(.option) else {
super.scrollWheel(with: event)
return
}
let dy = event.deltaY
if dy != 0.0 {
let magnification = self.scrollView.magnification + dy/30
let point = self.scrollView.contentView.convert(event.locationInWindow, from: nil)
self.scrollView.setMagnification(magnification, centeredAt: point)
}
}
LOL, I had exactly the same problem. I lost like two days messing around with autolayout. After I read your update I went in and just added another NSBox to the view and it gets drawn correctly and zooms as well.
Though, does it work for NSImageViews as subviews as well?

CATiledLayer blinks after zoom

I have UIScrollView with BIG subview with lots small CATiledLayers displaying images.
With zoomDidEnd I set new scale:
[tilesContainer setContentScaleFactor:scale];
cause redraw tilesContainer view with all it's layers with:
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
And I get perfect result: detailed images after zoom.
But there is a blink of all those layers (when loading new data).
Is it possible to hide it somehow?
I need to hold the old images in layers till load the new one.
Thanks a lot for any help!
subclass the CATiledLayer and return fadeDuration of 0 to disbale the "blink".
fadeDuration
The time, in seconds, that newly added images take to "fade-in" to the rendered representation of the tiled layer.
The default implementation returns 0.25 seconds.

Performance navigating through several full screen images on iPad

I need to let user navigate back and forward through different images (from 10 to 20) with a tap.
All the images are 1024x768 JPG's so I don't need to resize or transform them.
There are no animations between them (I'll switch them with a removeFromSuperView and addSubView).
All I want is avoid loading time or unresponding touch, so I actually was thinking about these possible solutions:
Load each image singularly on tap;
Load 3 images: previous, actual and next one;
Load an array or a uiviewimage with all the images and iterate through it;
I will avoid imageNamed and I'll use imageWithContentOfFile or imageWithData.
Which solution do you think is the best one?
Could solutions 1. and 3. bring some performance issue?
Method 1 will be good : iOS devices can load full screen images really fast. Especially if your images don't have alpha. It will depends on images but it takes around 0.05 seconds for a classic png. This means that users will not notice the waiting time if you have to change after a tap especially if you had a fade transition between images.
Things can get harder if user can scroll through images. In this case, I would advise to use UITableView. They behave like UISCrollView and they load/unload pages fastly and smoothly.
To get an horizontal table view, you can use this code which works perfectly : https://github.com/alekseyn/EasyTableView
If your upper limit for number of images is 20, just preload an array of UIImages, and set the UIImageView.image property on response to touch - don't worry about swapping views, reusing a single UIImageView will be fine.
I wouldn't worry about performance unless the upper limit rises much higher - if it does, a dynamic cache like option 2 would be a better choice, but more programming.
If you are concerned about performance in an iPad application destined for the app store, always remember to test on a first generation iPad, since there was a major performance jump after the original.
I have actually done this before. With large images, you are going to have to be careful with memory. I originally loaded all of my images into an NSArray but I experienced memory warning and crashes.
My implementation uses a UIScrollView with paging. I have to arrays, one contains all of the image names and the other is mutable and contains only a few UIImageViews. I record the current 'page' that the scroll view is on, and when I land on an image I ensure that the mutable array contains that image and two images on either side of it (and remove any other images from the array).
The problem with this implementation is that you keep having to read images from disk which will be slow on the main thread. Sooo when I initially create the UIImageViews I add a UIActivityIndicator to them. Then, I pass my array of UIImageViews to a method in the background that actually loads the UIImage and then makes the respective UIImageView set the image on the main thread like so:
// called before you try to load an image in a background thread
[imageView addObserver:self forKeyPath:#"image" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// called in the background thread after you load the image from disk
[imageView performSelectorOnMainThread:#selector(setImage:) withObject:fullImage waitUntilDone:NO];`
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
UIImageView *imageView = (UIImageView *)object;
[[imageView viewWithTag:1] removeFromSuperview]; // this is the activity indicator
[imageView removeObserver:self forKeyPath:#"image"];
}

Resources