Resolution: While trying to recreate this bug in a fresh project to submit to Apple, I discovered that it is specific to iPhone OS 2.1, and compiling for 2.2 fixes the problem. Stephen, thanks for your help; I'll be accepting your answer since it would have worked if the bug still existed or I wasn't willing to compile for 2.2.
I have an app which is radically changing its database schema in a way that requires me to transform old-style records to new-style ones in code. Since users may store a lot of data in this app, I'm trying to display a modal view controller with a progress bar while it ports the data over (i.e. as the very first thing the user sees). This view controller's viewDidAppear: begins a database transaction and then starts a background thread to do the actual porting, which occasionally uses performSelectorInMainThread:withObject:waitUntilDone: to tell the foreground thread to update the progress bar.
The problem is, viewDidAppear: is being called twice. I noticed this because that "start a transaction" step fails with a "database busy" message, but setting a breakpoint reveals that it is indeed called two times—once by -[UIViewController viewDidMoveToWindow:shouldAppearOrDisappear:], and again by -[UIViewController modalPresentTransitionDidComplete]. Those names appear to be private UIViewController methods, so I'm guessing this is either a framework bug, or I'm doing something UIKit isn't expecting me to do.
Two relevant code excerpts (some irrelevant code has been summarized):
- (void)applicationDidFinishLaunching:(UIApplication *)application {
(register some default settings in NSUserDefaults)
// doing this early because trying to present a modal view controller
// before the view controller is visible seems to break it
[window addSubview:[self.navigationController view]];
// this is the method that may present the modal view
[self.databaseController loadDatabaseWithViewController:self.navigationController];
if(!self.databaseController.willUpgrade) {
[self restoreNavigationControllerState];
}
}
And from my DatabaseController class:
- (void)loadDatabaseWithViewController:(UIViewController*)viewController {
(open the new database)
(compute the path the old database would live at if it existed)
if([[NSFileManager defaultManager] fileExistsAtPath:oldDBPath]) {
(open the old database)
[viewController presentModalViewController:self animated:NO];
}
}
So, is there something I'm screwing up here, or should I file a bug report with Apple?
I saw this in my app too. I never got it entirely confirmed, but I think this is what's happening:
Load root view
Load modal view
OS sends view did appear notification for the view in step 1
The current view controller, which in this instance happens to be your DatabaseController class, picks it up
OS sends the view did appear notification for the modal view
The current view controller gets the notification. In this case it's the exact same controller as last time
In my case I just reset what happened in the first call to viewDidAppear:.
In your case two options spring to mind: a static variable to track whether you've started the upgrade already; or look at the UIView* parameter passed in before starting.
Related
I have been all over stackoverflow and all over Google and I cannot seem to figure this one out. Here's my scenario:
I have my app's "main screen" where the user first makes decisions about what they're going to do. The app works off of a CoreData database which is created by "importing" XML files. The user can choose to open an XML file attached to an email in my application, which automatically triggers my main screen to show up and run the import of the file.
I can get this far without any issues. In my storyboard, I have a segue called ParseSegue from my main screen to a view controller which will handle the parsing and give the user some status information.
When the main screen is called via the email app, the main screen automatically calls
[self performSegueWithIdentifier:#"ParseSegue" sender:self];
I then check for this segue name in prepareForSegue and it's a valid name. This is where I assign the file URL to the parser controller so that it can parse the correct file.
The problem is that the segue never actually happens. The prepareForSegue method gets called, the name "ParseSegue" can be checked against and is valid, but the segue itself simply does not happen. If I add a button to the main screen and tell it to perform the segue within the storyboard, it works fine. But calling it programmatically seems to do nothing.
It turns out that I was looking in the wrong place entirely. My problem was that in my appDelegate, where the app reacts to the incoming URL, I was inadvertently creating a new instance of my storyboard and my main view controller. This was different than the one which was already active and may or may not have been on the screen.
The controller I was creating was never actually shown. I only noticed this because the log:
NSLog(#"Source: %#", [segue.sourceViewController description]);
would show different memory addresses for my test (the button push) and the import test. This led me to believe that I was, in fact, working with two different instances of the storyboard and the app's main view controller. Thanks to Paul for the suggestion of logging the destination and the source controllers.
Ok I am trying to refresh the tab content of each of my tabs after a web call has been made, and I have tried soo many different methods to do this that I have lost count. Could someone please tell me how this is possible?
The web call just calls JSON from a server and uses it to update the content of the tabs. For testing purposes I have a button set up inside my settings class. Settings class is a view within the home tab which has a button called refresh. When clicked this takes JSON stored on the device which is different to the one called from the web call on application start up. This saves me having to change the JSON on the server.
I will take you through some of the techniques I have tried and would be grateful if someone could tell me what I am doing wrong.
I tried making an instance of the class and calling the refresh method like this
DashboardVC *db = [[DashboardVC alloc] init];
[db refreshMe];
The refresh method in dashboard class is this
-(void) refreshMe
{
[self loadView];
[self viewDidLoad];
}
However no luck. This method will work if I call it inside the Dashboard class, but wont work if I call it from another class. I think it is become I am instantiating a new class and calling refresh on that. So I dropped that technique and moved onto the next method
This loops through all the tabBars and changes the tabTitles without any issues, so it I know it is definitely looping through the ViewControllers properly.
I also tried every varient of the view methods like ViewDidAppear, viewWillAppear etc, no luck.
I also tried accessing the refreshMe method I made in the dashBoard class through the tabController like this
[[[self.tabBarController viewControllers] objectAtIndex:0] refreshMe];
But again no luck, this just causes my application to crash.
I read through this guide
https://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/ViewControllerPGforiOSLegacy/TabBarControllers/TabBarControllers.html
on the apple website but it doesn't seem to cover how to refresh individual tab content.
All I want is to have each individual tab refresh its content after the web call is made, and have spent ages trying to figure this out, but nothing is working.
So would be very grateful if someone could show me what I am doing wrong?
Thanx in advance....
EDIT:
Expand on what I have tried
After discussion with Michael I realised you should never call loadView as against Apple guidelines. So I removed any references to LoadView. I have now placed a method in all the main ViewControllers called RefreshMe which sets up the views, images texts etc in the class. And this method is placed inside the ViewDidLoad. Now I want to be able to call these methods after a web call has taken place, so effectively refreshing the application.
My viewDidLoad now looks like this in all my the main classes.
- (void) viewDidLoad {
[super viewDidLoad];
[self refreshMe];
}
And then the refreshMe method contains the code which sets up the screen.
The JSON data pulled from the web call will set up the content of each of the 5 tabs, so need them all to be refreshed after web call.
I tried looping through the viewControllers and calling viewDidLoad, which should in turn call the refreshMe method which sets up the class, however nothing happens. Code I used was this
NSArray * tabBarViewControllers = [self.tabBarController viewControllers];
for(UIViewController * viewController in tabBarViewControllers)
{
[viewController viewDidLoad];
}
For the time being I have also included
NSLog(#"Method called");
in the viewDidLoad of each class to test if it is being called. However the message is only being printed out when I first load the application or if I re-enter the application. This method should be called after I click the refresh button in the settings screen but it isn't and I have no idea why.
Anyone have any idea why this is not working?
From the question and your comments, it sounds like there are at least two problems:
You're having trouble accessing the view controllers managed by your app's tab bar controller.
You seem to be working against the normal operation of your view controllers.
The first part should be straightforward to sort out. If you have a pointer to an object, you can send messages to that object. If the corresponding method doesn't execute, then either the pointer doesn't point where you think it does or the object doesn't have the method that you think it does. Let's look at your code:
NSArray * tabBarViewControllers = [self.tabBarController viewControllers];
for(UIViewController * viewController in tabBarViewControllers)
{
[viewController viewDidLoad];
}
This code is supposed to call -viewDidLoad on each of the view controllers managed by some tab bar controller. Leaving aside the wisdom of doing that for a moment, we can say that this code should work as expected if self.tabBarController points to the object that you think it does. You don't say where this code exists in your app -- is it part of your app delegate, part of one of the view controllers managed by the tab bar controller in question, or somewhere else? Use the debugger to step through the code. After the first line, does tabBarViewControllers contain an array of view controllers? Is the number of view controllers correct, and are they of the expected types? If the -viewDidLoad methods for your view controllers aren't being called, it's a good bet that the answer is "no," so figure out why self.tabBarController isn't what you think.
Now, it's definitely worth pointing out (as Michael did) that you shouldn't be calling -viewDidLoad in the first place. The view controller will send that method to itself after it has created its view (either loaded it from a .xib/storyboard file or created it programmatically). If you call -viewDidLoad yourself, it'll either run before the view has been created or it'll run a second time, and neither of those is helpful.
Also, it doesn't make much sense to try to "refresh" each view controller's view preemptively. If your app is retrieving some data from a web service (or anywhere else), it should use the resulting data to update its model, i.e. the data objects that the app manages. When a view controller is selected, the tab bar controller will present its view and the view controller's -viewWillAppear method will be called just before the view is displayed. Use that method to grab the data you need from the model and update the view. Doing it this way, you know that:
the view controller's view will have already been created
the data displayed in the view will be up to date, even if one of the other view controllers modified the data
you'll never spend time updating views that the user may never look at
Similarly, if the user can make any changes to the displayed data, you should ensure that you update the model either when the changes are made or else in your view controller's -viewWillDisappear method so that the next view controller will have correct data to work with.
Instead of refreshing your view controllers when updating your tab bar ordering, why not simply refresh your views right before they will appear by implementing your subclassed UIViewController's viewWillAppear: method?
What this means is that each time your view is about to appear, you can update the view for new & updated content.
new iOS guy here. I have a problem that Googling and searching on here has not shed any light on. I'm assuming this is basic. I have a simple app (app delegate and 1 view controller), and as part of it I'm using local notification. So, in the app delegate I use the 'didReceiveLocalNotification' to watch for the notifications. Depending on which one comes in, I then call one of several instance methods in my main view controller.
ie in the AppDelegate.m
- (void)application:(UIApplication *)application didReceiveLocalNotification: (UILocalNotification *)notification {
MyViewController* controller = [[MyViewController alloc] autorelease];
if ([[notification.userInfo objectForKey:#"id"] isEqualToString:#"myKey"]) {
[controller checkActive];
}
}
Through logging and watching some breakpoints, this is all working. If the app is in the background, the notification comes in, app opens, and the correct instance method is called.
What I cannot figure out at all is why some parts of the instance method are simply being passed by, with no effect. For a simple example, if we have this:
-(void)checkActive {
ViewThing.alpha = 1.0;
NSLog(#"checkActive ran");
}
The log statement will show up fine, but the ViewThing will not change. Elsewhere in the main view controller I'm calling the same checkActive method with no problems and it changes the ViewThing. (via another interface button IBAction method in that case).
There are no errors, no warnings, the console shows nothing, putting a breakpoint on ViewThing shows that it hits the line. I'm stumped, cannot see what is different from trying to calling the method from the delegate vs. on an IBAction.
Thanks for any tips!
If the alpha is not correctly changing there a few possible issues with 1 and 2 being the most likely.
ViewThing is nil. Reasons could be is the view unloaded and you set it to nil or checkActive was called before the outlets were set.
ViewThing.alpha is being set on a thread that is not the main thread. Attempting to change UI elements on a separate thread will caused undefined behavior such as never updating the change or taking an extended amount of time to update the change. You can check if it is the main thread using [NSThread isMainThread].
ViewThing is pointing a different view.
1 & 2 can easily be checked by logging view
NSLog(#"checkActive ran %#", ViewThing);
Any advice on how to fix this issue I have, or a better implementation design perhaps?
Requirement
Needed a way for the application at start up to take the user to the previous details page, if this was what they were on prior to quiting the application in their last session
If they were on the main screen of the app, then at restart they can stay here
The assumption is I'm working with UINavigationController and the main screen and details screen are built on a UITableViewController
My Implementation Concept
Put a check in "viewdidLoad" to see whether they were on a detailed screen, and then if so jump to this (refer to code below)
Issue
Works fine normally, however when I trigger a memory warning things screw up and I get nav bar weird behavior. For example I see the main page nav buttons, when it looks like I'm on the detail page content (UITableView)
My Analysis
From what I see when I am on the details page (appointmentsListController) and cause a memory warning in the simulator I see:
(a) the main page "viewDidLoad" actually gets called, which my concept didn't expect, so whilst I had hit the BACK button from the detailed view (UINavigationController) to go to the main view (RootViewController), in fact my code is run and it try's to throw the user back to the details page again
(b) I note in my logs after this point that [AppointmentListController viewDidLoad] seems to get called before the previous AppointmentListController dealloc method is called (i.e. like I was in controller A, went back to controller B, but got thrown back to A - and the initial dealloc for the first part didn't kick in until late...)
So I guess it's obvious my idea isn't too great
Question
Any suggestions on how to better implement my requirement? (how to check, which method to put them in)
Code
- (void)viewDidLoad {
[super viewDidLoad];
// My Implementation of the Requirements which seems flawed in the case there is memory warning triggered
if ( previousSelectedScreen >= 0 ) {
// Setup New Controller
AppointmentListController *appointmentListController = [[AppointmentListController alloc] initWithNibName:#"AppointmentListController" bundle:nil];
appointmentListController.screenToShow = previousSelectedScreen;
// Push new view onto stack
[[self navigationController] pushViewController:appointmentListController animated:NO];
[appointmentListController release];
}
}
Here's what I'd suggest: rather than having this logic in your view controller, but it in your application delegate. By constructing your navigation stack before displaying it you will hopefully avoid some of the weird things that can happen with nav bars, etc. To get rid of the memory warnings you may need to look at how your app allocates memory: it may not necessarily be to do with this.
Anyway - in your application delegate you can perform your check to see if the user was on a detail page when they exited. If they are, you can create an array containing the navigation stack (ie, Main Screen -> Details Page). You can then pass this into a navigation controller using its setViewControllers method. Once this is done, you can display your window and finish launching the app.
I'll start with a confession here... I'm a real newbie to Objective-C & iPhone programming (started studying in February & coding in March), I have a project that's very ambitious for that level of experience & a very tight deadline to catch an opportunity to give my app a field trial.
My app is Core Data driven & downloads all of it's data on first run which is a choice made because it will be used on sites where 3G network access may not be reliable. I'd like to present a modal view while this happens, nothing fancy just a bit of text to explain, a progress bar or activity indicator, a graphic to pretty it up & button becoming visible when the job is done. I've tried a couple of approaches & failed dismally so no code for that as all but the XIB has been trashed.
At the moment I'm running this code in applicationDidFinishLaunching ...
[self checkDataAndLoadIfNeeded];
[window addSubview:rootController.view];
[window makeKeyAndVisible];
rootController is a TabBarController with nested NavigationControllers. checkDataAndLoadIfNeeded is a method that checks a default for the data being loaded & if it is not YES presents an alert. The delegate method for dismissing the alert then a custom class, DataLoader, which goes about downloading & importing the data.
What's happening is that the rootController view becomes visible before the alert does & the table on the first tab doesn't load any data until the next run of the app. I'm wondering if that data not loading is because I'm doing that in viewDidLoad & whether I'd do better to have it in viewWillAppear or viewDidAppear. When I tried loading the modal view I've built my rootController view still became visible first & my modal view didn't become visible until the data had finished (or almost finished) downloading (the Done button became visible immediately).
Can anyone offer suggestions as to how I can make this work?
Cheers & TIA, Pedro :)
Sounds like your rootController is not watching for changes in the data. It should not matter if the element is displayed already or if the data loads first. If the data loads later then the UI element should notice that the data is updated and then refresh itself.
Depending on your app design, you should look at the NSFetchedResultsController class and implement it along with its delegate methods. This class is designed to watch the NSManagedObjectContext for changes and when data is saved out to disk, update its delegate with what has changed.