setNeedsDisplay not firing DrawRect - ios

I have a viewController which is obviously a subclass of UIViewController called MapViewController. In this viewController I use GPS to get the location of the user.
I also have a view called DrawCircle. This view is a subclass of UIView.
Using drawCircle I would like to be able to at any time draw on my MapViewController. But I am not sure I am understanding the concept of doing so. I know my drawing code is working, I have used it before. But I don't know how to draw onto MapViewController using DrawCircle.
From what it seems to my whenever I call [myCustomView setNeedsDisplay], it is not calling the DrawRect method in my view.
Here is some code:
MapViewController.h
#import "DrawCircle.h"
#interface MapViewController: UIViewController <CLLocationManagerDelegate>{
DrawCircle *circleView;
}
#property (nonatomic, retain) DrawCircle *circleView;
#end
MapViewController.m
#import "DrawCircle.h"
#interface MapViewController ()
#end
#implementation MapViewController
#synthesize circleView;
- (void) viewDidLoad
{
circleView = [[DrawCircle alloc] init];
[self setNeedsDisplay];
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
#end
DrawCircle.m
#import "DrawCircle.h"
#interface DrawCircle()
#end
#implementation DrawCircle
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(self) {
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGPoint point = CGPointMake(40, 137);
CGContextAddEllipseInRect(ctx, CGRectMake(point.x, point.y, 10, 10));
}
CGContextSetFillColor(ctx, CGColorGetComponents([[UIColor redColor] CGColor]));
CGContextFillPath(ctx);
}
Also if this offers any help into my thought process, here is my StoryBoard scene.
Where the viewcontrollers custom class is MapViewController and the views custom class is DrawCircle.
**EDIT:**I would also like to mention that, in my DrawCircle.m, I have methods that I am calling from MapViewController.m and are working.
Also. Initially, the DrawRect method is being called but I am not able to manually call using setNeedsUpdate. When debugging, it is not even entering the DrawRect method.

You're creating your DrawCircle, but you're never adding it to the view, e.g.
[self.view addSubview:circleView];
Therefore, it's falling out of scope and (if using ARC) getting released on you. You also don't appear to be setting its frame, such as:
circleView = [[DrawCircle alloc] initWithFrame:self.view.bounds];
Alternatively, you could add the view in interface builder (a standard UIView, but then specify your custom class right in IB).
Also, note, you generally don't even to call setNeedsDisplay. The adding it to the view hierarchy will call this for you. You only need to call this if you need to update the view based upon some custom property.
Personally, I'd be inclined to define drawRect like so:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
// I'd just use `rect` and I can then use the `frame` to coordinate location and size of the circle
CGContextAddEllipseInRect(ctx, rect);
// perhaps slightly simpler way of setting the color
CGContextSetFillColorWithColor(ctx, [[UIColor redColor] CGColor]);
CGContextFillPath(ctx);
}
This way, the location and size of the circle will be dictated by the frame I set for the circle (by the way, I apologize if this makes it more confusing, but I use a different class name, CircleView, as I like to have View in the name of my UIView subclasses).
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *circle;
circle = [[CircleView alloc] initWithFrame:CGRectMake(100.0, 100.0, 200.0, 200.0)];
circle.backgroundColor = [UIColor clearColor]; // because it calls `super drawRect` I can now enjoy standard UIView features like this
[self.view addSubview:circle];
circle = [[CircleView alloc] initWithFrame:CGRectMake(300.0, 300.0, 10.0, 10.0)];
circle.backgroundColor = [UIColor blackColor]; // because it calls `super drawRect` I can now enjoy standard UIView features like this
[self.view addSubview:circle];
}

Related

Overlapping transparent UIViews

