How do I use a CALayer to custom draw in a UIButton - objective-c

I'm trying to do draw a fancy UIButton using Quartz. I've declared my own button class like this:
#interface MyButton : UIButton
#end
In the .m file I'm constructing the button:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
CALayer *buttonLayer = self.layer;
buttonLayer.masksToBounds = YES;
CALayer *customDrawn = [CALayer layer];
customDrawn.delegate = self;
customDrawn.masksToBounds = YES;
[buttonLayer insertSublayer:customDrawn atIndex:0];
[customDrawn setNeedsDisplay];
}
return self;
}
But this results in some kind of recursion and finally fails with a EXC_BAD_ACCESS.
I've implemented a method drawLayer: inContext:, but it still crashes. The only way I can avoid the crash is by removing the delegate-assignment, but then I can't do any of the custom drawing I want to implement.
How can I make this work?

As described in the question mentioned by Kurt Revis, a UIView (such as UIButton) can't be used as a delegate for a sublayer. (It is already the delegate for the "main" layer of the UIView and can't be used as a delegate for another layer.) The solution is to use another object as a delegate which can be a "private" class just used to implement the drawLayer: inContext: method.

Related

Extending UIContol

I want to add borders and corner radius to my UIButtons,UITextFields, etc
I think I could extend UIControl to add this functionality.
I would need to add some properties and draw in awakeFromNib or 'layoutSubviews'.
If I subclass UIControl, I can't make UIButton use that subclass.
If I subclass my UIControl subclass, I need to "recreate" UIButton, UITextField, etc.
I could subclass UIButton, UITextField and add this behaviour, but then I would have one subclass for each component to add the same functionality to them, ending with a lot of duplicated code.
I think I can not change/alter UIControl's methods in a category.
Is there a way to add this kind of functionality to UIControl and it's subclasses without reinventing the wheel, or ending up with a lot of duplicated code?
What HellBoy89 says seems the easiest route to go on. Categories can also override default implementation, which makes things practical here. Here's an example to do what you want and it's automatically applied to all UIControls and their subclasses being instantiated.
#implementation UIControl (Styling)
- (id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self applyDefaultControlStyle];
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self applyDefaultControlStyle];
}
return self;
}
-(void)applyDefaultControlStyle {
[[self layer] setCornerRadius:8];
[[self layer] setBorderColor: [[UIColor redColor] CGColor]];
[[self layer] setBorderWidth: 2];
}
#end
You can use categories to extend UIControl. You will not be able to add properties to UIControl, but you can add methods.
Please refer to this question, it may be helpful: Adding a property to all of my UIControls

Using a UICollectionView over an SKScene

