Gyfis
Gyfis

Reputation: 1174

iOS - Setting block to block directly does not work

The title says it, I'd like to ask why the following does not work, as it should imho.

// ViewController.m
#import "B.h"
...

@implementation ViewController
{
    B *bInstance;
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
    {
        bInstance = [[B alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        [bInstance setBlockCalled:^(NSDictionary *dict) {
            NSLog(@"%@", dict[@"key"]);
        }];

        [self.view addSubview:bInstance];
    }
    return self;
}


// B.h
#import <UIKit/UIKit.h>

@interface B : UIView
@property (nonatomic, copy) void (^blockCalled)(NSDictionary *);
@end

// B.m
#import "B.h"
#import "A.h"

@implementation B
{
    A *aInstance;
    void (^blockCalled)(NSDictionary *);
}

@synthesize blockCalled;

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        aInstance = [[A alloc] initWithFrame:frame];
        [aInstance setBlockCalled:blockCalled];

        [self addSubview:aInstance];
    }
    return self;
}

@end


// A.h
#import <UIKit/UIKit.h>

@interface A : UIView

@property (nonatomic, copy) void (^blockCalled)(NSDictionary *);

@end

// A.m
#import "A.h"

@implementation A
{
    void (^blockCalled)(NSDictionary *);
}

@synthesize blockCalled;

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button setFrame:CGRectMake(0, 0, 100, 100)];
        [button setBackgroundColor:[UIColor redColor]];
        [button addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];

        [self addSubview:button];
    }
    return self;
}

- (void) buttonClicked
{
    blockCalled(@{@"key":@"value"});
}

@end

What I want to do is 'traverse up the view hiearchy', and as far as I see it, I'm assigning a block variable with the same arguments, so I'd expect it to work. Any reason why this is a wrong idea?

EDIT: added more complete example as of when this issue might happen.

EDIT2: added MCVE, which I've tested.

After I've checked the MCVE, the code crashes on the line blockCalled(@{@"key":@"value"}); in A.m because the blockCalled is nil.

Updated question: I'd like to know why calling [aInstance setBlockCalled:blockCalled] doesn't set the blockCalled in A, as it seems to me to be the same as

[aInstance setBlockCalled:^(NSDictionary *dict) 
{ 
    __strong typeof (self) strongSelf = self; 
    strongSelf.blockCalled(dict); 
}];

Upvotes: 1

Views: 146

Answers (2)

Mercurial
Mercurial

Reputation: 2165

@implementation ViewController
{
    B *bInstance;
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
    {
        bInstance = [[B alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        [bInstance setBlockCalled:^(NSDictionary *dict) {
            NSLog(@"%@", dict[@"key"]);
        }];

        [self.view addSubview:bInstance];
    }

At the moment of initializing bInstance, you haven't set the block yet. It means

aInstance = [[A alloc] initWithFrame:frame];
[aInstance setBlockCalled:blockCalled];

is called before

[bInstance setBlockCalled:^(NSDictionary *dict) {
    NSLog(@"%@", dict[@"key"]);
}];

You should override the block setter in B and call it on A.

// B.m
-(void)setBlockCalled(void(^)(NSDictionary*))passedBlock{
      [a setBlockCalled:passedBlock];
}

Upvotes: 1

Rob
Rob

Reputation: 437552

The code in your question (a) uses ivars and @synthesize statements that are unnecessary; and (b) this code snippet is insufficient to reproduce the crash you describe.

Having said that, there are two possible source of crashes that are suggested by the code sample in the question, namely: (a) the code fails to remove the observer if A is deallocated; and (b) it really should double-check to make sure that blocks are non-nil before trying to call them.

But, consider the following:

// A.h

@interface A : UIView
@property (nonatomic, copy) void (^blockCalled)(NSDictionary *dict);
@end

// A.m

@implementation A

- (instancetype) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"kNotification" object:nil];
    }
    return self;
}

- (void)handleNotification:(NSNotification *)notification {
    // never just call `blockCalled`; always check to see if not null

    if (self.blockCalled) {
        // everything is good, so let's call the block

        self.blockCalled(notification.userInfo);
    }
}

- (void)dealloc {
    // never just `addObserver`; make sure to remove the observer when this is deallocated

    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"kNotification" object:nil];
}

@end

// B.h

@interface B : UIView
@property (nonatomic, copy) void (^blockCalled)(NSDictionary *dict);
@property (nonatomic, strong) A *aInstance;
@end

// B.m
@implementation B

- (void) someMethod {
    // !!!!! why this crashes the app when blockCalled on aInstance is called:
    [self.aInstance setBlockCalled:self.blockCalled];

    // but this does not crash when the same happens
    __weak typeof (self) weakSelf = self;

    self.aInstance.blockCalled = ^(NSDictionary *dict) {
        __strong typeof (self) strongSelf = weakSelf;

        // again, never just call `blockCalled`; always check to see if not null

        if (strongSelf.blockCalled) {
            strongSelf.blockCalled(dict);
        }
    };
}

@end

Bottom line, when I fix the two obvious sources of crashes (failing to remove observer and failing to check to make sure the block was non-nil before calling it), and test it with logical scenario, it seems to work fine:

- (void)viewDidLoad {
    [super viewDidLoad];

    B *b = [[B alloc] init];
    b.blockCalled = ^(NSDictionary *dict) {
        NSLog(@"%@", dict);
    };
    A *a = [[A alloc] init];
    b.aInstance = a;
    [b someMethod];

    [self.view addSubview:a];
    [self.view addSubview:b];

    NSDictionary *dict = @{@"foo": @"bar", @"baz": @"qux"};
    [[NSNotificationCenter defaultCenter] postNotificationName:@"kNotification" object:nil userInfo:dict];
}

So, assuming one of these two issues wasn't the source of the problem, you must provide MCVE that we can use to reproduce the crash you describe.

Upvotes: 0

Related Questions