I’m overlaying two UIViews with a white backgroundColor at 25% opacity. In a small part, they overlap each other, meaning that at that area, they are summed to 50% opacity.
I’d like to keep that 25% opacity, even if the two views overlap, effectively meaning that in those overlapped points, each view’s opacity drops to 12.5% to total 25%.
I’ve done a little looking into compositing but I’m not sure which of these modes would help, or how I’d go about applying them to a specific part of these two UIView instances.
(http://docs.oracle.com/javase/tutorial/2d/advanced/compositing.html is what I was reading, and I found the CGBlendMode for drawing, if it comes to using that (though I’d prefer not to if possible!))
You can't control the compositing mode of views (or, really, CALayers) on iOS.
The best solution I can think of here is to leave both views with a clearColor (or nil) background, and use a single CAShapeLayer to draw the background of both. If your two views have the same parent, it's not too hard.
Let's say the parent is of type ParentView. Override layoutSubviews in ParentView to create and update the backdrop layer as necessary. Be sure to send setNeedsLayout to the parent view if you move either of the child views.
ParentView.h
#import <UIKit/UIKit.h>
#interface ParentView : UIView
#property (nonatomic, strong) IBOutlet UIView *childView0;
#property (nonatomic, strong) IBOutlet UIView *childView1;
#end
ParentView.m
#import "ParentView.h"
#implementation ParentView {
CAShapeLayer *backdrop;
}
- (void)layoutSubviews {
[super layoutSubviews];
[self layoutBackdrop];
}
- (void)layoutBackdrop {
[self createBackdropIfNeeded];
[self arrangeBackdropBehindChildren];
[self setBackdropPath];
}
- (void)createBackdropIfNeeded {
if (backdrop == nil) {
backdrop = [CAShapeLayer layer];
backdrop.fillColor = [UIColor colorWithWhite:1 alpha:0.25].CGColor;
backdrop.fillRule = kCAFillRuleNonZero;
backdrop.strokeColor = nil;
}
}
- (void)arrangeBackdropBehindChildren {
[self.layer insertSublayer:backdrop atIndex:0];
}
- (void)setBackdropPath {
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.childView0.frame];
[path appendPath:[UIBezierPath bezierPathWithRect:self.childView1.frame]];
backdrop.path = path.CGPath;
}
#end
If you add them both to the same parent UIView, tell that UIView to rasterize, then set the alpha on the parent, you'll get the desired effect. I'm not sure if that fits with your display structure or performance needs.
UIView *parent = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[parent setBackgroundColor:[UIColor clearColor]];
[parent.layer setShouldRasterize:YES];
[parent.layer setRasterizationScale:[[UIScreen mainScreen] scale]];
[parent setAlpha:0.25];
UIView *subview1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 125, 125)];
[subview1 setBackgroundColor:[UIColor whiteColor]];
[parent addSubview:subview1];
UIView *subview2 = [[UIView alloc] initWithFrame:CGRectMake(75, 75, 125, 125)];
[subview2 setBackgroundColor:[UIColor whiteColor]];
[parent addSubview:subview2];

UIView is never rendered if added as subview in viewDidLoad

