Smooth animation using drawRect: and UIPanGestureRecognizer - ios

Ok. I HAVE seen a LOT of topics which are similar to what I'm trying to do here, but nothing that quite matches. Maybe my approach is way off base. Here's the deal.
I'm drawing a chart to my UIView subclass in drawRect:. I then use a UIPanGestureRecognizer to translate the chart's offset (internal, not in the view hierarchy) to the right/left. This works great, but my problem is that the recognizer's action gets fired with relatively low frequency. Specifically, the resultant panning is MUCH choppier than, say, in a scrollview or tableview. Particularly when the number of points in my chart goes up. Now, what I'd LIKE to do is not actually trigger drawing from the pan gesture's action, but have a timer firing at something like 30 times per second, interpolating the offset from the moment the pan fired to the current moment. However, this is getting more and more complicated.
Anybody have suggestions? Should I really just be doing this with a UIScrollView? The reason I'd prefer not to is that the data may extend quite far in either direction, and I'd like to not have to draw (or at least store) the whole path at once, but just do the section which is currently displayed. Should I be using CAShapeLayer? I'd like some input before I spend another day playing around with these possibilities...
Any input / experiences would by most welcome.

So, in the end, I DID end up putting these things in a UIScrollView. I had to do quite a bit of work to get this up and running, the complicated part being the creation of a series of PANES which would be added to the scrollview. Each pane asks a delegate for the data it needs to draw, and then they're all stitched together. Fortunately, everything worked great once I put these scroll views containing chart panes into a table view. Still haven't tackled scaling, but I've seen a few posts on here which should make it relatively trivial.
So that's the answer to my original question. DON'T try to pan and draw on each frame, it WILL be choppy, and baring some real hack, there's no good way I could find to smooth it out. Scroll view handles all these problems for you, as long as you can split your view up nicely. I guess I could have used one long view, but then when the data set gets large I'm in trouble.
Now I've got a related problem though -- when offscreen charts scroll into view (they're in UITableViewCells), there's a big hiccup as the new chart gets drawn. I think I will try drawing them into an image asynchronously, so that when the new row scrolls into view, I've already got the image.

Related

How to pass touch event to another view under tableview?

