UIView content mode acts differently when override drawRect: - ios

Im learning iOS. I've been reading Apple's View programming guide.
I did the followings to try out content mode:
I dragged a uiview into storyboard and made it my CustomView class.
Then i changed the content mode to be left and set the background color to be pink color.
Then there's a button that just simply changes the custom view's width and height to be 1.5 times bigger.
Now, i found an interesting thing:
I run this, and click the button, then:
Case 1: If i override the draw rect method
drawRect: overrided
Then the appearance of the view shifted down
Case 2: Without overriding the draw rect method
drawRect: not overrided
The size actually increased
After doing some search online and look into Apple's document, i couldnt any relavent things except one question mention that this might have something to do with drawLayer:inContext: behave differently if override the drawRect:
Does anyone know what is going on here?
(sorry about the formatting, this is a new account and i can't post more than 2 links.)
CODE:
For the CustomView, just either override drawRect or without it.
#import "CustomView.h"
#implementation CustomView
- (void)drawRect:(CGRect)rect{
//Do nothing, for case 2 i just commented out this method.
}
#end
For Changing the frame of customView:
- (IBAction)changeBound:(id)sender {
self.customView.frame = CGRectMake(self.customView.frame.origin.x, self.customView.frame.origin.y, self.customView.frame.size.width * 1.5, self.customView.frame.size.height * 1.5);
}

After doing some research, i think i figure out what happened:
So drawing background code is actually handled separately in -(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context. That's why with empty drawRect, it still draws the background. Also, depending on the existence of drawRector not(Probably by calling respondsToSelector?), UIView behave different in drawLayer :inContext.
See Dennis Fan's answer in this link

Related

Adding button gradient before appearing in iOS

I want to use gradient for a button in iOS, but since the button's frame (button.layer.bounds) changes because of Auto Layout, I found that I have to put the insertSublayer in the viewDidAppear method -- which means the button first appears without the gradient and then gets repainted with it. Is there anyway to get the final button size in viewWillAppear so that I can apply the correct size gradient before it appears?
Here's how I do it now:
- (void)viewDidAppear:(BOOL)animated{
CAGradientLayer *gradient1=[CAGradientLayer layer];
gradient1.colors=[NSArray arrayWithObjects:(id)light.CGColor,(id)dark.CGColor,nil];
gradient1.cornerRadius=10;
gradient1.frame=_go_button.layer.bounds;
gradient1.borderColor=dark.CGColor;
gradient1.borderWidth=1;
[_go_button.layer insertSublayer:gradient1 atIndex:0];
Sometimes by asking a question leads you to finding your own answer.... Here's a link to a website I found that answered it: has the solution
The simple answer is to put the gradient code into viewDidLayoutSubviews instead of viewDidAppear.

How to draw parts of a UIView as it becomes visible inside UICollectionView?

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:
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.
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. :/
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.

IB_DESIGNABLE draw image

I've created a simple view that will show some AVPlayer content, or if this does not exist (initial state) draw a splash image.
I've added the IB_DESIGNABLE directive, and am drawing the splash image in drawRect. However, in Interface Builder, the component renders as a black square. I'd like it to draw the image. Is this possible?
At runtime all works fine.
To investigate what goes on within the IB rendition for your drawRect: implementation, I would recommend to debug how your view behaves with IB using the debugger.
For that, select your view in IB, and then use the Debug Selected Views item from the Editor menu in XCode. By setting up a breakpoint in the drawRect: you should be able to understand what's going on and why you get a black square.
Just for reference, you can set custom variables in IB (such as your images) using the keyword IBInspectable, f.e. as:
IB_DESIGNABLE
#interface MyCustomControl : UIView
{
IBInspectable UIImage *myImage;
...
}
...
#end
For more info read this awesome blog where I found that keyword (I had been looking for something like that for quite a while).

How to control what part of a UIView to update (iOS/Xamarin) - I need to prevent the screen from being fully erased

I am working on a graphing application which takes in a stream of data and displays it. It turns out that drawing a lot of line segments in UIView (Core Graphics) is a very slow. I'm trying to speed it up by only drawing a portion of the screen that has new data.
I currently use this function to redraw the screen (using Xamarin/monotouch)
NSTimer.CreateRepeatingScheduledTimer (TimeSpan.FromSeconds (0.1), delegate {
pane.SetNeedsDisplay ();
});
where pane is my UIView object. The pane object knows itself what part of the screen needs to be redrawn. I think there's another method that specifies a region of the screen to redraw - is that the right way to go? Or some other solution?
Also, the code draws a set of grids. These never change. Is there a way to save them to some layer or something so that I can just add it to the background?
The alternative is to go with openGL though drawing lines doesn't look as good and I'll have to implement a lot of extra code to make it look right.
Thanks!
For the graph data, you should be using CoreGraphics directly to render the lines in your UIView subclass' drawRect: method. Make sure that method only redraws the data that overlaps with the CGRect argument. The method setNeedsDisplay marks the entire UIView as needing a redraw, so instead of using that when your graph data changes, you should use the UIView's method setNeedsDisplayInRect: to indicate that there's new data in that region of the UIView that will call drawRect: to be called, which will only redraw that portion (if you've implemented it right).
Regarding the background grid, you can render the grid to a UIImage then display that UIImage behind your view that draws the graph. You'll want to look at the documentation for UIGraphicsBeginImageContext, CALayer renderInContext:, UIGraphicsGetImageFromCurrentImageContext, and UIGraphicsEndImageContext.
You can use this variant:
void SetNeedsDisplayInRect (RectangleF rect);
That will only request that the specified region be redrawn.

setNeedsDisplay confusion

I'm using this to move my image:
meteorDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(moveImage:)];
Here is my moveImage: method:
-(void) moveImage:(CADisplayLink *)sender{
CGPoint img;
imgLocation = imgImageView.center;
if (img < 100) {
img.y++;
}
else{
imgLocation.y = 0;
}
imgImageView.center = imgLocation;
}
This updates the location of my image on screen no matter what. Why does setNeedsDisplay have no effect here? What is the point of setNeedsDisplay?
I assume imgImageView is an instance of UIImageView. If it's not an instance of UIImageView, edit your question to tell us what it is.
Changing the center property of a view never requires your app to redraw the view. The display server (which is a separate process named backboardd in iOS 6 and springboard in earlier versions) has a copy of your view's pixel data already. When you change a view's center, the view just informs the display server of its new position, and the display server copies the view's pixel data to the appropriate parts of the screen.
Thus changing the center property of a view never requires you to send setNeedsDisplay.
More specifically, UIImageView doesn't even implement drawRect:. Instead, UIImageView just sends its image's pixel data to the display server when you set its image. So even if you send setNeedsDisplay to a UIImageView, it still won't run any drawRect: method.
By the way, you can do this in your own UIView subclasses too, by setting self.layer.contents to a CGImageRef instead of implementing drawRect:. You will need to #import <QuartzCore/QuartzCore.h> to do this. Of course, UIImageView does other things too, like handling animated and resizable images.
When you set properties that affect the layout of an object, setNeedsDisplay will be called internally. You need to call it yourself for custom properties, etc, that don't automatically call it.
setNeedsDisplay only applies to the contents of a layer or view. I.e., if the contents of a view has changed and needs to be redrawn, you would use setNeedsDisplay to tell the view that it needs to be redrawn and that would trigger its drawRect method.
Transformations on a view, such as position, scale, and rotation, etc., are handled directly by the GPU and do not require the view to be redrawn; therefore, setNeedsDisplay does not apply when you're simply moving or transforming a view.

Resources