I am creating a walking tour and am using mapkit. I have the map loading and am using custom icons as pins and loading my gps coordinates from a plist file. My callouts are working fine. My problem is that I would like the right callout button to load different information on another screen for each stop (picture of stop and an MP3). Would I load a different view controller for each stop on the tour? If so what code would I add to this following to have a view controller named Detailcontroller load?
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
NSLog(#"I've been tapped");
}
If I am way off and someone could point me in the right direction it would be greatly appreciated. I apologize for my wording as I am really new to coding/app development.
I would hope and expect that you'd use the same kind of view controller for each stop, but set up with different data. When I've done something like this, my annotations have been the data objects for each point of interest on the map, so I'd do this:
-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
StopInfo *stopInfo = view.annotation;
StopInfoDetailController *detailController = [[StopInfoDetailController alloc] initWithNibName:nil bundle:nil];
detailController.stopInfo = stopInfo;
[mapView.navigationController pushViewController:detailController animated:YES];
[detailController release];
}
In this case, StopInfo would be the class of the annotation objects. It should contain the information that the detail controller needs to do it's thing: MP3 file name, stop location, stop image file name, stop description, etc.
Note that you might actually have several different kinds of annotations on your map. Maybe some are stops on your walking tour while others are points of interest that aren't on the tour, food vendors, bathrooms, etc. In that case you might want to use different view controllers for each type of anntation, so you'd look at the annotation object to figure out what kind of view controller to instantiate.
Related
I'm relatively new to iOS dev and completely new to using the Google Maps API.
I have two separate views that I want to have a mapview. The first one is a scene that allows you to find a location on the map, enter in an address, or use your current location. In the next scene, I want to use the same location that the user input from the previous scene, but rather than the regular map I want to use the satellite view, and there will be some additional tools to manipulate the map to serve the purposes of my app.
I need to have two separate scenes, as the interfaces are very different, but I'm having trouble sending the information from one mapview to the other. Here's a few methods that I've tried(two are commented out):
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"showServiceArea"]) {
ServiceAreaViewController *destViewController = segue.destinationViewController;
destViewController.camera = [GMSCameraPosition cameraWithLatitude:curLocation.location.coordinate.latitude longitude:curLocation.location.coordinate.longitude zoom:17]; //Setting up a camera in the next scene using the curLocation coordinates from the current view
//destViewController.view = _mapView; //Setting the next view controller's view to the current mapView and changing the mapType to satellite
//_mapView.mapType = kGMSTypeSatellite;
//destViewController.serviceAreaMapView = _mapView; //copying the current mapview to a mapview on the next scene
//destViewController.serviceAreaMapView.mapType = kGMSTypeSatellite;
}
}
Every method that I've tried has thrown an exception and caused my app to crash. Here's the output from the crash of the above code:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewController setCamera:]: unrecognized selector sent to instance 0x17e73eb0'
For the first two commented out methods, I figured that by the time the next scene was loaded, the data I had assigned to the next view was being deleted. I'm not sure what is wrong with my current code.
Could anybody point me in the right direction to produce a mapView in a following scene using the current scene's data? Thanks.
I dont think you should pass UI between two controllers. You should only pass the non-UI data to your second controllers. Especially your UI data are probably weak, which would probably become nil. In addition, prepareForSegue happens before your second controller's view is loaded, whatever UI data you pass to the second controller will not be set.
The correct way to pass data between is to pass non-UI data. For example, if you have a UITextView in your first controller, you should not do secondController.textView = firstController.textView;, instead, you should pass the non-UI attributes. So you need to make a NSString *myText; in your controllers, then you can do secondController.myText = self.myText; in your first controller's prepareForSegue method. When your second controller's view is loaded (your UITextView will be loaded) in viewWillLoad() or viewDidLoaded() method, you can set your UITextView with the myText data in your second controller: self.textView = myText
In your case, you should not pass the mapView from your first controller to the second controller, you should declared variables to hold your mapView in your second controller, then you can pass the non-UI mapView attribute data to the strong variables in your second controller.
I have game that is played on a single view, when the game is over I want the user to be able to press a button (play again) that will completely reload the view (clearing all game data and refreshing the view as if it were loading for the first time). I have tried
[self.view setNeedsDisplay]
however nothing is happening. Do I have to manually clear out the data or is there a way to reset everything at once?
What I've similarly done in this case is to create a property which holds all of your game subviews, etc.
#property (nonatomic, strong) UIView *gameView;
Then in viewDidLoad, we call a method that sets up our game view for the first time (You will see we will use the same method a little later again for resetting the game)
- (void)viewDidLoad
{
[super viewDidLoad];
[self setUpGame];
}
- (void)setUpGame
{
// Your game views (subviews, buttons, etc.) set up here
self.gameView = [[UIView alloc] initWithFrame:self.view.bounds];
}
Then, when the user taps the play again button, we simply remove the game view from the superview, and call the prior method we discussed, which sets up the game again. Your button's target should be set to call this method below: [self playAgain];
- (void)playAgain
{
[self.gameView removeFromSuperview];
// This is the method we previously discussed above
[self setUpGame];
}
It's up to you to think of some cool and unique animations to make it a pleasing resetting of the game at this point :)
setNeedsDisplay just indicates that you would like iOS to redraw the screen, which you rarely should need to call manually.
I would probably implement something like #troop231 already stated, which is a reset method, but I would not re-allocate any buttons / views etc because that could be costly. In the MVC model, you should have your data (scores, # of lives, etc.) stored separately and your views should just reference them. So, reset the model and assuming you have your views aware of model changes, they will update accordingly.
Ways to do this include KVO, NSNotification, Core Data's NSFetchedResultsController (probably overkill), delegation, etc.
I've been having an issue with having multiple MKMapViews in my iOS application.
We are currently using a TabBarController for basic navigation. The issue comes up when a MKMapView's annotation's button segues to another view that has a button that leads to another MKMapView. The first MKMapView works fine with annotations and functionality, but the second MKMapView won't add annotations.
I believe the class is linked to the StoryBoard's layout fine since it triggers the viewDidLoad function upon the segue. When I step through the viewDidLoad function it reaches the addAnnotation function call, but the annotations do not get added to the map.
I know the post "Multiple map views in same app" covers a similar issue, but the poster didn't seem too friendly and didn't get any answers due to that.
Please let me know what you think, if you need more information, or if you've implemented multiple MKMapViews in your iOS project. Thanks!
Look closely at your setup of delegates & IBOutlets to make sure each view is pointing to the right mapview. Then make sure each function uses the parameters it was given, e.g.
- (MKAnnotationView *)mapView:(MKMapView *)aMapView viewForAnnotation:(id < MKAnnotation >)annotation
{
//Only use "aMapView here not "mapView"
}
I have an MKMapView that's supposed to track the user's location using a custom view (not the blue dot). In order to substitute this view for the blue dot, I return it thusly:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if (annotation == [mapView userLocation])
{
return userLocationView;
}
}
To initialize tracking, I call
[mapView setShowsUserLocation: YES];
[mapView setUserTrackingMode: MKUserTrackingModeFollow animated: NO];
[mapView setDelegate: self];
As would be expected, -mapView:didUpdateUserLocation: gets called once when the app loads. Unfortunately, it's never called again unless I change -mapView:viewForAnnotation: to have the following implementation:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if (annotation == [mapView userLocation])
{
return nil;
}
}
With these changes, the map loads the blue dot as the indicator of the user's location, and -mapView:didUpdateUserLocation: gets called frequently, as would be expected.
Is there some sort of mutual exclusivity for tracking users' locations and have a custom user location view? How can I make both happen?
Source
This project demonstrates this issue. https://dl.dropbox.com/u/2338382/MapKitFuckery.zip
Bug
This is most likely a bug, which I've filed as a radar. In the interim, the accepted answer should prove sufficient. However, it bears noting that I had to give up entirely on [mapView userLocation] and [mapView showsUserLocation], in favor of simply a custom annotation and the CLLocationManager.
Instead of relying on the map view's location updates, start a CLLocationManager, set its delegate and wait for -locationManager:didUpdateToLocation:fromLocation: (in iOS 5 and lower) or -locationManager:didUpdateLocations: (iOS 6). You will get much more reliable and plentiful information than using the map view's delegate methods. You probably know the way to do this, but here it is:
#import <CoreLocation/CoreLocation.h>
- (void)viewWillAppear:(BOOL)animated
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
[self.locationManager startUpdatingLocation];
}
// Deprecated in iOS 6
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
// Check the age of the newLocation isn't too long ago using newLocation.timestamp
// Set the map dot using newLocation.coordinate
// Set an MKCircle to represent accuracy using newLocation.horizontalAccuracy
}
I had a look at the delegate calls that come in to the mapView's delegate, and returning anything other than nil stops calls to -mapView:didUpdateUserLocation:, like you said. Here are the calls in the order they arrive:
- (void)mapViewWillStartLocatingUser:(MKMapView *)mapView
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation
- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView
- (void)mapView:(MKMapView *)mapView didFailToLocateUserWithError:(NSError *)error
Presumably the MKUserLocation object, not the MKMapView is the object responsible for calling the delegate with update calls. If you check the status of showsUserLocation and mapView.userLocation, they both look fine:
NSLog(#"%d %#", mapView.showsUserLocation, mapView.userLocation);
returns 1 and a non-nil object (1 <MKUserLocation: 0x1e02e580>). Maybe the mapView queries its userLocation object to get the current location, then sends it to the delegate. If that object has gone, it won't work.
It's a bit strange, but like I said, you'll get better updates from a CLLocationManager's updates.
An old question that I would like to answer my own way. I was having an issue somewhat similar. I just needed to make a web service call, passing the user location as a GET parameter, when my MapView's ViewController/Screen was loaded and user location retrieved by the mapView. It made sense then to call the web service within the delegate method didUpdateUserLocation. I didn't notice the wrong behaviour first because things seemed to work properly but then for some reasons sometimes upon opening the mapView screen, the user blue dot would show but the map would not "zoom in" and neither didUpdateUserLocation was called nor my inner web service call obviously. After a few seconds of staring at the screen, the map would "zoom in" and the didUpdateUserLocation/web service was called. Some small glitch I thought, not a big deal. Now my detail-oriented developer's mind was still frustrated and after a few weeks of thinking this over, I decided to take action on this. Stackoverflow didn't give me the answer straight away but pointed me towards the right direction nonetheless. And here was the culprit: Sequence of calls! Maddening but made total sense once I figured things out. I knew that in order to see the blue dot and get the user location I had to tell the mapView to do so. So being an Interface Builder lover, I set things up properly for my mapView in IB, that is I checked the box: "User Location", easy. Then carefully reading the mapView documentation I realised that my ViewController needed to conform to MKMapViewDelegate, done deal. As I said, things seemed to work ok, the blue dot would show right away but sometimes the "zoom in" of the map would take a few seconds... well my iPhone is already 3 years old, things are getting slow, I'd deal with the sluggishness... Then I read this stack overflow post (https://stackoverflow.com/a/37407955) and things became clear. In my case, since using IB, here was the sequence of calls:
User Location checkbox checked in IB
In viewDidLoad, call: mapView.delegate = self
Whereas, here is how the sequence of calls should have been:
User Location checkbox NOT checked in IB (This is important)
mapView.delegate = self
mapView.showsUserLocation = true
And this changes EVERYTHING. Instead of having the mapView zooming in sometimes right away and sometimes after a few seconds, the mapView now zooms in right away when the screen opens, and the didUpdateUserLocation/web service IS called.
Now a little more on the why, it still "sometimes" worked. This is simply due to the fact that the location of my iPhone was sometimes updated right after the map screen was loaded and sometimes not :-). I have to say that besides the stackoverflow post I mentioned above, what helped me too was testing my app in the Simulator since I needed to use a gpx file with obviously static location coordinate, the non predictable behaviour I described was then systematic.
So as much as I love Interface Builder, I might have to reconsider how unconditional that love is.
PS: I know this post is old and my answer not entirely related to the issue #Patrick Perini had but I wish it helps others and that it might answer #Patrick Perini's conclusion regarding the fact that a Bug was at fault when it's not. Thanks for reading this blog post :-)
I'm currently implementing automatic state preservation/restoration in an iOS6-only app.
For a restoration of a table view, I added the UIDataSourceModelAssociation protocol to my table view controllers and implemented
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
and
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
When pressing the home button, the state preservation methods, including modelIdentifierForElementAtIndexPath:iView:, are getting called as expected and return valid identifier strings for the given index paths.
When killing the app and relaunching it, state restoration works more or less. I.e. the app re-opens the correct table view. However, the table view is always scrolled to the top, even when it was scrolled to another position before.
Here's the implementation of the UIDataSourceModelAssociation methods in my table view controller. Nothing fancy going on in there (the NdlFriend::accountUidproperty returns a unique identifier string for a given NdlFriend record):
#pragma mark - UIDataSourceModelAssociation
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
{
NSString* identifier = nil;
NSArray* content = self.contentArray;
// Sometimes idx might be nil...
if(idx && idx.row<content.count)
{
NdlFriend* friend = content[idx.row];
identifier=friend.accountUid;
}
return identifier;
}
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
NSIndexPath * indexPath=nil;
NSArray* content = self.contentArray;
NSInteger count = content.count;
for(NSInteger i=0;i<count;i++)
{
NdlFriend* friend = content[i];
if([identifier isEqualToString:friend.accountUid])
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
break;
}
}
return indexPath;
}
I set break points in both methods.
To test the methods, I opened the table view and scrolled down a little bit. Then, when pressing the home button:
modelIdentifierForElementAtIndexPath:inView: gets called once, with the index path of the top most visible row. The method returns the correct uid for this row.
So far so good.
Then I stop and relaunch the app. Here's what happens (I'm especially puzzled by the first hit break point):
modelIdentifierForElementAtIndexPath:inView: gets called, with nil as index path (the view argument contains the correct pointer of the table view).
indexPathForElementWithModelIdentifier:inView: gets called with a valid identifier string (and a valid index path is returned by the method).
indexPathForElementWithModelIdentifier:inView: gets called again (with the same identifier string).
The table view is refreshed, but scrolled to the very top.
Does someone know, why the restoration of the scroll position fails? Does maybe the call of modelIdentifierForElementAtIndexPath:inView: with nil as indexPath has something to do with it (or is this normal behavior).
There is a bug in iOS 6 regarding state restoration of Table Views in Navigation Controllers.
You can see the open radar here: rdar://13438788
As you can see, it's a duplicate, so Apple are aware of this.
Also, see this next link, the guy who posted that open radar also did this blog post, it has the suggested workarounds that the Apple Engineers told him.
It makes the state preservation/restoration for me not such an enjoyable feature to implement, although remember, this is for your users ! So you should just do the workaround anyway.
Note there are 2 workarounds, one for the table view's view information to restore (scroll offset for example), and another workaround for the use when implementing UIDataSourceModelAssociation which is your case.
http://useyourloaf.com/blog/2013/04/07/bug-table-view-state-not-restored-when-embedded-in-navigation-controller.html
I don't think the problem you're having of the table view's scroll position resetting has to do with the UIDataSourceModelAssociation methods.
I believe there's a bug with a table view controller embedded in a nav controller, that causes it to reset its scroll position after restoration. As I understand it, if the cells in your table view don't reorder, you don't need to implement the UIDataSourceModelAssociation methods and you should get the scroll position "for free" (i.e. as long as you've opted into state preservation and set the restorations IDs). I can't really confirm this explicitly from the documentation, except to point out that UITableView descends from UIScrollView, which saves its scroll position. I've tested that if you set the table view controller to be the root controller, or embed a table view controller in a tab bar controller, then the scroll position is restored.
I've filed a bug report, you should too, if you haven't already.
I agree with Aky and this might indeed be a bug in iOS 6. See my answer on a related question: https://stackoverflow.com/a/14567551/322548
Make sure that you're not performing an asynchronous fetch on your data. If you are fetching your data from viewDidLoad, make sure that you use a [myManagedObjectContext performBlockAndWait:^{}] call instead of a [myManagedObjectContext performBlock:^{}].
It may be that you have a race condition where self.contentArray is empty when indexPathForElementWithModelIdentifier gets called.