I'm trying to add a subview to a view controller programatically, but it seems like if I add the subview in the viewDidLoad of the view controller whatever I have added as subview in the view itself is not rendered in the view controller.
But if I move the [self.view addSubview:myUIView] in the init method of the view controller, everything is rendered.
Also, if I call the same methods in the viewDidAppear I get all the elements rendered, but it's right after the view controller was displayed, and I can see when the elements are rendered in the view controller.
This would be my view controller:
//interface
#class RLJSignInView;
#protocol RLJSignInViewControllerDelegate;
#interface RLJSignInViewController : UIViewController <UITextFieldDelegate>
#property (nonatomic, readwrite) RLJSignInView *signInView;
#property (nonatomic, assign) id<RLJSignInViewControllerDelegate> delegate;
#end
//implementation
#import "RLJSignInViewController.h"
#import "RLJSignInView.h"
#import "RLJSignInViewControllerDelegate.h"
#import "UIColor+RLJUIColorAdditions.h"
#implementation RLJSignInViewController
- (id)init
{
self = [super init];
if (self) {
_signInView = [[RLJSignInView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.signInView];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%#", self.signInView);
[self.view setBackgroundColor:[UIColor rgbWithRed:250 green:250 blue:250]];
}
#end
And this would be my view:
//interface
#interface RLJSignInView : UIView
#property (strong, nonatomic, readwrite) UITextField *username;
#property (strong, nonatomic, readwrite) UITextField *password;
#property (strong, nonatomic, readwrite) UIButton *signIn;
#property (strong, nonatomic, readwrite) UIButton *signUp;
#end
//implementation
#import "RLJSignInView.h"
#import "UITextField+RLJUITextFieldAdditions.h"
#import "UIButton+RLJUIButtonAdditions.h"
#implementation RLJSignInView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
CGRect usernameInputFrame = CGRectMake(10.0, 40.0, self.bounds.size.width - 20, 40);
CGRect passwordInputFrame = CGRectMake(10.0, 79.0, self.bounds.size.width - 20, 40);
CGRect signInButtonFrame = CGRectMake(10.0, 160, self.bounds.size.width - 20, 40);
CGRect signUpButtonFrame = CGRectMake(10.0, 220, self.bounds.size.width - 20, 40);
UIView *usernameInputLeftView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 40)];
UIView *passwordInputLeftView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 40)];
self.username = [[UITextField alloc] initWithFrame:usernameInputFrame
textAlignment:NSTextAlignmentLeft
textColor:[UIColor blackColor]
clearButton:UITextFieldViewModeWhileEditing
leftView:usernameInputLeftView
placeholder:#"Username"
backgroundColor:[UIColor whiteColor]
strokeWidth:2.0
strokeColor:[UIColor lightGrayColor]
keyboardType:UIKeyboardTypeEmailAddress
byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
cornerRadii:CGSizeMake(4.0, 4.0)
secure:NO];
self.password = [[UITextField alloc] initWithFrame:passwordInputFrame
textAlignment:NSTextAlignmentLeft
textColor:[UIColor blackColor]
clearButton:UITextFieldViewModeWhileEditing
leftView:passwordInputLeftView
placeholder:#"Password"
backgroundColor:[UIColor whiteColor]
strokeWidth:2.0
strokeColor:[UIColor lightGrayColor]
keyboardType:UIKeyboardTypeDefault
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(4.0, 4.0)
secure:YES];
self.signIn = [[UIButton alloc] initWithFrame:signInButtonFrame
title:#"Sign In"
colorNormal:[UIColor whiteColor]
colorHighlighted:[UIColor whiteColor]
colorDisabled:[UIColor whiteColor]
backgroundNormal:[UIColor colorWithRed:82 / 255.0 green:156 / 255.0 blue:201 / 255.0 alpha:1.0]
cornerRadius:4.0];
self.signUp = [[UIButton alloc] initWithFrame:signUpButtonFrame
title:#"Sign Up"
colorNormal:[UIColor blackColor]
colorHighlighted:[UIColor blackColor]
colorDisabled:[UIColor blackColor]
backgroundNormal:[UIColor whiteColor]
cornerRadius:4.0];
[self addSubview:self.username];
[self addSubview:self.password];
[self addSubview:self.signIn];
[self addSubview:self.signUp];
}
return self;
}
#end
I'm not sure what I could do about it, but I know for sure that I would not like the view to be added to the subview on initialisation.
Or maybe I'm doing the rendering of the subviews of the view wrong. I would appreciate some input on this matter if anyone encountered the same thing or if noticed that I messed up something.
Don't call self.view from inside your init method. This will trigger viewDidLoad being called before your init method even returns, which is incorrect behavior.
Instead, follow this pattern:
create your objects (properties / instance variables) in init, or by using lazy instantiation
add the subviews in viewDidLoad
set the frames in viewWillLayoutSubviews
This pattern will avoid bugs like the one you posted here, and set you up for success handling rotation and resizing events in the future.
The key might be you are using self.view.bounds too early. It might work when you provide an initial size frame (like IB would do).
init is too early. The view controller is initializing, the view is not loaded yet, which means there's no view and the bounds will return CGRectZero.
viewDidLoad is also too early to rely on the view's bounds. The view is just loaded, it does not have a superview yet and might be resized later.
viewWillAppear is the first moment you can rely on the view's bounds. Since it will be resized according to the autoresizingmasks or autolayout constraints.
You can add the subview in viewDidLoad but you you should provide an initial frame (and you set the correct autoresizingmasks or constraints) it should all work fine.

Get the current value of UISlider in drawRect: method

I'm trying to move a point drawn in a UIView based on the value of a UISlider. The code below is for a UIView (subview ?) with a custom class (WindowView) on a UIViewController.
WindowView.h
#import <UIKit/UIKit.h>
#interface WindowView : UIView
- (IBAction)sliderValue:(UISlider *)sender;
#property (weak, nonatomic) IBOutlet UILabel *windowLabel;
#end
WindowView.m
#import "WindowView.h"
#interface WindowView ()
{
float myVal; // I thought my solution was using an iVar but I think I am wrong
}
#end
#implementation WindowView
#synthesize windowLabel;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)sliderValue:(UISlider *)sender
{
myVal = sender.value;
windowLabel.text = [NSString stringWithFormat:#"%f", myVal];
}
- (void)drawRect:(CGRect)rect
{
// I need to get the current value of the slider in drawRect: and update the position of the circle as the slider moves
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(myVal, myVal, 10, 10)];
[circle fill];
}
#end
OK, you need to store the slider value in an instance variable and then force the view to redraw.
WindowView.h:
#import <UIKit/UIKit.h>
#interface WindowView : UIView
{
float _sliderValue; // Current value of the slider
}
// This should be called sliderValueChanged
- (IBAction)sliderValue:(UISlider *)sender;
#property (weak, nonatomic) IBOutlet UILabel *windowLabel;
#end
WindowView.m (modified methods only):
// This should be called sliderValueChanged
- (void)sliderValue:(UISlider *)sender
{
_sliderValue = sender.value;
[self setNeedsDisplay]; // Force redraw
}
- (void)drawRect:(CGRect)rect
{
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(_sliderValue, _sliderValue, 10, 10)];
[circle fill];
}
You probably want to initialise _sliderValue to something useful in the view's init method.
Also _sliderValue probably isn't the name you want to choose; perhaps something like _circleOffset or some such.

