How to do some stuff in viewDidAppear only once?
Asked Answered
E

9

30

I want to check the pasteboard and show an alert if it contains specific values when the view appears. I can place the code into viewDidLoad to ensure it's only invoked once, but the problem is that the alert view shows too quickly. I know I can set a timer to defer the alert's appearance, but it's not a good work-around I think.

I checked the question iOS 7 - Difference between viewDidLoad and viewDidAppear and found that there is one step for checking whether the view exists. So I wonder if there's any api for doing this?

Update: The "only once" means the lifetime of the view controller instance.

Edgewise answered 29/1, 2015 at 4:33 Comment(1)
I've created a lib to do the same github.com/T-Pham/ViewDidAppearFirstTimeRhizotomy
Y
87

There is a standard, built-in method you can use for this.

Objective-C:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    if ([self isBeingPresented] || [self isMovingToParentViewController]) {
        // Perform an action that will only be done once
    }
}

Swift 3:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    if self.isBeingPresented || self.isMovingToParentViewController {
        // Perform an action that will only be done once
    }
}

The call to isBeingPresented is true when a view controller is first being shown as a result of being shown modally. isMovingToParentViewController is true when a view controller is first being pushed onto the navigation stack. One of the two will be true the first time the view controller appears.

No need to deal with BOOL ivars or any other trick to track the first call.

Yeargain answered 29/1, 2015 at 4:55 Comment(10)
That's nice, I didn't know thatAfteryears
Upvoted... But is this still true if you've maintained a strong reference to a view controller and dismiss and present it multiple times?Dallis
@LyndseyScott Yes. If the view controller is dismissed and then shown again (new instance or the same instance), then one of these will be true when it is first displayed again.Yeargain
@LyndseyScott Possibly. The question is a little vague on that detail. If the OP's desire is to only call the method once in the lifetime of the app (or at least only once ever for the lifetime of the view controller instance) regardless of how many times it is displayed, then this is not the proper solution. In that case, using dispatch_once like in Inder's answer would be best.Yeargain
@Yeargain The lifetime of the view controller instance.Edgewise
@Edgewise ok then you don't want this answer.Yeargain
absolute, answer i was searching for! this question was a hard find.Puling
@Yeargain I tried it, seems like it's not working when the viewController is added as childViewController. :(Carnotite
@farzadshbfn, The container will get these flags correct in its viewDid/Will appear, it depends how it passes theses flags down to child. UINavigationController does not pass these flags down to its rootviewcontrolerBeefburger
@Yeargain what is the swift solution for Inder's answer?Coffin
B
9

rmaddy's answers is really good but it does not solve the problem when the view controller is the root view controller of a navigation controller and all other containers that do not pass these flags to its child view controller.

So such situations i find best to use a flag and consume it later on.

@interface SomeViewController()
{
    BOOL isfirstAppeareanceExecutionDone;
}
@end

@implementation SomeViewController

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    if(isfirstAppeareanceExecutionDone == NO) {
        // Do your stuff
        isfirstAppeareanceExecutionDone = YES;
    }
}

@end
Beefburger answered 7/10, 2016 at 5:51 Comment(1)
Correct. Thanks for the helpTesty
D
5

If I understand your question correctly, you can simply set a BOOL variable to recognize that viewDidAppear has already been called, ex:

- (void)viewDidAppear {
    if (!self.viewHasBeenSet) { // <-- BOOL default value equals NO

        // Perform whatever code you'd like to perform
        // the first time viewDidAppear is called

        self.viewHasBeenSet = YES; 
    }
}
Dallis answered 29/1, 2015 at 4:44 Comment(3)
I don't think you need to set the BOOL to NO in viewDidLoad. It'll start out as NO when the object is created, and viewDidLoad, afaik, will only be called once in the lifetime of the view controller object.Enwind
@Enwind I wrote it this way for clarity's sake, but yes, that's true.Dallis
OK, fine. I've removed the initial setting of the variable and have added a comment instead to explain that BOOLs default to NO.Dallis
A
2

This solution will call viewDidAppear only once throughout the life cycle of the app even if you create the multiple object of the view controller this won't be called after one time. Please refer to the rmaddy's answer above

You can either perform selector in viewDidLoad or you can use dispatch_once_t in you viewDidAppear. If you find a better solution then please do share with me. This is how I do the stuff.

- (void)viewDidLoad {
  [super viewDidLoad];
  [self performSelector:@selector(myMethod) withObject:nil afterDelay:2.0];
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  static dispatch_once_t once;
  dispatch_once(&once, ^{
    //your stuff
    [self myMethod];
  });
}