I have an app with several SKScenes. To keep it snappy, I have a single UIViewController, that handles 1 SKView.
I'm trying to add a UICollectionView to one of the SKScenes. However the problem comes when I try to set the delegate for the collection view to the SKScene initialising it.
I initialise it here:
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size]) {
//Initialise collectionView
UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height) collectionViewLayout:layout];
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView setBackgroundColor:[UIColor redColor]];
}
return self;
}
After initialising, I add the collection view as a subview of the UIViewController's view by calling:
- (void)didMoveToView:(SKView *)view
{
[self.view addSubview:_collectionView];
}
But the delegate or datasource methods aren't called. I've set up the header:
#interface BrowseScene : SKScene <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
I'm guessing that despite setting self as the delegate, once the collection view is added, it is added as a subview of the UIViewController's SKView, and therefore has no reference to the SKScene that initialised it. I tried calling:
_collectionView.delegate = self.view.scene;
But I get a compiler error saying SKScene is an incompatible type for UICollectionViewDelegate.
So my question is, how best to approach this?
It turns out, the problem lay in how I was initialising the UICollectionView, not where. And it's perfectly ok to set an SKScene as the delegate class.
The main reason the delegates weren't being called was because I'd initialised the collectionView with an empty layout object (as UICollectionViewLayout is just an abstract class). I initialised with a standard UICollectionViewFlowLayout instead, and the methods were then called when the class initialised.
I was also missing a call to
[_collectionView registerClass:[collectionViewCell class] forCellWithReuseIdentifier:#"collectionViewCell"];
(In the complete code, I had calls to reloadData, so that wasn't the problem here).

How to write a custom initializer that prevents viewDidLoad being called?

I would like to build a custom init method for a UIViewController, but after digging around on the Internet and specifically in SO I am confused about designated initializers.
I have a subclass of an UIViewController with these two initializers:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if ( self ) {
}
return self;
}
- (id) initWithFilename:(NSString *)aFilename {
self = [self initWithNibName:#"WallpaperDetailsViewController" bundle:nil];
if ( self ) {
self.filename = aFilename;
}
return self;
}
Then I have a viewDidLoad method that customizes the view according to the filename property:
- (void)viewDidLoad {
[super viewDidLoad];
// Create a UIImageView to display the wallpaper
self.wallpaper = [[UIImageView alloc] initWithImage:[UIImage imageNamed:self.filename]];
// ...
}
In another UIViewController I make the following call:
WallpaperDetailsViewController *detailsViewController = [[WallpaperDetailsViewController alloc] initWithFilename:#"foobar.png"];
[[self navigationController] pushViewController:detailsViewController animated:YES];
The result is that viewDidLoad is being called as a consequence of [self initWithNibName:], which does not initialize the UIImageView because self.filename is null.
According to other SO questions and answers, that should be the expected behavior. I am not sure about this because of my own experience in other projects prior to iOS 5. My question is:
How can I ensure that viewDidLoad: is call after initWithFilename: and not between initWithFilename: and initWithNibNameOrNil:bundle:?
If that's not possible, how can I implement an initializer method that receives custom data to create and customize the view?
Thanks!
I have found the problem.
WallpaperDetailsViewController does not inherit directly from UIViewController, but from another custom UIViewController I have implemented.
And what was the problem? That I have initialized a subview in the parent's initWithNibName method, instead of following the lazy-load technique and doing it in viewDidLoad. When WallpaperDetailsViewController was calling its parent initializer it got messy and cause viewDidLoad not to behave properly.
The solution? I moved every subview initialization in the parent class to its viewDidLoad method, and keep my original implementation of WallpaperDetailsViewController intact. Now everything is working as expected
Thanks to #Josh Caswell and #logancautrell
You don't need that empty implementation of initWithNibName:bundle:. Furthermore, it looks like your class here is establishing its designated initializer to be initWithFilename: If that's true, initWithFilename: should be calling the superclass's D.I.:
- (id) initWithFilename:(NSString *)aFilename {
// Call super's designated initializer
self = [super initWithNibName:#"WallpaperDetailsViewController"
bundle:nil];
if ( self ) {
self.filename = aFilename;
}
return self;
}
The rule is that all initializers within a class should call the class's D.I., and the D.I. should itself call the superclass's D.I.
It's not completely clear from what you've posted why loadView: is being called before your initializer has completed. Logancautrell's comment suggesting setting breakpoints in the view loading methods is good.
Why don't you just use a custom setter for the filename property that initializes the UIImage every time the filename is set?
Or, alternately, set the UIImage from the filename property in viewWillAppear: instead of viewDidLoad.
First, it is not recommended that you use dot syntax within your initializer. See the following for some good discussion:
Objective-C Dot Syntax and Init
Second, what you could do is assign the image in your initializer as well. So you could do something along the lines of
- (id) initWithFilename:(NSString *)aFilename {
self = [self initWithNibName:#"WallpaperDetailsViewController" bundle:nil];
if ( self ) {
filename = [aFilename retain];
wallpaper = [[UIImageView alloc] initWithImage:[UIImage imageNamed:aFileName]];
}
return self;
}
This will allow you to get everything setup and in good shape before viewDidLoad is called.
Good Luck!

Working on custom component: subclass UIView or UIViewController?

I'm working on a custom implementation of UISegmentedControl.
I'd like to create a component that able to receive config data and from which obtain a custom View similar to UISegmentedControl.
I started subclassing a UIView and i can create a custom UISegmentedControl with this code:
CustomSegment *segment = [[CustomSegment alloc]
initWithTitles:[NSArray arrayWithObjects:#"one",#"two",nil]];
[self.window addSubview:segment];
But now i'd like to improve my class and add some more customizable parameters to it.
For example i'd like add a custom separators, define the button fonts and so on... here my doubt:
Is it better to work on a UIView subClass or you suggest me to subclass a UIViewController, where i can manage View hierarchy in method like -(void)loadView and -(void)viewDidLoad ?
In a simple UIView subclass, when i launch the custom init method, i setup immediately subviews... while using a UIViewController i can call custom init and define how my subview is builded into -(void)loadView.
Don't use an UIViewController, just extend the UIView class like you did and keep extending its functionality.
Remember to save a pointer to each subview you add (i.e. buttons) in order to be able to access them later.
Define custom setters, for example, a custom setter for changing a button label title would be:
- (void) setButton1Title:(NSString*)str forState:(UIControlState)state{
//You can add some control here
if ([str length] > 20) return;
[_button1 setTitle:str forState:state]; //_button1 is my reference to the button
}
And so on. Don't provide direct access to your subviews, use methods instead.
Also, you can use "layoutSubviews" method to define how your views are going to be displayed in your custom view.
Hope it helps you.
Edit: In your case, I don't see why using lauoutSubviews method but I want to show you what I was trying to say.
Lets say that for example I need to create an UIView class to represent a "Contact" object in my application.
This is what I would do:
#interface ContactView : UIView{
UILabel* _nameLabel;
UILabel* _ageLabel;
Contact* _contact;
}
#property (retain) Contact* contact;
#end
#implementation ContactView
#synthetize contact = _contact;
-(id)initWithContact:(Contact*)c{
self = [super init];
if (self) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.frame = CGRectZero;
[self addSubview:_nameLabel];
[_nameLabel release];
_ageLabel = [[UILabel alloc] init];
_ageLabel.frame = CGRectZero;
[self addSubview:_ageLabel];
[_ageLabel release];
self.contact = c;
}
}
- (void) layoutSubviews{
[super layoutSubviews];
_nameLabel.frame = CGRectMake(0.0f, 0.0f, 200.0f, 25.0f);
_ageLabel.frame = CGRectMake(0.0f, 25.0f, 200.0f, 25.0f);
if (self.contact){
_nameLabel.text = self.contact.name;
_ageLabel.text = self.contact.age;
}else{
_nameLabel.text = #"Unavailable";
_ageLabel.text = #"Unavailable";
}
}
- (void) setContact:(Contact*)c{
self.contact = c;
[self layoutSubviews];
}
#end
Check out how the "layoutSubiews" is used to set the correct frame and data to the labels.
Usually, I use it a lot when creating custom UITableViewCells where you have to reuse the view.
Let me know if I'm being confusing.

Subviews not showing up in UIView class

I'm trying to lay out some images in code using addSubview, and they are not showing up.
I created a class (myUIView) that subclasses UIView, and then changed the class of the nib file in IB to be myUIView.
Then I put in the following code, but am still getting a blank grey screen.
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Initialization code
[self setupSubviews];
}
return self;
}
- (void)setupSubviews
{
self.backgroundColor = [UIColor blackColor];
UIImageView *black = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"black.png"]];
black.center = self.center;
black.opaque = YES;
[self addSubview:black];
[black release];
}
yes, just implement initWithCoder.
initWithFrame is called when a UIView is created dynamically, from code.
a view that is loaded from a .nib file is always instantiated using initWithCoder, the coder takes care of reading the settings from the .nib file
i took the habit to do the initialization in a separate method, implementing both initWithCode and initWithFrame (and my own initialization methods when required)
try implementing initWithCoder: sometimes I've had trouble with IB and initWithFrame:
or at least add a logging call to see if your init method is executed