Using CALayer Delegate - iphone

I have a UIView whose layers will have sublayers. I'd like to assign delegates for each of those sublayers, so the delegate method can tell the layer what to draw. My question is:
What should I provide as CALayer's delegate? The documentation says not to use the UIView the layers reside in, as this is reserved for the main CALayer of the view. But, creating another class just to be the delegate of the CALayers I create defeats the purpose of not subclassing CALayer. What are people typically using as the delegate for CALayer? Or should I just subclass?
Also, why is it that the class implementing the delegate methods doesn't have to conform to some sort of CALayer protocol? That's a wider overarching question I don't quite understand. I thought all classes requiring implementation of delegate methods required a protocol specification for implementers to conform to.

Preferring to keep the layer delegate methods in my UIView subclass, I use a basic re-delegating delegate class. This class can be reused without customization, avoiding the need to subclass CALayer or create a separate delegate class just for layer drawing.
#interface LayerDelegate : NSObject
- (id)initWithView:(UIView *)view;
#end
with this implementation:
#interface LayerDelegate ()
#property (nonatomic, weak) UIView *view;
#end
#implementation LayerDelegate
- (id)initWithView:(UIView *)view {
self = [super init];
if (self != nil) {
_view = view;
}
return self;
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
NSString *methodName = [NSString stringWithFormat:#"draw%#Layer:inContext:", layer.name];
SEL selector = NSSelectorFromString(methodName);
if ([self.view respondsToSelector:selector] == NO) {
selector = #selector(drawLayer:inContext:);
}
void (*drawLayer)(UIView *, SEL, CALayer *, CGContextRef) = (__typeof__(drawLayer))objc_msgSend;
drawLayer(self.view, selector, layer, context);
}
#end
The layer name is used to allow for per-layer custom draw methods. For example, if you have assigned a name to your layer, say layer.name = #"Background";, then you can implement a method like this:
- (void)drawBackgroundLayer:(CALayer *)layer inContext:(CGContextRef)context;
Note, your view will need a strong reference the instance of this class, and it can be used as the delegate for any number of layers.
layerDelegate = [[LayerDelegate alloc] initWithView:self];
layer1.delegate = layerDelegate;
layer2.delegate = layerDelegate;

The lightest-wight solution would be to create a small helper class in the the file as the UIView that's using the CALayer:
In MyView.h
#interface MyLayerDelegate : NSObject
. . .
#end
In MyView.m
#implementation MyLayerDelegate
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
. . .
}
#end
Just place those at the top of your file, immediately below the #import directives. That way it feels more like using a "private class" to handle the drawing (although it isn't -- the delegate class can be instantiated by any code that imports the header).

Take a look at the docs on formal vs informal protocols. The CALayer is implementing an informal protocol which means that you can set any object to be its delegate and it will determine if it can send messages to that delegate by checking the delegate for a particular selector (i.e. -respondsToSelector).
I typically use my view controller as the delegate for the layer in question.

A note regarding "helper" classes for use as a layer's delegate (with ARC at least):
Make sure you store a "strong" reference to your alloc/init'd helper class (such as in a property). Simply assigning the alloc/init'd helper class to the delegate seems to cause crashes for me, presumably because mylayer.delegate is a weak reference to your helper class (as most delegates are), so the helper class gets freed up before the layer can use it.
If I assign the helper class to a property, then assign it to the delegate, my weird crashes go away, and things behave as expected.

I personally voted for Dave Lee's solution above as being the most encapsulating, particularly where you have multiple layers. However; when I tried it on IOS 6 with ARC I got errors on this line and suggesting that I need a bridged cast
// [_view performSelector: selector withObject: layer withObject: (id)context];
I therefore amended Dave Lee's drawLayer method from his re-delegating delegate class to employ NSInvocation as below. All usage and ancillary functions are identical to those Dave Lee posted on his earlier excellent suggestion.
-(void) drawLayer: (CALayer*) layer inContext: (CGContextRef) context
{
NSString* methodName = [NSString stringWithFormat: #"draw%#Layer:inContext:", layer.name];
SEL selector = NSSelectorFromString(methodName);
if ( ![ _view respondsToSelector: selector])
{
selector = #selector(drawLayer:inContext:);
}
NSMethodSignature * signature = [[_view class] instanceMethodSignatureForSelector:selector];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:_view]; // Actually index 0
[invocation setSelector:selector]; // Actually index 1
[invocation setArgument:&layer atIndex:2];
[invocation setArgument:&context atIndex:3];
[invocation invoke];
}