Afteryears answered 29/1, 2015 at 4:43 Comment(8)
I like your answer, but if static dispatch_once_t once; is declared locally, how will the app know not to execute it the next time around after the variable has been deallocated? Seems like it would need to be stored as a class variable...Dallis
The real problem with the dispatch_once solution is that the static spans instances of the view controller. This solution ensures that myMethod will only be called once in the entire lifetime of the app process.Yeargain
@LyndseyScott No it's perfectly okay. It's a static variable not local variable. You can read about the static variables. Here is one thread https://mcmap.net/q/219063/-difference-between-static-auto-global-and-local-variable-in-the-context-of-c-and-c/468724Afteryears
@Yeargain Correct, if you tear down and rebuild the view controller, the code wrapped by dispatch_once will not execute again. It will never execute again until the user hard resets the app! This is undesired behavior in almost all cases (including the case specified in this post)Agbogla
How about add a non-static property of dispatch_once_t that way it would execute only once for each instance.Decurrent
@Decurrent ried that but it seems that static or non static has no effect. You can also tryAfteryears
Then best to have a Boolean flag that you set the first time. Subsequently check if the flag is setDecurrent
@InderKumarRathore What is the swift solution for this?Coffin
C
1

By reading other comments (and based on @rmaddy 's answer), I know this is not what OP asked for, but for those who come here because of title of the question:

extension UIViewController {
    var isPresentingForFirstTime: Bool {
        return isBeingPresented() || isMovingToParentViewController()
    }
}

UPDATE

You should use this method in viewDidAppear and viewWillAppear. (thanks to @rmaddy)

UPDATE 2

This method only works with modally presented view controllers and pushed view controllers. it's not working with a childViewController. using didMoveToParentViewController would be better with childViewControllers.

Carnotite answered 8/7, 2016 at 17:0 Comment(3)
You should note that this will only work if isPresentingForFirstTime is accessed from viewWillAppear: or viewDidAppear:.Yeargain
Neither suggestion work for me for view controllers inside of a UITabBarController 🤷🏼‍♂️Esteban
@OliverPearmain ViewControllers inside UITabBarController are managed as child containers (If I'm not mistaken)... if you check for didMoveToParentViewController it should get called only onceCarnotite
L
1

You shouldn't have issues in nested view controllers with this check

extension UIViewController {

  var isPresentingForFirstTime: Bool {
      if let parent = parent {
        return parent.isPresentingForFirstTime
      }
      return isBeingPresented || isMovingFromParent
  }
}
Lasandralasater answered 29/10, 2020 at 15:31 Comment(0)
M
0

Try to set a BOOL value, when the situation happens call it.

@interface AViewController : UIViewController
   @property(nonatomic) BOOL doSomeStuff;
@end

@implementation AViewController
- (void) viewWillAppear:(BOOL)animated
{
    if(doSomeStuff)
    {
       [self doSomeStuff];
       doSomeStuff = NO;
    }
}

in somewhere you init AViewController instance:

AddEventViewController *ad = [AddEventViewController new];
ad.doSomeStuff = YES;

Not sure why you do this in ViewDidAppear? But if you want doSomeStuff is private and soSomeStuff was called only once, here is another solution by notification:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doSomeStuff) name:@"do_some_stuff" object:nil];

- (void) doSomeStuff
{}

Then post when somewhere:

[[NSNotificationCenter defaultCenter] postNotificationName:@"do_some_stuff" object:nil];
Mayday answered 29/1, 2015 at 4:44 Comment(3)
Where does this code go? It also requires that doSomeStuff be initialized to YES somewhere.Yeargain
That's not an aprpropriate solution. doSomeStuff should not be public. The view controller itself should be the only class with knowledge of whether doSomeStuff should be set or not.Yeargain
Not sure why you do this, might be another way can help, edit it again, hope it helps.Mayday
L
-1

You can use this function in ViewDidLoad method

performSelector:withObject:afterDelay:

it will call that function after delay. so you don't have to use any custom timer object. and For once you can use

dispatch_once DCD block.Just performSelector in the dispatch_once block it will call performSelector only once when ViewDidLoad is called

Hope it helps

Lollapalooza answered 29/1, 2015 at 4:40 Comment(3)
Where is this to be called from? Improve your answer by showing how this is to actually be used.Yeargain
the question was around viewDidApear methodIvory
@mohamadrezakoohkan viewdidApear could be called multiple times even if view is already loaded. so problem in the above asked question is he want's to perform some thing only once. so I asked him to shift his logic from viewdidApear to viewdidLoad as viewDidload is called only once. if he want's to do some UI stuff then he can call it with perform selecter after delay until UI is properly displayed, so he can avoid extra if condition he has to implement in viewdidApear. hope it will solve your concerns.Lollapalooza
F
-1

swift 5

I've tried isBeingPresented() or isMovingToParent. But It doesn't work. So I tried below code. and It's work for me!

override func viewDidAppear(_ animated: Bool) {
    if (self.isViewLoaded) {
         // run only once
    }
}
Forgery answered 1/3, 2022 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.