I have a scenario where I overlapped a view(pageViewController inside the view) with a uitableview with an offset of 130, and that uitableview is Transparent, only cells will be scrolled and visible.
My requirement is within this offset place of 130, I want to pass touch events to the view(pageViewController) which is below the uitableview and that view is not a superview(It is a sibling view) of table view but I overlapped it with tableview.
So is it possible to pass touch (Horizontal Swiping Event) to the view which is below the uitableview in the transparent area (Offset Area of 130); if yes, what I have to do for this?
There are lots of options for controlling how touch events and scrolling interact in iOS. You can override hitTest, you can use delegate messages sent to gesture recognizers to say which combinations of recognizers should fire when, etc. Without seeing your problem in detail it's hard to say which is best... and even then, you might say it's partly a matter of style. (At first blush, though, I'd guess that overriding hitTest to return different views at different times might be it.)
I'd recommend watching the WWDC 2014 video on Advanced Scrollviews and Touch Handling Techniques. That talk works through solving some problem that sound almost exactly like yours, so you'll almost certainly find a good solution in there. And lots of other tips that might be helpful elsewhere in your app. (Actually, if you have the time, I'd recommend watching the UIScrollView-themed sessions from the past several years' WWDCs... the so-called "Josh & Eliza Show" has always been full of cool demos and useful insights.)

IOS optimization using time profiler and Core Animation

So I have an app, in which I use custom UIView. There are other UIView, which are subviews of that view. All of them have pan gesture recognizers, so that the user can pan them in the specific fashion. When I test my app using one such view all is well and good, it is all smooth and fps is around 55-60. However, when I add the second one(somewhat smaller then the first one, and kinda inside first one, but not as a subview). The FPS of the outer views drops to 20-30, while the inner one(which is smaller) works in 60 fps. This two views are ABSOLUTELY independent, meaning that interacting with one doesn't affect the other one. In addition to that, I checked running time on the interval of one second, the results are the following:
For 1 view: 153ms
For 2 views 145ms
For 3 views 150ms
The calculations that are performed on each view are ABSOLUTELY identical in complexity. Why does one work smoothly, but when I add the second one it starts to lag, while the execution time stays the same ? I would appreciate if somebody could hint me at how I should debug it.
The most consuming operation according to the Time Profiler is repositioning one of the views:
view.center = newCenter
I simply can't understand why FPS drops 2-3 times, while the running time stays the same. What should I look for ?
UPDATE:
So I found the source of the problem, when one view is inside the frame of the other the whole thing is being constantly redrawn, even though no changes were made to the layout of the inner view. So when I positioned a few of them in different parts of the screen everything was fine, but when I put one "inside" the other things get messed up. Inner one works quickly, while the outer one lags. "Flash updated regions" flag in core animation show that when I interact with outer view, inner view gets updated as well. Any suggestions on what I might need to do, because I need that layout when they are one inside each other. So how can I prevent(lock) other view from redrawing ?
Thank You.

Too many UIViews? What's the cost, what are the tradeoffs?

EDIT:
Imagine that I'm in the business of reimplementing CoreText.
I'd get an NSString here which is an ordered list of code-points, and a set of glyphs there (an NSFont/CTFont) that basically is a list of CGPathRef.
Something in-between is in charge of the layout.
And some kind of environment must provide a CGGraphicContext to render into.
The kind of objects I have to display are CGPath. I am worried of creating one UIView for each CGPath! For a block of text, you wouldn't create an UIView for each character-glyph, would you?
I have a potentially large set of (tiny) objects to draw at once. In the 1000's. Many will be offscreen until scrolled into view.
One one hand it looks like my best bet would be to create one UIView for each one. The reasons are that:
with proper tiling I can make sure that the views that are offscreen
are not even in memory most of the time and brought up to life when
required
more importantly, when zooming I want to set each such view's layer contentScale to the zoomFactor such that I still get the full
precision and not the blurry stair-case
On the other hand I have no idea how the (otherwise perfectly generic) parent view will behave with 1000's of children.
So I am tempted to rethink my code such that the "children" just become an area of the single view.
But then I start thinking of setting that view's layer contentScale and I'm worried about exhausting memory just while zooming: I do not know if the layer machinery is "clever" enough not to create a huge backing bitmap when most of its contents would be invisible anyway
What to do, oh what to do?
Anyone has any experience they want to share?
I recently saw a presentation on a game involving hundreds of sprites implemented as UIViews, mostly image views. Performance was excellent on both an older and recent generation iPad. His code is available at https://github.com/bentford/ButtonWars/ , so you may want to test it to see if the performance meets your needs.
There is often an assumption that views are heavyweight, but experience doesn't bear that out.
If most of the objects will not be on the screen at any time, it's probably better not to create views for all of those off-screen objects. You need to intelligently add views for just those objects that are visible, and remove them from the view hierarchy when they go off-screen. You might be able to use UICollectionView to make this easy - it is very well designed, efficient, flexible, and easy to use and customize - but I don't know if it works with zooming.
If you decide to draw everything in one view, with deep zooming, you should look at CATiledLayer. It is designed for zooming in very deeply. Apple's MKMapView uses it under the covers to support scaling its viewport from Earth-sized down to a (relatively) few square meters.
If you cant use UICollectionView due to iOS version boundations, and you are intrested in re using views in UIScrollView, than have a look at this class. It is used the same way UITableview is used. Just pass in number of views and it will reuse the views.You can go through readme. Its very straightforward.

UICollectionView performance - _updateVisibleCellsNow

I'm working on a custom UICollectionViewLayout that displays cells organized by day/week/month.
It is not scrolling smooth, and it looks like the lag is caused by [UICollectionView _updateVisibleCellsNow] being called on each rendering loop.
Performance is OK for < 30 items, but at around 100 or more, its terribly slow. Is this a limitation of UICollectionView and custom layouts, or am I not giving the view enough information to perform correctly?
Source here: https://github.com/oskarhagberg/calendarcollection
Layout: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarWeekLayout.m
Data source and delegate: https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarView.m
Time Profile:
Update
Maybe its futile? Some testing with a plain UICollectionViewController with a UICollectionViewFlowLayout that is given approximately the same amount of cells/screen results in a similar time profile.
I feel that it should be able to handle ~100 simple opaque cells at a time without the jitter. Am I wrong?
Also don't forget to try to rasterize the layer of the cell:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
I had 10 fps without that, and boom 55fps with!
I'm not really familiar with GPU and compositing model, so what does it do exactly is not entirely clear to me, but basically it flatten the rendering of all subviews in only one bitmap (instead of one bitmap per subview?).
Anyway I don't know if it has some cons, but it is dramatically faster!
I have been doing considerable investigation of UICollectionView performance. The conclusion is simple. Performance for a large number of cells is poor.
EDIT: Apologies, just re-read your post, the number of cells you have should be OK (see the rest of my comment), so cell complexity may also be a problem.
If your design supports it check:
Each cell is opaque.
Cell content clips to bounds.
Cell coordinate positions do not contain fractional values (e.g. always calculate to be whole pixels)
Try to avoid overlapping cells.
Try to avoid drop shadows.
The reason for this is actually quite simple. Many people don't understand this, but it is worth understanding: UIScrollViews do not employ Core Animation to scroll. My naive belief was that they involved some secret scrolling animation "sauce" and simply requested occasional updates from delegates every now and then to check status. But in fact scroll views are objects which don't directly apply any drawing behaviour at all. All they really are is a class which applies a mathematical function abstracting the coordinate placement of the UIViews they contain, so the Views coordinates are treated as relative to an abstract contentView plane rather than relative to the origin of the containing view. A scroll view will update the position of the abstract scrolling plane in accord with user input (e.g. swiping) and of course there is a physics algorithm as well which gives "momentumn" to the translated coordinate positions.
Now if you were to produce your own collection view layout object, in theory, you could produce one which 100% reverses the mathematical translation applied by the underlying scrollview. This would be interesting but useless, because it would then appear that the cells are not moving at all as you swipe. But I raise this possibility because it illustrates that the collection view layout object working with the collection view object itself does a very similar operation to the underlying scrollview. E.g. it simply provides an opportunity to apply an additional mathematical frame by frame translation of the attributes of the views to be displayed, and in the main this will be a translation simply of position attributes.
It is only when new cells are inserted or deleted moved or reloaded that CoreAnimation is involved at all; most usually by calling:
- (void)performBatchUpdates:(void (^)(void))updates
completion:(void (^)(BOOL finished))completion
UICollectionView requests cell layoutAttributes for each frame of scrolling and each visible view is laid out for each frame. UIView's are rich objects optimised for flexibility more than performance. Every time one is laid out, there are a number of tests the system does to check it's alpha, zIndex, subViews, clipping attributes etc. The list is long. These checks and any resulting changes to the view are being conducted for each collection view item for each frame.
To ensure good performance all frame by frame operations need to be completed within 17ms. [With the number of cells you have, that is simply not going to happen] bracketed this clause because I have re-read your post and I realise I had misread it. With the number of cells you have, there should not be a performance problem. I have personally found with a simplified test with vanilla cells, containing only a single cached image, the limit tested on an iPad 3 is about 784 onscreen cells before performance starts to drop below 50fps.
In practice you will need to keep it less than this.
Personally I'm using my own custom layout object and need higher performance than UICollectionView provides. Unfortunately I didn't run the simple test above until some way down the development path and I realised there are performance problems. I'm so I'm going to be reworking the open source back-port of UICollectionView, PSTCollectionView. I think there is a workaround that can be implemented so, just for general scrolling about, each cell item's layer is written using an operation which circumvents the layout of each UIView cell. This will work for me since I have my own layout object, and I know when layout is required and I have a neat trick that will allow the PSTCollectionView to fall back to its normal mode of operation at this time. I've checked the call sequence and it doesn't appear to be too complex, nor does it appear at all unfeasible. But for sure it is non-trivial and some further tests have to be done before I can confirm it will work.
Some more observations that might be helpful:
I am able to reproduce the problem, using flow layout with around 175 items visible at once: it scrolls smoothly in the simulator but lags like hell on iPhone 5. Made sure they are opaque etc.
What ends up taking the most time seems to be work with a mutable NSDictionary inside _updateVisibleCellsNow. Both copying the dictionary, but also looking up items by key. The keys seems to be UICollectionViewItemKey and the [UICollectionViewItemKey isEqual:] method is the most time consuming method of all. UICollectionViewItemKey contains at least type, identifier and indexPath properties, and the contained property indexPath comparison [NSIndexPath isEqual:] takes the most time.
From that I'm guessing that the hash function of UICollectionViewItemKey might be lacking since isEqual: is called so often during dictionary lookup. Many of the items might be ending up with the same hash (or in the same hash bucket, not sure how NSDictionary works).
For some reason it is faster with all items in 1 section, compared to many sections with 7 items in each. Probably because it spends so much time in NSIndexPath isEqual and that is faster if the row diffs first, or perhaps that UICollectionViewItemKey gets a better hash.
Honestly it feels really weird that UICollectionView does that heavy dictionary work every scroll frame, as mentioned before each frame update needs to be <16ms to avoid lag. I wonder if that many dictionary lookups either is:
Really necessary for general UICollectionView operation
There to support some edge case rarely used and could be turned off for most layouts
Some unoptimized internal code that hasn't been fixed yet
Some mistake from our side
Hopefully we will see some improvement this summer during WWDC, or if someone else here can figure out how to fix it.
Here is Altimac's answer converted to Swift 3:
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
Also, it should be noted that this code goes in your collectionView delegate method for cellForItemAtIndexPath.
One more tip - to see an app's frames per second (FPS), open up Core Animation under Instruments (see screenshot).
The issue isn't the number of cells you're displaying in the collection view total, it's the number of cells that are on screen at once. Since the cell size is very small (22x22), you have 154 cells visible on screen at once. Rendering each of these is what's slowing your interface down. You can prove this by increasing the cell size in your Storyboard and re-running the app.
Unfortunately, there's not much you can do. I'd recommend mitigating the problem by avoiding clipping to bounds and trying not to implement drawRect:, since it's slow.
Big thumbs up to the two answers above!
Here's one additional thing you can try: I've had big improvements in UICollectionView performance by disabling auto layout. While you will have to add some additional code to layout the cell interiors, custom code seems to be tremendously faster than auto layout.
Beside the listed answers (rasterize, auto-layout, etc.), you may also want to check for other reasons that potentially drags down the performance.
In my case, each of my UICollectionViewCell contains another UICollectionView (with about 25 cells each). When each of the cell is loading, I call the inner UICollectionView.reloadData(), which significantly drags down the performance.
Then I put the reloadData inside the main UI queue, the issue is gone:
DispatchQueue.main.async {
self.innerCollectionView.reloadData()
}
Carefully looking into reasons like these might help as well.
In few cases it is due to Auto-layout in UICollectionViewCell. Turn it off (if you can live without it) and scrolling will become butter smooth :)
It's an iOS issue, which they havnt resolved from ages.
If you are implementing a grid layout you can work around this by using a single UICollectionViewCell for each row and add nested UIView's to the cell. I actually subclassed UICollectionViewCell and UICollectionReusableView and overrode the prepareForReuse method to remove all of the subviews. In collectionView:cellForItemAtIndexPath: I add in all of the subviews that originally were cells setting their frame to the x coordinate used in the original implementation, but adjusting it's y coordinate to be inside the cell. Using this method I was able to still use some of the niceties of the UICollectionView such as targetContentOffsetForProposedContentOffset:withScrollingVelocity: to align nicely on the top and left sides of a cell. I went from getting 4-6 FPS to a smooth 60 FPS.
Thought I would quickly give my solution, as I faced a very similar issue - image-based UICollectionView.
In the project I was working in, I was fetching images via network, caching it locally on device, and then re-loading the cached image during scrolling.
My flaw was that I wasn't loading cached images in a background thread.
Once I did put my [UIImage imageWithContentsOfFile:imageLocation]; into a background thread (and then applied it to my imageView via my main thread), my FPS and scrolling was a whole lot better.
If you haven't tried it yet, definitely give a go.

