I have a GPS app that uses Google Maps to handle location based events. The app handles all location events within the app and does not switch to Googles own Google Maps app.
The storyboards can be seen in the image below.
In the app I have a main map view (My Map View Controller as in the StoryBoard) that displays the users current location as well as a list of marked locations around the user on the map. This map also contains a button that will take the user to a list of their marked points (List of Points Table View Controller). Selecting any of the list points takes them to a detailed description of the point (Log a Point). And finally clicking "View on Map" button on this view takes them to the last view (Submit Point Map View Controller) where they can see this point zoomed in on another views map.
Both the map views (My Map View Controller AND Submit Point Map View Controller) use similar code as listed below. However, when I run run the code and I get to "Submit Point Map View Controller", this views viewDidLoad method is executed twice as I have noticed while stepping through the code. This causes 2 views to load, one right after the other. I can also see this happening in the emulator. On the emulator the first view that loads has a back button titled "Log a Point" as would be expected as this was the previous view in the stack. The next view that loads simply has "Back" for the back button - as can be seen on the images below.
This is not an issue on the emulator and I can navigate back to Log a Point view. But on my phone the app crashes when I try and navigate back to Log a Point view and gives error "nested push animation can result in corrupted navigation bar. Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted."
I have checked and I am not segue-ing to this view twice or doing anything that I am not doing on the first map view. Does anyone know why this views viewDidLoad method could be called twice? I have read on SO that this error is thrown from List views but I am not coming from a list view in this case - even though there is a list view earlier in the process.
Below is my Submit Point Map View Controller .h and .m files (some code omitted for brevity)
SubmitPointMapViewController.h
#import <UIKit/UIKit.h>
#import <GoogleMaps/GoogleMaps.h>
#interface SubmitPointMapViewController : UIViewController <CLLocationManagerDelegate>
{
}
#property (nonatomic) CLLocationCoordinate2D *location;
#property double latitudes;
#property double longitudes;
#end
SubmitPointMapViewController.m
#import "SubmitPointMapViewController.h"
#import <GoogleMaps/GoogleMaps.h>
#import <Parse/Parse.h>
#interface SubmitPointMapViewController () <GMSMapViewDelegate>
#end
#implementation SubmitPointMapViewController
{
GMSMapView *mapView;
CLLocationManager *locationManager;
}
#synthesize latitudes;
#synthesize longitudes;
- (void)viewDidLoad
{
// This entire method called twice - one right after the other
mapView.delegate = self;
locationManager = [[CLLocationManager alloc] init];
GMSCameraPosition *camera = [GMSCameraPosition cameraWithLatitude: latitudes longitude: longitudes zoom:17];
mapView = [GMSMapView mapWithFrame:CGRectZero camera:camera];
mapView.myLocationEnabled = YES;
[mapView setMapType:kGMSTypeNormal];
self.view = mapView;
// Set the MyLocationButton and add the button to the MapView
mapView.settings.myLocationButton = YES;
// Setup Markers on the Map
[self setupMarkersOnMap];
}
#end
EDIT: Below is my Connections inspector on the Log a Point view, as well as the segue code when the View on map button is pushed on the same view
- (IBAction)viewOnMapButtonPreseed:(id)sender
{
[self performSegueWithIdentifier:#"SubmitPointmapViewSegue" sender:sender];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"SubmitPointmapViewSegue"])
{
SubmitPointMapViewController *vc = [segue destinationViewController];
vc.latitudes = pointObject.latitude;
vc.longitudes = pointObject.longitude;
}
}
As suggested by #Simon Goldeen and #pbasdf above - the issue was that I was pushing 2 map view controllers onto the stack. There was and old segue that I was using previously for debugging that was causing the issue. I deleted all segues to this map view and instead segued to the map view as follows:
SubmitPointMapViewController *vc = [[SubmitPointMapViewController alloc] init];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
vc = (SubmitPointMapViewController *)[storyboard instantiateViewControllerWithIdentifier:#"SubmitPointMapViewControllerStoryboardID"];
[[self navigationController] pushViewController:vc animated:YES];
Thought I would post my answer here in case anyone else had the same issue.
All credit to #Simon Goldeen and #pbasdf for helping me troubleshoot this issue.
Related
I am attempting to accomplish something and i was looking for a few tips. I started working on a project that consisted of one xib file. Later on i upgraded to xcode 5 and pretty soon for the rest of my interface files i began using my storyboard however i kept my old xib (mainly cause of how i pass in methods to it) and simply called it like :
mapViewController *mapView=[[mapViewController alloc]initWithNibName:#"mapViewController" bundle:nil location:0 option:#"poe"];
Recently I have added new functionality and upon clicking a callout I would like to prepare a segue for a new view to be displayed.
Firstly in the xib interface. I was wondering if I could from my mapView (not in storyboard) prepare and call a view/segue(in story board) .
Or if I can simply drop or copy and paste my mapView into my storyboard and then draw my segue connectors to my new view. As I dont think it is necessarily for my to add the mapView to my "story line" however I still want to call is using the method posted above.
However if none of these are feasible please suggest another method.
Thanks
UPDATE
I have moved my xib to my storyboard interface however i am facing a little problem
When i called my map from the xib it would zoom in to a specific location supplied in the init method. I am passing in the same parameter but i am having a little trouble recreating my old results in my storyboard
if ( ([receivedRainObject isEqualToString:#"Natural"])) {
// mapViewController *mapView=[[mapViewController //alloc]initWithNibName:#"mapViewController" bundle:nil location:0 option:#"na"];
// self.navigationItem setHidesBackButton:NO animated:NO];
// [self.navigationController setNavigationBarHidden:NO animated:YES];
// [self.navigationController pushViewController:mapView animated:NO];
[self performSegueWithIdentifier:#"countryMap" sender:self];
}
Then in preparing the srgue
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"countryMap"]) {
mapViewController *transferViewController=segue.destinationViewController;
transferViewController.location=0;
transferViewController.option=#"na";
}
}
Then in view did load of the map
location does evaulate to 0 and the breakpoint shows me that all this code is being stepped thru but the region of the coordinates i supplied isnt what is being shown.( the the span or region)
(however when i call my old xib it works fine)
- (void)viewDidLoad
{
[super viewDidLoad];
{
// Do any additional setup after loading the view from its nib.
if (location==0)
{
CLLocationCoordinate2D location1;
location1.latitude=(double)15.435786;
location1.longitude=(double)-61.318447;
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta=0.50;
span.longitudeDelta=0.40;
region.span=span;
region.center= location1;
[mapView setRegion:region animated:TRUE];
[mapView regionThatFits:region];
}
}
INIT of my mapview
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil location:(int)num option:(NSString *)row
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
self.location=num;
self.option=row;
}
return self;
}
In your mapView, you should be able to do something like this in the callout method:
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"YourStoryBoardName" bundle:[NSBundle mainBundle]];
YourViewControllerFromStoryBoard *viewController = [sb instantiateViewControllerWithIdentifier:#"YourViewControllerIdentifierInYourStoryBoard"];
Then in your mapView you can override prepareForSegue to do any prep work and then present your viewController.
You can't call a segue between a xib defined controller and a storyboard defined one (or between controllers in two different storyboards either). You can instantiate the controller in the storyboard using the UIStoryboard method, instantiateViewControllerWithIdentifier:, and then push or present it in code. It's not clear from your question why you don't want to move the xib based controller into your "story line". You can create a new controller there, and copy and paste the view from your xib file if you want to go that route.
In my app I've got a UINavigationController.
In some of the pages I got a MKMapView as the back view (allows changes to map view the overlay UIView UI, so I can't make it as a image).
In some combinations, It's possible to have 3 or so MKMapView alloc'd.
The problem is that each MKMapView takes nearly 60MB, so the memory jumps up to 180+ MB only from the maps. not to mention if the user opens more ViewControllers with map, he'll get a OutOfMemoryException.
All the maps have the same content, I tried moving a map from ViewController to another, but according to another post in StackOverflow, moving UIView from ViewController to another is against MVC, not to mention it removes it from the caller ViewController.
Basically since all the maps contains the same content, and I want them the contain all the annotations it'd be best to just move the map but I had hard time with that.
I've got a solution out of the box which is to remove the MKMapView from its superView and on viewWillAppear realloc it, but this doesn't seem like the best idea.
I don't see any problem with passing a pointer to a single map view when you move from controller to controller. Unless you're viewing more than one map at a time, I don't see why there should be more that one instance. Remove the view in viewWillDisappear, and pass a pointer to the map in prepareForSegue:
- (void)viewDidLoad {
[super viewDidLoad];
if (! self.mapView) {
self.mapView = [[MKMapView alloc] initWithFrame:self.view.frame];
}
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.view addSubview:self.mapView];
[self.view sendSubviewToBack:self.mapView];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.mapView removeFromSuperview];
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
SecondViewController *secVC = segue.destinationViewController;
secVC.mapView = self.mapView;
}
Subsequent controllers could have the same code in viewWillAppear, viewWillDisappear, and prepareForSegue (except for what controller is the destinationViewController). The map only needs to be instantiated once in the viewDidLoad method of the first controller. An even simpler implementation would be to have all the subsequent view controllers inherit from the first controller. If you do it that way, you won't need to put anything in viewWillAppear or viewWillDisappear. You only need to override prepareForSegue.
First, have a look at this screenshot of my storyboard:
It is an application for a sound map. The user can either record a new field recording or chose an existing one from the library and upload them. The ViewController where the user has to add a title/description etc. (or modifies them when coming from the library) is presented modally (on the top right).
If the user choses to cancel this and to delete the recording, he shall return to the recording screen, if he comes from there, otherwiese to the library. If he choses to save/upload the recording, he shall return to the library, where the upload progress will be displayed.
How can I come back to the desired ViewController independently of the ParentViewController that I come from?
I thought about unwindSegue, but that doesn't work for my layout. Then I figured out a dirty workaround where I changed the selected tab of the TabBarController. But then I also want to set up the delegate correctly to pass some data.
Any thoughts are welcome!
I don't think changing the selectedIndex of the tab bar controller is a "dirty workaround" -- that's the way to do it, given your set up. It appears that you would want the EntryViewController (Library) to be the delegate of the EntryDetailViewController. If that's so, you could set up the delegate in the viewDidLoad method of the EntryDetailViewcontroller like this:
#import "DetailViewController.h"
#import "TableController.h"
#interface DetailViewController ()
#property (strong,nonatomic) UITabBarController *tbc;
#end
#implementation DetailViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tbc = (UITabBarController *)self.presentingViewController;
self.delegate = (TableController *)[(UINavigationController *)self.tbc.viewControllers[1] topViewController];
}
- (IBAction)saveAndUpload:(UIButton *)sender {
[self.tbc setSelectedIndex:1];
[self.delegate saveRecording:#"test recording"];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)delete:(UIButton *)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
In my example, I just have two buttons to either save or cancel, and my TableController would be the same as your Library controller.
Ok so I am trying to pass a string from one view controller to another via the AppDelegate. I want to stay on the current view while this happens.
This is the main body of the code I am currently using to do this:
AppDelegate *dataCenter = (AppDelegate *)[[UIApplication sharedApplication] delegate];
MyMealViewController *vc = [[MyMealViewController alloc] initWithNibName:nil bundle:nil];
dataCenter.selectedMenuItem = recipeLabel.text;
[self presentViewController:vc animated:YES completion:NULL];
When I run the program I am able to confirm that the string is correctly passed. However, then the view on the simulator just turns black. I assume that this is because initWithNibName is set to nil.
So my question is: how should I change my code so that the string will still be passed, but the current view will continue to be displayed on the iphone. Is there a line of code that I could write that would just reload the current view?
Thanks for your help with this issue. I am new to xcode so I may be making a very basic error. Please let me know if any additional information would be helpful in answering this question.
Edit: It looks like you want to show a list of food items in the first view. Tapping an items opens a detail view. From that detail view, the user can press a button to add it to the meal. Eventually, they can tap a button on the first view to open the meal view, which should contain all of the items that they selected.
If this is the case, keep an array on the first view controller, and make sure the detail (second) view controller has a reference to the first view controller when it is presented. This will let us use that array. Note that there are better ways to architect this, but this will work for now:
#interface FoodListViewController : UIViewController
#property (strong, nonatomic) NSMutableArray *foodItems
#end
#implementation FoodListViewController
- (void)showFoodItem
{
FoodItemDetailViewController *detailViewController = [[FoodItemDetailViewController alloc] initWithNibName:nil bundle:nil];
detailViewController.foodListController = self;
[self presentModalViewController:detailViewController animated:YES];
}
#end
Once the detail view is presented, tapping the 'add to meal' button should add the current 'mealItem' to the array. In your example, you were using strings - if you would rather keep an array of strings for some reason, I'll leave that to you.
#interface FoodItemDetailViewController : UIViewController
#property (nonatomic, weak) FoodItemsViewController *foodListController;
#end
#implementation FoodItemDetailViewController
- (IBAction)buttonTapped:(id)sender
{
[self.foodListController.foodItems addObject:self.mealItem];
// Update the UI to let the user know that the item was added to the meal
}
#end
Finally, when it comes time to present the MealDetailsViewController, just pass it the array that you have been building:
#interface MealDetailsViewController : UIViewController
#property (nonatomic, strong) NSArray *foodItems;
#end
#implementation MealDetailsViewController
// Set foodItems before this view controller is presented, then use it to drive the
// UITableView data source, or find some other way of displaying it.
#end
As you can see, both the second and third view controllers are presented by the first. View controllers (nearly) always form a hierarchy - so keeping your data at the top of that hierarchy (by storing it in FoodListViewController) lets you neatly pass it down the hierarchy as you present other view controllers.
UPDATE: Tried the links you suggested, but can't get it to work.
Just to confirm I'm understanding it right, these are the steps I took.
First view controller is called FirstViewController, second is called SearchViewController.
Import SearchViewController into FirstViewController, and add
In FirstViewController.m, add the following:
SearchViewController *svc =[[SearchViewController alloc] init];
svc.delegate =self;
In SearchViewController.h I added:
#protocol SearchViewControllerDelegate
(void)setLat:(CLLocationDegrees)lat setLon:(CLLocationDegrees) lon;
as well as the delegate property:
#property (assign) id <SearchViewControllerDelegate> delegate;
In SearchViewController.m I synthesized delegate, and added
[self.delegate setLat:51.0000 setLon:-0.10000];
From some NSLog testing it seems that the method setLatSetLon method itself is never called.
I'm currently trying to finish off my university iPad application. The purpose of it is to provide a live google/apple maps view of the campus with overlays of annotations and MKPolygons. This all works fine.
I've now added a button that loads a popover (the second view controller) with a table of the campus buildings (which are themselves objects of CampusBuilding, where the coordinates are stored) as well as a search box, and this is all fine. What I'm looking to do is when the user selects a row, the map view centres on that specific building. I've written a method below which works fine when I reference it from the maps view controller itself.
But how would I do this from the second view controller?
Here's the method that is present in the first view controller:
- (void)setLat:(CLLocationDegrees)lat setLon:(CLLocationDegrees) lon{
MKCoordinateSpan span;
span.latitudeDelta = (double) .003;
span.longitudeDelta = (double).003;
//Define the default region to focus on
MKCoordinateRegion region;
region.span=span;
region.center=CLLocationCoordinate2DMake
(lat,lon);
//set the default region to 'region'
[_mapView setRegion: region animated:YES];
[_mapView regionThatFits:region];
}
Now in the second view controller, it works fine as a table view controller. I've set it up so when the user selects a row it loads a new view. I also tried to initiate an object of the first view controller and then wrote this:
FirstViewController* firstVC;
[firstVC setLat:building.latValue setLon:building.lonValue];
It all compiles but no movement happens, I assume because I'm initiating a new object, rather than referring to the one that is currently active.
Is there a way to do this? Comments on how I can improve the code are always welcome, I'm relatively new to this.
Thanks
You were right that firstVC will be a new instance of FirstViewController. Not the previous one.
What you need is to use Objective-C's delegate pattern. It is commonly used in this scenario. Here are two examples from my previous SO answers that have sample code on how to implement.
how to resume timer when poping to view2
Calling a method from another class in Objective C