4

I have read so many positive things about using blocks - in particular that it simplifys the code by elimanting delegate calls. I have found examples where blocks are used at end of animation instead of delegate calls - there I understand how it can be done.

But I would really like to know if the cumbersome scheme of having to use delegates when presenting and dismissing viewcontrollers can also be simplified with blocks.

The standard recommended way to display and dismiss scheme looks like this, where in VC1 a new VC2 is presented which is dismissed by the delegate in VC1 again.

  VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil ];
  vc2.delegate = self; 
  [self presentModalViewController: vc2 animated: YES]; // or however you present your VC2

Withis VC2 returning to vc1:

[self.delegate vc2HasFinished];

For this to work one has to create a protocol like this: in VC2Protocol.h file

 @protocol VC2Protocol <NSObject>
    -(void)vc2HasFinished;
    @end

Then include this VC2Protocol.h in VC1 and VC2. In VC1 one has to define this method like this:

-(void) weitereRufNrVCDidFinish{
    [self dismissModalViewControllerAnimated:YES completion: nil];

}

Would really be nice if one can write more concise code, avoiding having to declare a protocol just for that.

Thanks!

user387184
  • 10,953
  • 12
  • 77
  • 147

4 Answers4

4

For the case of dismissing a modally presented vc, please be aware that the vc can dismiss itself. So instead of [self.delegate vc2HasFinished]; you can just say [self dismissModalViewControllerAnimated:YES completion: nil]; within vc2.

But I agree with you that blocks useful and delegates are clumsy (and more error prone, especially pre-ARC). So here's how you can replace delegate callbacks in a vc. Let's invent a situation where the vc would want to tell it's delegate something, say for example, that it just fetched an image...

// vc2.h
@property (nonatomic, copy) void (^whenYouFetchAnImage)(UIImage *);
// note, no delegate property here

// vc2.m
// with your other synthesizes
@synthesize whenYouFetchAnImage=_whenYouFetchAnImage;

// when the image is fetched
self.whenYouFetchAnImage(theFetchedImage);

The presenting vc doesn't set a delegate, but it does give the new vc some code to run (in it's own execution context) when an image is fetched...

// presenting vc.m
VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];

// say this presenting vc has an image view that will show the image fetched
// by vc2.  (not totally plausible since this image view will probably be covered by vc2
// when the block is invoked)
vc2.whenYouFetchAnImage = ^(UIImage *image) { self.myImageView.image = image; };
danh
  • 62,181
  • 10
  • 95
  • 136
3

Blocks can be used as properties/ivars on VC2, so you could have a completionBlock that VC1 sets to be whatever it wants it to be and VC2 would be able to call it as completionBlock(); once it's done.

Basically:

typedef void (^VC2CompletionBlock)(void);
@interface VC2 : UIViewController {
     VC2CompletionBlock completionBlock;
}

@property (nonatomic, copy) VC2CompletionBlock completionBlock;

@end

And then somewhere in VC2.m you can call

...
self.completionBlock();
...

Typedef'ing your blocks allows you to create a new custom type of blocks, perhaps one with a return value or some other parameter, which can then be passed to the block by VC2

typedef void (^VC2CompletionBlock)(BOOL success, NSData data);

I hope this helps, using blocks is powerful, as the object itself only needs to know the basic structure of the block (i.e the parameters it is capable of taking in), it does not need any info about the block or who created it.

Caution though, blocks may cause weird memory issues, so make sure to read the suitable documentation on them.

Henri Normak
  • 4,695
  • 2
  • 23
  • 33
  • Looking at Bernd Rabe's answer, he is right, dismissing the controller is best done via the mentioned method, however replacing delegate protocols with blocks is still a viable strategy. – Henri Normak Sep 02 '12 at 17:49
  • 1
    Agree with Henri and Bernd. Regarding weird memory issues, the specific danger Henri is referring to are retain cycles. The block copy retains all the objects to which it refers (that's how it can run in the caller's execution context). If any of those objects are the block's owner, it's a cycle. The workaround is to declare unretained copies of those objects when using in the block. Here's a decent so ref...http://stackoverflow.com/questions/4352561/retain-cycle-on-self-with-blocks – danh Sep 02 '12 at 17:56
  • also agree with Henri that typedefs clean up the syntax. – danh Sep 02 '12 at 17:59
2

You can solve this in a general way, without have to create specific block iVars in all controllers. You could create a class provides "when done" block processing, and then just inherit from it, and your view controllers will all have "when done" ability. You can just set the property or provide a "convenience" method.

Note, the original code for this first piece was just broken, so I changed it. -- Ugh. How embarrassing. Anyway, you get the idea (and I only suggest this if you are one who abhors associations).