How to create multidirectional infinite/circular scrolling view like the HBO GO iPad App

My question is essentially what it says in the title--I would like to create a scrolling view similar to the one that appears under the 'Home' tab of the HBO GO iPad application.
I have looked into circular/infinite UIScrollViews, but they only discuss infinite content in one direction (either horizontal OR vertical) and bring up many problems when scroll speed gets too high. So my question is twofold:
A) Could they have created this scrolling view by subclassing UIScrollView? If so, please do let me know how?
B) If not, does anyone have ideas as to a starting point for how they could have created it? It runs very, very smoothly even at fast acceleration, and I'm trying to figure out how they created this.
Thanks in advance!
Reposting to get answer ;-)
The sample is named StreetScroller.
Referenced video is here.
I believe the successful technique will be to apply the techniques in the video in either a 2x2 or 3x3 grid and handle scrolling in both directions.
I have put together a library that provides an infinitely scrolling view in all directions. It allows you to very easily achieve the effect you’re looking for and much more. As the user scrolls around, the framework lays out the tiles and lets the delegate know so it can set up the tiles' presentations. This is indeed done by subclassing UIScrollView and as for performance, the framework introduces no lag: full 60 fps no matter how fast you scroll.
The framework with a sample app that displays Flickr images in an infinitely scrolling wall is here: https://github.com/vovagalchenko/scroll-for-days. Additionally, here's a video of the sample app in action: https://cloud.box.com/s/d6bgvlot175au5a3jeh5
I don't think there is an easy way to do it by sub classing UIScrollView
I have done something similar with a UIView and a custom gesture recognizer, moving views around nice they disappear off the side of the screen.
I hope this helps you.

Resources