I prefer the following solution. I would like to use the drawLayer:inContext: method of the UIView to render a subview that I might add without adding extra classes all over the place. My solution is as follows:
Add the following files to your project:
UIView+UIView_LayerAdditions.h with contents:
#interface UIView (UIView_LayerAdditions)
- (CALayer *)createSublayer;
#end
UIView+UIView_LayerAdditions.m with contents
#import "UIView+UIView_LayerAdditions.h"
static int LayerDelegateDirectorKey;
#interface LayerDelegateDirector: NSObject{ #public UIView *view; } #end
#implementation LayerDelegateDirector
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
[view drawLayer:layer inContext:ctx];
}
#end
#implementation UIView (UIView_LayerAdditions)
- (LayerDelegateDirector *)director
{
LayerDelegateDirector *director = objc_getAssociatedObject(self, &LayerDelegateDirectorKey);
if (director == nil) {
director = [LayerDelegateDirector new];
director->view = self;
objc_setAssociatedObject(self, &LayerDelegateDirectorKey, director, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return director;
}
- (CALayer *)createSublayer
{
CALayer *layer = [CALayer new];
layer.contentsScale = [UIScreen mainScreen].scale;
layer.delegate = [self director];
[self.layer addSublayer:layer];
[layer setNeedsDisplay];
return layer;
}
#end
Now add the header to your .pch file. If you add a layer using the createSublayer method, it will automagically show up without bad allocs in the override to drawLayer:inContext:. As far as I know the overhead of this solution is minimal.

It's possible to implement a delegation without resorting to a strong ref.
NOTE: The basic concept is that you forward the delegate call to a selector call
Create a selector instance in the NSView you want to get the delegation from
implement the drawLayer(layer,ctx) in the NSView you want to get the delegation from call the selector variable with the layer and ctx vars
set the view.selector to a handleSelector method where you then retrieve the layer and ctx (this can be anywhere in your code, weak or strongly referenced)
To see an example of how you implement the selector construction:(Permalink) https://github.com/eonist/Element/wiki/Progress#selectors-in-swift
NOTE: why are we doing this? because creating a variable outside methods whenever you want to use the Graphic class is non-sensical
NOTE: And you also get the benefit that the receiver of the delegation doesn't need to extend NSView or NSObject

Can you use the passed in layer parameter to construct a switch statement so you can put everything in this method(against the advice of the documents):
-(void) drawLayer: (CALayer*) layer inContext: (CGContextRef) context {
if layer = xLayer {...}
}
Just my 2 cents.

Related

Multiple delegates per one object?

I have a UIScrollView that I need to subclass and within the subclass I need to attach the UIScrollViewDelegate so I can implement the viewForZoomingInScrollView method.
Then I have a UIViewController where I need to instantiate an object of this UIScrollView subclass that I created, and I would also like to make the UIViewController a UIScrollViewDelegate for this object so I can implement scrollViewDidZoom in this UIViewController class.
How is it possible to make one object have two delegates? (I know I could easily just have one delegate and just implement both methods there, but for design purposes I'd like to do it the way that I'm mentioning).
Sometimes it makes sense to attach several delegates to a scroll view. In that case you can build a simple delegation splitter:
// Public interface
#interface CCDelegateSplitter : NSObject
- (void) addDelegate: (id) delegate;
- (void) addDelegates: (NSArray*) delegates;
#end
// Private interface
#interface CCDelegateSplitter ()
#property(strong) NSMutableSet *delegates;
#end
#implementation CCDelegateSplitter
- (id) init
{
self = [super init];
_delegates = [NSMutableSet set];
return self;
}
- (void) addDelegate: (id) delegate
{
[_delegates addObject:delegate];
}
- (void) addDelegates: (NSArray*) delegates
{
[_delegates addObjectsFromArray:delegates];
}
- (void) forwardInvocation: (NSInvocation*) invocation
{
for (id delegate in _delegates) {
[invocation invokeWithTarget:delegate];
}
}
- (NSMethodSignature*) methodSignatureForSelector: (SEL) selector
{
NSMethodSignature *our = [super methodSignatureForSelector:selector];
NSMethodSignature *delegated = [(NSObject *)[_delegates anyObject] methodSignatureForSelector:selector];
return our ? our : delegated;
}
- (BOOL) respondsToSelector: (SEL) selector
{
return [[_delegates anyObject] respondsToSelector:selector];
}
#end
Then simply set an instance of this splitter as a delegate of the scroll view and attach any number of delegates to the splitter. All of them will receive the delegation events. Some caveats apply, for example all the delegates are assumed to be of the same type, otherwise you’ll have trouble with the naive respondsToSelector implementation. This is not a big problem, it’s easy to change the implementation to only send delegation events to those who support them.
You don't want an object with 2 delegates. You want to keep your customScrollView keep the responsibility of its own UIScrollViewDelegate functions.
To make your parentVC respond to the delegate methods of UIScrollView as well you will have to make a custom delegate inside your customScrollView.
At the moment a UIScrollViewDelegate function gets called you will also call one of your delegate functions from your custom delegate. This way your parentVC will respond at the moment you want it to.
It will look somewhat like this.
CustomScrollView.h
#protocol CustomDelegate <NSObject>
//custom delegate methods
-(void)myCustomDelegateMethod;
#end
#interface CustomScrollView : UIScrollView <UIScrollViewDelegate>
{
id<CustomDelegate> delegate
//the rest of the stuff
CustomScrollView.m
-(void) viewForZoomingInScrollView
{
[self.delegate myCustomDelegateMethod];
//rest of viewForZoomingInScrollView code
ParentVC.h
#interface CustomScrollView : UIViewController <CustomDelegate>
{
//stuff
ParentVC.m
-(void)makeCustomScrollView
{
CustomScrollView *csv = [[CustomScrollView alloc] init];
csv.delegate = self;
//other stuff
}
-(void)myCustomDelegateMethod
{
//respond to viewForZoomingInScrollView
}
I hope this fully covers your problem.
Good luck.
Short answer: you don't. Delegates are typically a weak one-to-one relationship:
#property (nonatomic, weak /*or assign*/) id<MyViewDelegate> delegate;
Sometimes you will see a "listener" design pattern, which is the one-to-many form of delegates:
- (void) addListener:(id<MyViewListener>)listener;
- (void) removeListener:(id<MyViewListener>)listener;
In your case, there doesn't appear to be a nice public override point in UIScrollView that allows subclasses to specify the viewForZoomingInScrollView. I would avoid making the UIScrollView its own delegate, if possible. You could make the UIViewController the UIScrollViewDelegate and have it provide the viewForZooming. Or you could make an intermediate view subclass which uses UIScrollView, provides the viewForZooming, and forwards the other delegate methods up.
I don't think you can have two UIScrollViewDelegate delegates directly connected to the same object.
What you can do is having the two delegates chain-connected. I.e., you connect one delegate to the other, then have the former forward messages to the latter when it cannot handle them itself directly.
In any case, I think I am missing a bit to fully suggest a solution, namely the reason why you do need a second delegate and cannot do always through one single delegate. In other words, what I think is that there might be alternative designs that would avoid needing two delegates.
Here's another potential problem with what you're trying to do...
Let's say you have two instances of a UIScrollView and one delegate object. In the delegate object, you override scrollViewDidScroll(UIScrollView *): method of the UIScrollViewDelegate protocol.
Inside the method, you want to access the value of the contentOffset property of both scroll views because, perhaps, you have two adjacent collections views, and you're trying to get the index path of the item at the center of the collection view to get the values of properties associated with those two items (think UIDatePicker).
In that case, how do you different between scroll views? The scrollView property only refers to one scroll view; but, even if it referred to both, how do you get the value of their respective contentOffset properties?
Now, you might say, "I can create an IBOutlet for both, and use their assigned references instead of the scrollView property in the delegate method, such as self.collectionViewFirst.contentOffset and self.collectionViewSecond.contentOffset, and ignore the scrollView property of the delegate method.
The problem is this: that property isn't stored. It's only available when the delegate method is called. Why? Because there's only one delegate object, and only one contentOffset property. By scrolling another scroll view, the value of the contentOffset property would change, and not reflect the content offset of any other scroll view except the last one scrolled.
It's bad practice to do what you're trying to do, even if the case (or a case like it) as I described doesn't apply to your situation. Remember: writing code is about sharing code. Incorrect code sends a message to others that diminishes your reputation.

How to create a proxy protocol like UIAppearance

I know how to create a protocol already but I'm wondering what would be the best practice to create a proxy protocol like Apple did for the UIAppearance protocol and the implementation on certain UI classes.
Why I want to do it this way? Because I already have a lot of UI classes and I would like to centralize the implementation of the code for changing color.
Maybe an odd question but my curiosity drove me to this point.
Thanks
Just make the proxy a static object and access it through class-level methods, the same way you'd implement a singleton, e.g.
#implementation MyClass
+ (MyProxyObject *)proxy
{
static MyProxyObject *sharedProxy = nil;
if (sharedProxy == nil)
{
sharedProxy = [[MyProxyObject alloc] init];
}
return sharedProxy;
}
#end
Then for any property of your class, e.g. textColor, just have your class use the value in [[self class] proxy].textColor instead of storing its own value. E.g.
#interface MyClass : UIView
#property (nonatomic, strong) textColor
#end
#implementation MyClass
- (UIColor *)textColor
{
return textColor ?: [[self class] proxy].textColor
}
#end
If you need a way to refresh your onscreen views immediately whenever a property on the proxy is changed, you could do that by having the proxy broadcast an NSNotification in its textColor setter method, and have all the instances observe that notification and call setNeedsDisplay on themselves when they receive it.

How does respondsToSelector behave when there is a delegate present?

I recently tried to subclass UITextField and set the delegate to myself (found this trying ti solve my problem: http://www.cocoabuilder.com/archive/cocoa/241465-iphone-why-can-a-uitextfield-be-its-own-delegate.html)
#interface MyObject :UITextField <UITextFieldDelegate>
#end
#implementation MyObject
-(id) initWithFrame:(CGRect) frame
{
if((self=[super initWithFrame:frame]))
{
self.delegate=self;
}
return self;
}
-(BOOL) respondsToSelector:(SEL)selector
{
NSLog(#"responds to selector");
return [super respondsToSelector:selector];
}
// Implement all the missing methods
#end
Calling a method defined on the interface results in an infinite recursion. I don't see anything in the Apple docs that defines how respondsToSelector is supposed to behave in the presence of a delegate.
The docs for respondsToSelector states the following:
You cannot test whether an object
inherits a method from its superclass
by sending respondsToSelector: to the
object using the super keyword. [..]
Therefore, sending respondsToSelector:
to super is equivalent to sending it
to self. Instead, you must invoke the
NSObject class method
instancesRespondToSelector: directly
on the object’s superclass
It seems that this could be the cause for your recursion problem. I don't know if the delegate stuff is even related. Just a guess though.

Subclassed UIView from NIB File not typed to subclass

I have a NIB file, with some class (SomeClassView : UIView) set as the custom class of the top level view of the NIB. SomeClass has IBOutlets that I use to hookup subviews of SomeClass that I lay out in Interface Builder.
I instantiate SomeClass like this:
- (id)initWithFrame:(CGRect)frame {
self = [[[[NSBundle mainBundle] loadNibNamed:#"SomeClassView" owner:nil options:nil] objectAtIndex:0] retain];
// "SomeClassView" is also the name of the nib
if (self != nil) {
self.frame = frame;
}
return self;
}
Now say I subclass SomeClass with SubClassView. I add a method to SubClassView called -(void)foo:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
[self foo];
}
return self;
}
At this point I get a runtime error: * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SomeClassView foo:]: unrecognized selector sent to instance 0xAAAAAA'
It seems as though "self" within initWithFrame of SubClassView is still set to the super, SomeClassView. A quick hack fix to work around this is to change the isa pointer within SubClassView initWithFrame:
- (id)initWithFrame:(CGRect)frame {
Class prevClass = [self class];
self = [super initWithFrame:frame];
if (self != nil) {
isa = prevClass;
[self foo]; // works now
}
}
It's not an ideal workaround, since I have to update isa each time I subclass, or there could even be different init methods which I'll also have to update.
1) Ideally, is there an easy way to fix this, purely by code? Maybe with the way I'm setting self = the loaded nib?
2) Is there an alternative architecture that works better for what I'm trying to do? One example is to set the owner to self, but then you'd have to set all the property/outlet mappings manually.
Swapping isa pointers is a problem if your subclasses have instance variables other than those declared in SomeClassView. Note that your nib file has an object of type SomeClassView, which means that, upon loading the nib file, the nib loader will allocate an object of that type and unmarshall it from the nib file. Changing the isa pointer to [SubViewClass class] temporarily won’t make it an object of type SubViewClass per se since what the nib loader allocates is a SomeClassView object.
That said, I don’t think there’s a reliable and automatic way to use nib files containing objects whose types need to be changed upon nib loading.
What you could do is to have your SomeClassView objects declare a delegate conforming to some protocol. This protocol would define methods for behaviour in SomeClassView that can potentially be extended. For instance,
#protocol SomeClassViewDelegate
#optional
- (void)someClassViewDidAwakeFromNib:(SomeClassView *)someClassView;
#end
Instead of subclassing SomeClassView, you’d have arbitrary objects performing whatever custom behaviour you currently have in SubClassView. For instance,
#interface SubClassViewBehaviour : NSObject <SomeClassViewDelegate>
…
#end
#implementation SubClassViewBehaviour
- (void)someClassViewDidAwakeFromNib:(SomeClassView *)someClassView {
// whatever behaviour is currently in -[SubClassView foo]
}
#end
A SubClassViewBehaviour object would be created in code and set as the nib file’s owner upon loading the nib file, or any other IB proxy object for that matter. SomeClassView would have a delegate outlet connected to file’s owner/proxy object, and it’d invoke the delegate methods in the appropriate places. For instance,
#implementation SomeClassView
- (void)awakeFromNib {
SEL didAwakeFromNib = #selector(someClassViewDidAwakeFromNib:);
if ([[self delegate] respondsToSelector:didAwakeFromNib]) {
[[self delegate] performSelector:didAwakeFromNib withObject:self];
}
}
#end
One further remark: your code currently leaks a view object since two objects are being instantiated: one via +alloc in your code and another one via nib loading. You’re assigning the latter to self, hence the one created via +alloc is leaking. Also, I believe you’ve missed a call to super in your third code snippet.
Rather than do this from within the subclass itself why not ensure it is the right class when you first instantiate it from outside the class:
SubClass *instance = [[SubClass alloc] initWithNibName:#"SomeClassView" bundle:nil];

UIView and UIViewController practice for inheritance

I was just wondering if this approach looks like a good practice for apps with a lot of custom views, for nested PNG graphics and animations that may change based on user interaction. I created a BaseView class that extends UIView
#interface BaseView : UIView {
#protected
BaseViewController *controller;
}
#property (retain) BaseViewController *controller;
#end
and a corresponding controller class which is the primary location which I am putting code to manipulate the view
#interface BaseViewController : UIViewController {
#protected
CGRect drawArea;
}
- (void) drawArea:(CGRect) _drawArea;
- (CGRect) drawArea;
- (void) linkSubviewController: (BaseViewController *) _subviewController;
#end
where "drawArea" is the CGRect used to pass to the view as a frame.
"linkSubviewController" allows you to nest a controller and view as follows :
- (void) linkSubviewController: (BaseViewController *) _subviewController {
[self.view addSubview:[_subviewController view]];
}
In addition I layered another custom pair called "ImageView" and "ImageViewController" which extend BaseView but also store a UIImage and an x,y,w,h
In the "drawRect" drawing methods on views I can check to see if any vars in the self.controller vars have been changed, or assign images, for example :
UIImage *image = [(ImageViewController *)self.controller image];
CGContextDrawImage( ... ) etc
I write most of the loadView methods something like this
- (void)loadView {
ImageView *v = [[ImageView new] initWithFrame:drawArea];
v.controller = self;
self.view = v;
}
The base "initWithFrame" routine contains
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
So I can load a variety of images with transparent backgrounds without having to assign that each time.
I've been able to use this code throughout my app and it seems to make it easy to write a top level class which assembles the layout of custom items. For animations I've been putting them in the controllers and manipulating self.view.layer.
Basically I am looking for feedback, I am new with Objective-C and the IPhone SDK
There are several issues here:
Using [[Classname new] init...] is incorrect usage of new. Using new is short for [[Classname alloc] init] so you are effectively calling init twice.
Views shouldn't really need to know who is controlling them.
Your view is retaining the controller, and since UIViewController retains its view, you have a retain cycle and neither will ever be fully released.
If you want this type of behavior (whereby a view can delegate its drawing to a parent), try creating a DrawDelegate protocol, have your controllers implement that protocol, and in your view subclass have a non-retaining drawDelegate property:
#protocol DrawDelegate
- (void) drawArea:(CGRect)rect;
#end
#interface BaseView : UIView {
id<DrawDelegate> drawDelegate;
}
#property (assign) id<DrawDelegate> drawDelegate;
#end