6

On Android I have item views with potentially large widths on a horizontal scrolling list. The view loads and draws chunks "images" as parts of the view become visible on the list. This is an optimization to avoid drawing all images at once as it would be wasteful and slow. The content drawn is essentially an audio waveform. Do to the ways things need to work I can't split the chunks as individual view items on a list. This strategy works perfect because of how android drawing architecture works.

Now I'm trying to use a similar pattern in iOS but I'm having some trouble and I'm not too sure how to come up with a solution.

In iOS I'm are using a UICollectionView which draws large width Cells and we need that same optimization of loading and drawing only the visible chunks.


Solution 1:
Check what parts of the UIView is visible and draw only those visible chunks. The problem with this solution is that as the UICollectionView scrolls the UIView doesn't draw the next chucks that are becoming visible. Here are examples of what I'm talking about.

UIView loads initial chunks
These can be seen by their different chunk colors: enter image description here

Scroll a bit
Black is shown and no content is loaded because there is no hint that the view needs to draw again so we can't load the next visible chunk. enter image description here


Solution 2:
Use custom UIView backed by a CATiledLayer. This works perfect because it draws the tiles as they become visible while scrolling the UICollectionView.

The problem is that drawing happens on the background thread or on the next drawing cycle if shouldDrawOnMainThread is defined. This brings issues when the UIView is resized or my internal zoom logic kicks. Things shift because the drawing cycle is not synchronized to the view resizing.


So how could I get notified like CATiledLayer that a part is becoming visible and I could draw normally like a CALayer backed UIView?


UPDATE 1
I'm looking into using preferredLayoutAttributesFittingAttributes as a way to check if a new chunk need to be drawn. This is called every time the cell is moved into a new position because of scrolling. Hopefully, this isn't a bad idea. :)

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes

UPDATE 2
After much testing and playing around. Using solution 1 is not an option. When a UIView reaches a huge width memory usage goes off the roof while using CATiledLayer memory usage is at minimal as I guess one would expect. :/

Jona
  • 13,325
  • 15
  • 86
  • 129
  • Did you tried actually `func draw(_ rect: CGRect)` ? – MichaelV Feb 28 '18 at 15:42
  • @MichaelVorontsov well that's not really the problem. :P Drawing is no issue. The problem is triggering the drawing for only the visible area. On my update #2 I had topped into a limitation that leaves CATiledLayer as more or less the answer. :/ – Jona Mar 05 '18 at 14:23
  • `func setNeedsDisplay(CGRect)` ? You can provide additional flag to do drawing only when you asked to draw particular rect, and draw nothing when flag down. – MichaelV Mar 05 '18 at 15:18

1 Answers1

2

I don't know if you can completely turn off the effect. You can soften it by setting the fadeDuration to 0. To do this, you must create a subclass of CATiledLayer and overwrite the class method fadeDuration.

You can also try to implement shouldDrawOnMainThread class method, but this is a private method, and you're risking a rejection on the App Store:

@implementation MyTiledLayer

+(CFTimeInterval)fadeDuration {
    return 0.0;
}

+ (BOOL)shouldDrawOnMainThread {
    return YES;
}

@end

or in Swift:

class MyTiledLayer: CATiledLayer {
    override class func fadeDuration() -> CFTimeInterval {
        return 0.0
    }

    class func shouldDrawOnMainThread() -> Bool {
        return true;
    }
}

If you want to implement your own tiled layer instead, you should waive of the tiles and try to compute the visible part (section and zoom level) of the view. Than you can draw only this part as layer whole content.

Apparently, there is no easy way to re-draw the view when the scrollview is scrolling. But you may implement scrollViewDidScroll:, and trigger the redrawing manually. The visibleRect method always returns the complete area of the layer, but you can use

CGRect theVisibleRect = CGRectIntersection(self.frame, self.superlayer.bounds);

or in Swift

let theVisibleRect = frame.intersection(superlayer.bounds)

to determine the visible area of the layer.

clemens
  • 16,716
  • 11
  • 50
  • 65
  • Thanks for your answer. So the weird issue I'm mentioning isn't the fade in animation. The problem is that I need to draw our cached images immediately after the UIView is resized. Since the CATiledLayer sends a request to draw on the bg thread there is a sync issue between the size of the UIView and the shown layer content. This causes a shaking effect when the view is resized from the left. – Jona Feb 22 '18 at 14:32
  • @Jona: Yes, I know this effect, and I think you can't get fully rid of it. But decreasing the fade duration mitigates it. You can also try to implement shouldDrawOnMainThread class method, but this is a private method, and you're risking a rejection on the App Store. I've update my post for that. – clemens Feb 22 '18 at 14:54
  • Ah very cool! Thanks for sharing this. I just tried that method. It does cause drawing to happen on the main thread. However, unfortunately it posts the drawing update to the next main thread cycle. So the sync issue still happens. I wish I could implement my own custom CATiledLayer. I just can't figure out how to enable it's draw visible tiles only behavior. – Jona Feb 22 '18 at 15:16
  • @Jona: I think you cannot get rid of drawing in the next run loop cycle, even if you're implementing your own tiled layer class. You can't do that because the calls of the drawing methods come always from the run loop. You cannot draw to the screen directly. – clemens Feb 22 '18 at 15:33
  • So using a standard CALayer backed UIView everything works perfect when resizing it. The call to update the UI happens on the same cycle in which the view size is changed. So everything syncs correctly. I want the behavior of CATiledLayer which tells me to draw a certain part of the view but all from the same main thread cycle. It has to be possible somehow! :P Hope, I'm not confusing you... – Jona Feb 22 '18 at 15:43
  • Thanks for the suggestion. That is kinda how I got started. It works perfect but when you start scrolling the large UIView doesn't give me any hints that a particular part is now visible. I tried overriding various methods to see which is called when the collection view is scrolling but I couldn't find anything. So the only way would be to invalidate the collectionview as I scroll but that seems like an overkill. – Jona Feb 22 '18 at 16:05
  • @Jona: Do you have set `contentMode` to `redraw`? – clemens Feb 22 '18 at 16:08
  • Yes, I do use contentMode with redraw. By the way I improved my question a bit more after our discussion here. – Jona Feb 22 '18 at 16:40
  • @Jona: I've extended my post. – clemens Feb 23 '18 at 06:13