Allowing one method call at a time to a category method ios (@synchronized)
Asked Answered
F

3

6

I have a UIViewController and a Category for adding methods to the UIViewController. There is a method in the category:

@implementation UIViewController (AlertAnimationsAndModalViews)
-(void)someAddedMethod
{
    UIView *someView;
    //do some animation with the view that lasts 3 seconds
    //remove the view and return

}

And in any view controller i can call this method

[self someAddedMethod];

However, i only want to allow this method to run one at a time. For example, if i make two calls one after the other

[self someAddedMethod];//call1
[self someAddedMethod];//call2

i want the second call to wait until the first call has completed. I understand that UIView animationWithduration... is run in a seperate thread, and seeing as i cant create iVars in the category i cant really use @synchronized(someObject)..

Any advice?

Thanks in advance!

EDIT

The method looks like this:

 -(void)showTopBannerWithHeight:(CGFloat)height andWidth:(CGFloat)width andMessage:(NSString *)message andDuration:(CGFloat)duration
 {
 
 UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, -height, width, height)];
[self.view.superview addSubview:messageLabel];


[UIView animateWithDuration:0.5
                      delay:0
                    options: UIViewAnimationOptionBeginFromCurrentState
                 animations:^{
                     messageLabel.frame = CGRectMake(0, 0, SCREEN_WIDTH, height);
                 }
                 completion:^(BOOL finished){
                     
                     [UIView animateWithDuration: 0.5
                                           delay:duration
                                         options: UIViewAnimationOptionBeginFromCurrentState
                                      animations:^{
                                          messageLabel.frame = CGRectMake(0, -height, SCREEN_WIDTH, height);
                                      }
                                      completion:^(BOOL finished){
                                          [messageLabel removeFromSuperview];
                                      }];
                 }];

}

So i show a "banner" from the top of the screen, wait for a duration (CGFloat) then slide the view out of the screen and remove. As this is in a category i can't add instance variables.. so what i want to achieve is that if more than one call to this method is made, i want the first call to execute without waiting, but each call after that to wait until the previous call has finished.

Flick answered 23/5, 2013 at 8:4 Comment(2)
Do you also want to wait until the animation has finished?Wendish
yes, see edited versionFlick
U
1

Assuming you want to start next animation after previous one has finished. This way you can use some shared NSMutableArray* _animationQueue storage:

-(void)someAddedMethod
{
    NSTimeInterval duration = 3.0;

    void (^animationBlock)() = ^{
        //do some animations here 
        self.view.frame = CGRectOffset(self.view.frame, 40, 0);
    };

    __block void (^completionBlock)(BOOL) = ^(BOOL finished){
        [_animationQueue removeObjectAtIndex:0];
        if([_animationQueue count]>0) {
            [UIView animateWithDuration:duration animations:_animationQueue[0] completion:completionBlock];
        }
    };

    [_animationQueue addObject:animationBlock];
    if([_animationQueue count]==1) {
        [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock];
    }
}

Note, you don't need any @synchronized features since everything goes on main thread.

UPDATE: the code below does exactly you need:

-(void)showTopBannerWithHeight:(CGFloat)height andWidth:(CGFloat)width andMessage:(NSString *)message andDuration:(CGFloat)duration
{
    static NSMutableArray* animationQueue = nil;
    if(!animationQueue) {
        animationQueue = [[NSMutableArray alloc] init];
    }

    void (^showMessageBlock)() = ^{
        UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, height)];
        messageLabel.text = message;
        [self.view.superview addSubview:messageLabel];

        [UIView animateWithDuration: 0.5
                              delay:duration
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations:^{
                             messageLabel.frame = CGRectOffset(messageLabel.frame, 0, -height);
                         }
                         completion:^(BOOL finished){
                             [messageLabel removeFromSuperview];
                             [animationQueue removeObjectAtIndex:0];
                             if([animationQueue count]>0) {
                                 void (^nextAction)() = [animationQueue objectAtIndex:0];
                                 nextAction();
                             }
                         }];
    };

    [animationQueue addObject:showMessageBlock];
    if([animationQueue count]==1) {
        showMessageBlock();
    }
}
Ursine answered 23/5, 2013 at 9:46 Comment(7)
yes i originally had a similar implementation using an array. It was a recursive method that stored "method calls" in an array and before the method ended, it would call itself again and check if any calls were added, if they were it was added again. But since moving it into a category i cannot using these variables unless i pass in a shared array, which i believe destroys the reason for using a categoryFlick
You can use objc_setAssociatedObject to add ivar in category. Also look at my updated solution.Ursine
wow, this worked! thanks a lot! just a few questions too improve my understanding if thats ok... when you declare the static array, why do you set it to nil and then check if(!animationQueue) then alloc and init? if you set it to nil wont you always be alloc and init-ing it? and what is the purpose of the block nextAction() at the end of the animation?Flick
The static variable animationQueue acts in the same way as a global variable. Initially it is nil, and every function call it is checked whether it needs to allocate an instance of NSMutableArray. Once allocated, animationQueue become not-nil.Ursine
nextAction is just one of the cached showMessageBlock objects in the queue.Ursine
I have decided to change the functionality of my method, and i only want to display the view if there isnt one already being displayed. Do you have any sample code of how i could do this? I was thinking of declaring a static boolean, but i cant seem to set it too false inside the block...Flick
Excellent stuff. Thanks onegray this is just what I needed.Mcdowell
N
2

