user387184
user387184

Reputation: 11053

simplifying delegate scheme with blocks - is it possible in this context?

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!

Upvotes: 4

Views: 545

Answers (4)

danh
danh

Reputation: 62676

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; };

Upvotes: 4

Jody Hagins
Jody Hagins

Reputation: 28409

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.

Upvotes: 2

Henri Normak
Henri Normak

Reputation: 4725

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.

Upvotes: 3

Bernd Rabe
Bernd Rabe

Reputation: 790

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

Upvotes: -1

Related Questions