// Use this as a base class for your view controllers...
typedef void(^WhenDoneWithViewControllerBlock)(
    UIViewController *viewController,
    BOOL canceled);
@interface BlockDismissingViewController : UIViewController
@property (nonatomic, strong) WhenDoneWithViewControllerBlock whenDone;
- (void)done:(BOOL)canceled;
@end

@implementation BlockDismissingViewController
- (void)done:(BOOL)canceled {
    if (self.whenDone) {
        self.whenDone(self, canceled);
    }
}
@end

// The "convenience" method should probably be something like this...
@implementation UIViewController (BlockDismissingViewController)
- (void)presentViewController:(BlockDismissingViewController *)viewControllerToPresent
                     animated:(BOOL)flag
                   completion:(void (^)(void))completion
                     whenDone:(WhenDoneWithViewControllerBlock)whenDone {
    viewControllerToPresent.whenDone = whenDone;
    [self presentViewController:viewControllerToPresent
                        animated:flag
                      completion:completion];
}
@end

Or, you could do it as a category on UIViewController, and now all your view controllers will get this functionality. You can use the notification center to invoke the appropriate block...

@interface UIViewController (WhenDoneWithViewControllerBlock)
- (void)done:(BOOL)canceled;
@end

@implementation UIViewController (WhenDoneWithViewControllerBlock)
- (void)presentViewController:(UIViewController *)viewControllerToPresent
                     animated:(BOOL)flag
                   completion:(void (^)(void))completion
                     whenDone:(WhenDoneWithViewControllerBlock)doneBlock {
    if (doneBlock) {
        __block id observer = [[NSNotificationCenter defaultCenter]
                               addObserverForName:@"DoneWithViewControllerNotification"
                               object:viewControllerToPresent
                               queue:nil
                               usingBlock:^(NSNotification *note) {
            [[NSNotificationCenter defaultCenter] removeObserver:observer];
            doneBlock(viewControllerToPresent, [[note.userInfo objectForKey:@"canceled"] boolValue]);
        }];
    }
    [self presentViewController:viewControllerToPresent
                       animated:flag
                     completion:completion];
}

- (void)done:(BOOL)canceled {
    [[NSNotificationCenter defaultCenter]
        postNotificationName:@"DoneWithViewControllerNotification"
                      object:self
                    userInfo:@{ @"canceled" : @(canceled) }];
}
@end

Or, if you still want a category, but want an iVar and to bypass the notification center...

// Using associated objects in a category
@interface UIViewController (WhenDoneWithViewControllerBlock)
@property (nonatomic, strong) WhenDoneWithViewControllerBlock whenDone;
- (void)done:(BOOL)canceled;
@end
@implementation UIViewController (WhenDoneWithViewControllerBlock)
char const kWhenDoneKey[1];
- (WhenDoneWithViewControllerBlock)whenDone {
    return objc_getAssociatedObject(self, kWhenDoneKey);
}
- (void)setWhenDone:(WhenDoneWithViewControllerBlock)whenDone {
    objc_setAssociatedObject(self, kWhenDoneKey, whenDone, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)presentViewController:(UIViewController *)viewControllerToPresent
                     animated:(BOOL)flag
                   completion:(void (^)(void))completion
                     whenDone:(WhenDoneWithViewControllerBlock)whenDone {
    viewControllerToPresent.whenDone = whenDone;
    [self presentViewController:viewControllerToPresent animated:flag completion:completion];
}

- (void)done:(BOOL)canceled {
    if (self.whenDone) {
        self.whenDone(self, canceled);
    }
}
@end

Of course, these are just examples, but hopefully you get the idea.

When your view controller is done, it just calls

[self done:canceledOrSuccess];

and the block will be invoked.

Using the last category is my favorite, even though there is a performance cost in both time and memory for associated objects. You get the convenience of an "iVar" that holds your "whenDone" block (you can set it explicitly), and you get the "convenience" method for presenting, and every view controller automatically gets this functionality, just by adding the category.

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • wow - thank you very much for this elaborate answer - I will try the last approach as you said - even though I did not quite understand all of it reading it once... – user387184 Sep 03 '12 at 01:26
-1

The recommended way of dismissing a view controller is dismissViewControllerAnimated:completion. So here is your block you where looking for.

Bernd Rabe
  • 790
  • 6
  • 23
  • Thanks, but this is not what I have asked, I like to simplify the delegate that dismisses the VC, if possible! – user387184 Sep 02 '12 at 17:03
  • Well you can call that method on the vc itself. It is automatically forwarded then to your delegate or presenting vc. – Bernd Rabe Sep 02 '12 at 17:05