If its just about the animations you may check if ([someView.layer animationForKey:@"sameKeyAsOnCreation"] == nil). Than you will only add an animation, if it is not currently runnning.

You could also use associated objects to store the state on your own (animation running / not running).

Noaccount answered 23/5, 2013 at 9:37 Comment(0)
U
1

Assuming you want to start next animation after previous one has finished. This way you can use some shared NSMutableArray* _animationQueue storage:

-(void)someAddedMethod
{
    NSTimeInterval duration = 3.0;

    void (^animationBlock)() = ^{
        //do some animations here 
        self.view.frame = CGRectOffset(self.view.frame, 40, 0);
    };

    __block void (^completionBlock)(BOOL) = ^(BOOL finished){
        [_animationQueue removeObjectAtIndex:0];
        if([_animationQueue count]>0) {
            [UIView animateWithDuration:duration animations:_animationQueue[0] completion:completionBlock];
        }
    };

    [_animationQueue addObject:animationBlock];
    if([_animationQueue count]==1) {
        [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock];
    }
}

Note, you don't need any @synchronized features since everything goes on main thread.

UPDATE: the code below does exactly you need:

-(void)showTopBannerWithHeight:(CGFloat)height andWidth:(CGFloat)width andMessage:(NSString *)message andDuration:(CGFloat)duration
{
    static NSMutableArray* animationQueue = nil;
    if(!animationQueue) {
        animationQueue = [[NSMutableArray alloc] init];
    }

    void (^showMessageBlock)() = ^{
        UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, height)];
        messageLabel.text = message;
        [self.view.superview addSubview:messageLabel];

        [UIView animateWithDuration: 0.5
                              delay:duration
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations:^{
                             messageLabel.frame = CGRectOffset(messageLabel.frame, 0, -height);
                         }
                         completion:^(BOOL finished){
                             [messageLabel removeFromSuperview];
                             [animationQueue removeObjectAtIndex:0];
                             if([animationQueue count]>0) {
                                 void (^nextAction)() = [animationQueue objectAtIndex:0];
                                 nextAction();
                             }
                         }];
    };

    [animationQueue addObject:showMessageBlock];
    if([animationQueue count]==1) {
        showMessageBlock();
    }
}
Ursine answered 23/5, 2013 at 9:46 Comment(7)
yes i originally had a similar implementation using an array. It was a recursive method that stored "method calls" in an array and before the method ended, it would call itself again and check if any calls were added, if they were it was added again. But since moving it into a category i cannot using these variables unless i pass in a shared array, which i believe destroys the reason for using a categoryFlick
You can use objc_setAssociatedObject to add ivar in category. Also look at my updated solution.Ursine
wow, this worked! thanks a lot! just a few questions too improve my understanding if thats ok... when you declare the static array, why do you set it to nil and then check if(!animationQueue) then alloc and init? if you set it to nil wont you always be alloc and init-ing it? and what is the purpose of the block nextAction() at the end of the animation?Flick
The static variable animationQueue acts in the same way as a global variable. Initially it is nil, and every function call it is checked whether it needs to allocate an instance of NSMutableArray. Once allocated, animationQueue become not-nil.Ursine
nextAction is just one of the cached showMessageBlock objects in the queue.Ursine
I have decided to change the functionality of my method, and i only want to display the view if there isnt one already being displayed. Do you have any sample code of how i could do this? I was thinking of declaring a static boolean, but i cant seem to set it too false inside the block...Flick
Excellent stuff. Thanks onegray this is just what I needed.Mcdowell
P
0

Try to use this one.And used self in @synchronized directive.

- (void)criticalMethod {
    @synchronized(self) {
        // Critical code.
    }
}

Note: The @synchronized() directive takes as its only argument any Objective-C object, including self. This object is known as a mutual exclusion semaphore or mutex. It allows a thread to lock a section of code to prevent its use by other threads.

Picul answered 23/5, 2013 at 8:8 Comment(1)
If you really want to wait until the '3 second animation' has finished before allowing another invocation of your method to run this won't work.Wendish

© 2022 - 2024 — McMap. All rights reserved.