Kela
Kela

Reputation: 3

Keep object reference of local variable in ARC

I have created a class under ARC with some methods that accepts blocks. The problem is app keep crashing, and I think the reason of crash is the object is getting released by ARC. My question is, how can I fix this, i'e how can I keep the reference of object so the object wont be released until the block has been processed.

Here is .h class

#if NS_BLOCKS_AVAILABLE
typedef void (^KelaMagicalControlCompletionBlock)();
#endif

@interface KelaMagicalControl : NSObject

+(KelaMagicalControl *)controlWithTitle:(NSString *)title message:(NSString *)message;
-(id)initWithTitle:(NSString *)title message:(NSString *)message;

-(void)showWithTouchCompletionBlock:(KelaMagicalControlCompletionBlock)completionBlock;

@end

Here is .m class

#import "KelaMagicalControl.h"

@interface KelaMagicalControl()

@property (nonatomic, strong) NSString * title;
@property (nonatomic, strong) NSString * message;

@property (copy) KelaMagicalControlCompletionBlock completionBlock;

@end

@implementation KelaMagicalControl

-(void)dealloc
{
   NSLog(@"deallocated");
}

+ (KelaMagicalControl *)toastWithTitle:(NSString *)title message:(NSString *)message
{
   KelaMagicalControl * magicalControl = [[KelaMagicalControl alloc] initWithTitle:title   message:message];
   return magicalControl;
}
-(id)initWithTitle:(NSString *)title message:(NSString *)message
{
    if(self = [super init])
    {
        _title = title;
        _message = message;
    }
    return self;
}

-(void)showWithTouchCompletionBlock:(void (^)())completionBlock
{

    UIWindow * mainWindow = [[UIApplication sharedApplication]keyWindow];
    UIView * viewTemp = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 300, 100)];
    [viewTemp setTag:10001];
    [viewTemp setBackgroundColor:[UIColor redColor]];
    [mainWindow addSubview:viewTemp];

    UITapGestureRecognizer * tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mainViewTapped)];
    [viewTemp addGestureRecognizer:tapGestureRecognizer];

    self.completionBlock = completionBlock;

}

-(void)mainViewTapped
{
    if(self.completionBlock)
    {
        self.completionBlock();
        self.completionBlock = nil;
    }
}

From controller class, I send message to custom class' methods like this:

-(IBAction)showMagicalControl:(id)sender
{
    NSString * title = @"Title";
    NSString * message = @"This is a very long message";


    KelaMagicalControl * magicalControl = [[KelaMagicalControl alloc] initWithTitle:title message:message];
    [magicalControl showWithTouchCompletionBlock:^{
        NSLog(@"control tapped");
    }];
}

So it shows the control fine, but when I tap on it, instead of executing block, it crashes with error "obj_msgsend". It doesn't even reach on showMagicalControl method. I think, as I am using ARC, it is getting released automatically, I can see dealloc gets called immediately (before executing block). It doesn't crash if I create a property of the magicalRecord and use it, but as per my requirement, I dont want to use a global iVar or property just to call this code of block.

Any suggestions please?

Upvotes: 0

Views: 1750

Answers (3)

Ultrakorne
Ultrakorne

Reputation: 1303

You are right, magicalControl get deallocated because it ends his scope. I have not tested the following but it should work.

KelaMagicalControl * magicalControl = [[KelaMagicalControl alloc] initWithTitle:title message:message];
    [magicalControl showWithTouchCompletionBlock:^{
        KelaMagicalControl *retainedVar = magicalControl;
        NSLog(@"control tapped");
    }];

declaring a strong reference inside the block will retain magicalControl.

Upvotes: 0

newacct
newacct

Reputation: 122489

One solution is to use a blocks API for UIGestureRecognizer (there are many versions of this floating around the Internet), and then in the block, call [self mainViewTapped]. This retains your KelaMagicalControl and will ensure that the KelaMagicalControl will be available for as long as the gesture recognizer may call it.

Upvotes: 0

David
David

Reputation: 1152

The problem is that your KelaMagicalControl is getting released at the end of the showMagicalControl: method, it's not getting retained anywhere. Only the UIView that you created inside showWithTouchCompletionBlock: is getting retained because you added it to a superview, in this case the window. That's why the the popup is shown correctly. But a target is always unsafe_unretained, so when you tap that view, the gestureRecognizer will try to call your KelaMagicalControl which was already released, so you get a crash.

You can easily solve this by making your KelaMagicalControl a subclass of UIView. I quickly typed out the changed you'd have to make:

.h file

#import <UIKit/UIKit.h>

#if NS_BLOCKS_AVAILABLE
typedef void (^KelaMagicalControlCompletionBlock)();
#endif

@interface KelaMagicalControl : UIView
{
    NSString* _title;
    NSString* _message;
}

-(id)initWithTitle:(NSString *)title message:(NSString *)message;
-(void)showWithTouchCompletionBlock:(KelaMagicalControlCompletionBlock)completionBlock;

@end

.m file

#import "KelaMagicalControl.h"

@interface KelaMagicalControl()

@property (nonatomic, strong) NSString * title;
@property (nonatomic, strong) NSString * message;

@property (copy) KelaMagicalControlCompletionBlock completionBlock;

@end

@implementation KelaMagicalControl

-(void)dealloc
{
    NSLog(@"deallocated");
}

+ (KelaMagicalControl *)toastWithTitle:(NSString *)title message:(NSString *)message
{
    KelaMagicalControl * magicalControl = [[KelaMagicalControl alloc] initWithTitle:title   message:message];
    return magicalControl;
}

-(id)initWithTitle:(NSString *)title message:(NSString *)message
{
    self = [super initWithFrame:CGRectMake(10, 10, 300, 300)];
    if (self)
    {
        _title = title;
        _message = message;
    }
    return self;
}

-(void)showWithTouchCompletionBlock:(void (^)())completionBlock
{
    UIWindow * mainWindow = [[UIApplication sharedApplication]keyWindow];
    [self setTag:10001];
    [self setBackgroundColor:[UIColor redColor]];
    [mainWindow addSubview:self];

    UITapGestureRecognizer * tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mainViewTapped)];
    [self addGestureRecognizer:tapGestureRecognizer];

    self.completionBlock = completionBlock;
}

-(void)mainViewTapped
{
    if(self.completionBlock)
    {
        self.completionBlock();
        self.completionBlock = nil;
    }
}
@end

Since your KelaMagicalControl is now the UIView that you're showing, it will automatically be retained since it has a superview. When you tap the view the completion block will now execute as you want. Make sure that at the end of the completionblock you remove it from its superview otherwise it'll never get released.

Upvotes: 1

Related Questions