How to add a custom button to a UINavigationBar (a la Foursquare)

Im going through the different options of creating a custom UINavigationBar and since my app is iOS 5+, i am using this code:
// Set the background image all UINavigationBars
[[UINavigationBar appearance] setBackgroundImage:NavigationPortraitBackground
forBarMetrics:UIBarMetricsDefault];
Now i want a custom image button on the very right side and am a bit lost. Should I go another way, subclass UINavigationBar and add a button to it or would there be an easier way?
Absolutely subclass this thing. UINavigationBar is easy to subclass, but very hard to put into a navigation controller. I will show you my subclass (CFColoredNavigationBar), which also comes with an arbitrarily colored background for free.
//.h
#import <UIKit/UIKit.h>
#interface CFColoredNavigationBar : UINavigationBar
#property (nonatomic, strong) UIColor *barBackgroundColor;
#property (nonatomic, strong) UIButton *postButton;
#end
//.m
#import "CFColoredNavigationBar.h"
#implementation CFColoredNavigationBar
#synthesize barBackgroundColor = barBackgroundColor_;
#synthesize postButton = postButton_;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(void)awakeFromNib {
postButton_ = [[UIButton alloc]initWithFrame:CGRectMake(CGRectGetWidth(self.frame)-60, 0, 60.0f, CGRectGetHeight(self.frame))];
[postButton_ setImage:[UIImage imageNamed:#"Post.png"] forState:UIControlStateNormal];
[self addSubview:postButton_];
}
- (void)drawRect:(CGRect)rect {
if (barBackgroundColor_ == nil) {
barBackgroundColor_ = [UIColor blackColor];
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [barBackgroundColor_ CGColor]);
CGContextFillRect(context, CGRectInset(self.frame, 0, self.frame.size.height*-2));
}
#end

UIView in greyscale. View not being redrawn

I managed to get a UIView in greyscale by adding the following view on top:
#interface GreyscaleAllView : UIView
#property (nonatomic, retain) UIView *underlyingView;
#end
#implementation GreyscaleAllView
#synthesize underlyingView;
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// draw the image
[self.underlyingView.layer renderInContext:context];
// set the blend mode and draw rectangle on top of image
CGContextSetBlendMode(context, kCGBlendModeColor);
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
CGContextFillRect(context, rect);
[super drawRect:rect];
}
#end
It works, but the content doesn't get updated unless i manually call setNeedsDisplay. (I can press a UIButton and the action fires, but nothing changes in appearance) So for it to behave like expected I call setNeedsDisplay 60 times each second. What am I doing wrong?
UPDATE:
The viewcontroller inits the overlayview with this:
- (void)viewDidLoad
{
[super viewDidLoad];
GreyscaleAllView *grey = [[[GreyscaleAllView alloc] initWithFrame:self.view.frame] autorelease];
grey.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
grey.userInteractionEnabled = NO;
[self.view addSubview:grey];
}
I've added this for the underlaying views to redraw:
#implementation GreyscaleAllView
- (void)setup {
self.userInteractionEnabled = FALSE;
self.contentMode = UIViewContentModeRedraw;
[self redrawAfter:1.0 / 60.0 repeat:YES];
}
- (void)redrawAfter:(NSTimeInterval)time repeat:(BOOL)repeat {
if(repeat) {
// NOTE: retains self - should use a proxy when finished
[NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
}
[self setNeedsDisplay];
}
What you really want is for the subview to only be told to redraw when the parent view redraws, in which case you can override -setNeedsDisplay in the parent view and make it call -setNeedsDisplay on the GreyscaleAllView. This requires subclassing UIView for your UIViewController's view property.
i.e. implement loadView in the controller, and set
self.view = [[CustomView alloc] initWithFrame:frame];
where CustomView overrides setNeedsDisplay as described above:
#implementation CustomView
- (void)setNeedsDisplay
{
[grayView setNeedsDisplay]; // grayView is a pointer to your GreyscaleAllView
[super setNeedsDisplay];
}
#end
For completeness, you should also do the same for -setNeedsDisplayInRect:

Resources