MapKit: annotation views don't display until scrolling the map - objective-c

I expect once clicking the toolbar button like "cafe", cafes will show in the visible map region. Data are fetched from Google Places API. Everything works fine except pins don't display after clicking the button.
A latency is expected here for sure. Yet i do the fetch in a background queue and puts up a spinning wheel while waiting, and the spinning wheel hides when fetching and parsing is done. So I am quite sure the data is there at the moment that spinning wheel disappears. But the pins don't show up until I scroll the map.
I figure that scrolling map only triggers mapView:regionDidChangeAnimated:. But I can't figure out how it relates the problem. Can anyone help?
The source code of ViewController.m, where pretty much everything happens.
#import "ViewController.h"
#import "MapPoint.h"
#import "MBProgressHUD.h"
#define kGOOGLE_API_KEY #"AIzaSyCHqbAoY7WCL3l7x188ZM4ciiTixejzQ4Y"
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
#interface ViewController () <CLLocationManagerDelegate, MKMapViewDelegate>
#property (weak, nonatomic) IBOutlet MKMapView *mapView;
#property (strong, nonatomic) CLLocationManager *locationManager;
#property int currentDist;
#property CLLocationCoordinate2D currentCentre;
#end
#implementation ViewController
#synthesize mapView = _mapView;
#synthesize locationManager = _locationManager;
#synthesize currentDist = _currentDist;
#synthesize currentCentre = _currentCentre;
- (void)viewDidLoad{
[super viewDidLoad];
}
- (void)viewDidUnload{
[self setMapView:nil];
[super viewDidUnload];
}
// set the map region after launching
-(void)viewWillAppear:(BOOL)animated
{
//Instantiate a location object.
self.locationManager = [[CLLocationManager alloc] init];
//Make this controller the delegate for the location manager.
self.locationManager.delegate = self;
//Set some parameters for the location object.
[self.locationManager setDistanceFilter:kCLDistanceFilterNone];
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
// order: latitude(纬度), longitude(经度)
CLLocationCoordinate2D center = self.locationManager.location.coordinate;
// 单位是degree
MKCoordinateSpan span = MKCoordinateSpanMake(0.03, 0.03);
MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
[self.mapView setRegion:region animated:YES];
// NSLog(#"currentCentre is (%f , %f)", self.currentCentre.latitude, self.currentCentre.longitude);
}
// Get place tpye from button title
// All buttons share this one method
- (IBAction)toolbarButtonPressed:(id)sender
{
UIBarButtonItem *button = (UIBarButtonItem *)sender;
NSString *buttonTitle = [button.title lowercaseString];
//Use this title text to build the URL query and get the data from Google.
[self queryGooglePlaces:buttonTitle];
}
// Parse response JSON data
-(void)parseData:(NSData *)responseData {
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData
options:kNilOptions
error:&error];
//The results from Google will be an array obtained from the NSDictionary object with the key "results".
NSArray* places = [json objectForKey:#"results"];
[self plotPositions:places];
NSLog(#"Plot is done");
}
// Format query string
-(void) queryGooglePlaces: (NSString *) googleType {
// query string
NSString *url = [NSString stringWithFormat:#"https://maps.googleapis.com/maps/api/place/search/json?location=%f,%f&radius=%#&types=%#&sensor=true&key=%#", self.currentCentre.latitude, self.currentCentre.longitude, [NSString stringWithFormat:#"%i", _currentDist], googleType, kGOOGLE_API_KEY];
//string to URL
NSURL *googleRequestURL=[NSURL URLWithString:url];
// Retrieve data from the query URL by GCD
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: googleRequestURL];
[self parseData:data];
[MBProgressHUD hideHUDForView:self.view animated:YES];
});
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = #"Please Wait..";
}
#pragma mark - Map View Delegate
// called many times when map scrolling or zooming
// Use this to get currentCentre and currentDist (radius)
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
//Get the east and west points on the map so you can calculate the distance (zoom level) of the current map view.
MKMapRect mRect = self.mapView.visibleMapRect;
MKMapPoint eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect));
MKMapPoint westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect));
//Set your current distance instance variable.
self.currentDist = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint);
//Set your current center point on the map instance variable.
self.currentCentre = self.mapView.centerCoordinate;
// NSLog(#"currentCentre is (%f , %f)", self.currentCentre.latitude, self.currentCentre.longitude);
}
// Setup annotation objects
-(void)plotPositions:(NSArray *)data {
// 1 - Remove any existing custom annotations but not the user location blue dot.
for (id<MKAnnotation> annotation in self.mapView.annotations) {
if ([annotation isKindOfClass:[MapPoint class]]) {
[self.mapView removeAnnotation:annotation];
}
}
// 2 - Loop through the array of places returned from the Google API.
for (int i=0; i<[data count]; i++) {
//Retrieve the NSDictionary object in each index of the array.
NSDictionary* place = [data objectAtIndex:i];
// 3 - There is a specific NSDictionary object that gives us the location info.
NSDictionary *geo = [place objectForKey:#"geometry"];
// Get the lat and long for the location.
NSDictionary *loc = [geo objectForKey:#"location"];
// 4 - Get your name and address info for adding to a pin.
NSString *name=[place objectForKey:#"name"];
NSString *vicinity=[place objectForKey:#"vicinity"];
// Create a special variable to hold this coordinate info.
CLLocationCoordinate2D placeCoord;
// Set the lat and long.
placeCoord.latitude=[[loc objectForKey:#"lat"] doubleValue];
placeCoord.longitude=[[loc objectForKey:#"lng"] doubleValue];
// 5 - Create a new annotation.
MapPoint *placeObject = [[MapPoint alloc] initWithName:name address:vicinity coordinate:placeCoord];
[self.mapView addAnnotation:placeObject];
}
NSLog(#"addAnnotation is done");
}
// Setup annotation view
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
// Define your reuse identifier.
static NSString *identifier = #"MapPoint";
if ([annotation isKindOfClass:[MapPoint class]]) {
MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [self.mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
} else {
annotationView.annotation = annotation;
}
annotationView.enabled = YES;
annotationView.canShowCallout = YES;
annotationView.animatesDrop = YES;
// NSLog(#"annotation view is added");
return annotationView;
}
return nil;
}
#end

A couple of things:
Move your removeAnnotation and addAnnotation code to run on the UI thread, e.g.:
dispatch_async (dispatch_get_main_queye(), ^
{
[self.mapView addAnnotation:placeObject];
});
Move your viewWillAppear initialization code to viewDidLoad. viewWillAppear may be called multiple times during the lifetime of your view controller

Related

Xcode iOS app hanging on launch screen "semaphore_wait_trap()"

Excuse me but I'm a total Noob, not a programmer. I based a photo editing app on a template and customised it heavily with help from Google searches, tutorials etc.
Using Xcode 7.3.1, iOS 9.3, newer Photosframework and only objective C.
Ive got the app to a point that Im happy with it, except that I noticed on first launch, the app hangs (debug reports semaphore_wait_trap().
The app can't get to next step "request to access photos" alert pop up in iOS 9.3, and only way to get to it is to hit the home button, then see the grant access alert, then switch back to app. Then quit the app, reload it and then it runs fine overtime after that. This is of course not an ideal user experience.
I see if I pause on debug mode its hanging on: "semaphore_wait_trap()"
Ive googled and searched for days and can't find a solution to get the permissions alert popup to show on top of my app window.
Its beyond me. Any Ideas would be greatly appreciated.
See screen shot of the launch image that remains on top of the alert pop up.
If you press the "Home" button, the alert to grant access to photos appears.
The app delegate:
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if ([UIApplication instancesRespondToSelector:#selector(registerUserNotificationSettings:)]){
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
}
UILocalNotification *locationNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
if (locationNotification) {
// Sets icon badge number to zero
application.applicationIconBadgeNumber = 0;
}
// END Local Notification ==========================
return true;
}
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
// Resets icon's badge number to zero
application.applicationIconBadgeNumber = 0;
}
Here is a snippet of the main View controller (hope its not to long, not sure where the problem lies)
HomeVC.m:
#import "HomeVC.h"
#import "Configs.h"
#import "AAPLGridViewCell2.h"
#import "NSIndexSet+Convenience.h"
#import "UICollectionView+Convenience.h"
#import "AAPLRootListViewController.h"
#import "Configs.h"
#import "ImageEditorTheme.h"
#import "ImageEditorTheme+Private.h"
#import PhotosUI;
#import UIKit;
#interface HomeVC()
<
PHPhotoLibraryChangeObserver,
UICollectionViewDelegateFlowLayout,
UICollectionViewDataSource,
UICollectionViewDelegate
>
#property (nonatomic, strong) NSArray *sectionFetchResults;
#property (nonatomic, strong) NSArray *sectionLocalizedTitles;
#property (nonatomic, strong) PHCachingImageManager *imageManager;
#property CGRect previousPreheatRect;
#property (nonatomic, strong) IBOutlet UICollectionViewFlowLayout *flowLayout;
#property (nonatomic, assign) CGSize lastTargetSize;
#end
#implementation HomeVC
{
UIActivityIndicatorView *_indicatorView;
}
static NSString * const AllPhotosReuseIdentifier = #"AllPhotosCell";
static NSString * const CollectionCellReuseIdentifier = #"CollectionCell";
static NSString * const CellReuseIdentifier = #"Cell";
static CGSize AssetGridThumbnailSize;
- (void)awakeFromNib {
self.imageManager = [[PHCachingImageManager alloc] init];
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
}
- (void)dealloc {
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
_logoImage.layer.cornerRadius = 30;
[self loadPhotos];
[_libraryOutlet addTarget:self action:#selector(touchUp:) forControlEvents:UIControlEventTouchUpInside];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handle_data) name:#"reload_data" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideMenu) name:#"hide_menu" object:nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Begin caching assets in and around collection view's visible rect.
[self updateCachedAssets];
}
-(void)handle_data {
//[self.collectionView2 layoutIfNeeded];
//[self resetCachedAssets];
[self.collectionView2 reloadData];
[self updateCachedAssets];
NSLog(#"did it work?");
}
- (void)viewDidLayoutSubviews
{
NSInteger section = [self.collectionView2 numberOfSections] - 1;
NSInteger item = [self.collectionView2 numberOfItemsInSection:section] - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
[self.collectionView2 scrollToItemAtIndexPath:indexPath atScrollPosition:(UICollectionViewScrollPositionTop) animated:NO];
//[self loadPhotos];
}
-(void) loadPhotos {
PHFetchOptions *allPhotosOptions = [[PHFetchOptions alloc] init];
allPhotosOptions.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"creationDate" ascending:YES]];
PHFetchResult *allPhotos = [PHAsset fetchAssetsWithOptions:allPhotosOptions];
if (self.assetsFetchResults == nil) {
self.assetsFetchResults = allPhotos;
}
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance {
// Check if there are changes to the assets we are showing.
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
if (collectionChanges == nil) {
return;
}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue(), ^{
// Get the new fetch result.
self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];
UICollectionView *collectionView = self.collectionView;
if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
// Reload the collection view if the incremental diffs are not available
[collectionView reloadData];
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
[collectionView performBatchUpdates:^{
NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
if ([removedIndexes count] > 0) {
[collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
if ([insertedIndexes count] > 0) {
[collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
if ([changedIndexes count] > 0) {
[collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
} completion:NULL];
}
[self resetCachedAssets];
});
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.assetsFetchResults.count;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; {
CGFloat colum = 3.0, spacing = 0.0;
CGFloat value = floorf((CGRectGetWidth(self.view.bounds) - (colum - 1) * spacing) / colum);
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(value, value);
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
layout.minimumInteritemSpacing = spacing;
layout.minimumLineSpacing = spacing;
return CGSizeMake(value, value);
//return self.collectionView.frame.size;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PHAsset *asset = self.assetsFetchResults[indexPath.item];
// Dequeue an AAPLGridViewCell.
AAPLGridViewCell2 *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellReuseIdentifier forIndexPath:indexPath];
cell.representedAssetIdentifier = asset.localIdentifier;
// Request an image for the asset from the PHCachingImageManager.
[self.imageManager requestImageForAsset:asset
targetSize:CGSizeMake(130, 130)
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Set the cell's thumbnail image if it's still showing the same asset.
if ([cell.representedAssetIdentifier isEqualToString:asset.localIdentifier]) {
cell.thumbnailImage = result;
}
}];
CGPoint bottomOffset = CGPointMake(-0, self.collectionView.contentSize.height - self.collectionView.bounds.size.height + self.collectionView.contentInset.bottom);
[self.collectionView setContentOffset:bottomOffset animated:NO];;
return cell;
}
- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
// Prepare the options to pass when fetching the live photo.
PHAsset *asset = self.assetsFetchResults[indexPath.item];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
options.networkAccessAllowed = NO;
dispatch_async(dispatch_get_main_queue(), ^{
_indicatorView = [ImageEditorTheme indicatorView];
_indicatorView.center = self.containerView.center;
[self.containerView addSubview:_indicatorView];
[_indicatorView startAnimating];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
PreviewVC *prevVC = (PreviewVC *)[storyboard instantiateViewControllerWithIdentifier:#"PreviewVC"];
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFit options:options resultHandler:^(UIImage *result, NSDictionary *info) {
// Show the UIImageView and use it to display the requested image.
passedImage = result;
prevVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:prevVC animated:true completion:nil];
[_indicatorView stopAnimating];
}];
});
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// Update cached assets for the new visible area.
[self updateCachedAssets];
}
I managed to solve the issue. It was as simple as removing the call to "[self resetCachedAssets];" in "awakeFromNib"
Works great now.

watch os 2 not waking parent app and changing UITableView of parent

I have a watch app that is being updated for watch os 2. The sendmessage does not wake the parent app. According to the transition documentation is this how you would wake a parent in the background.
"The iOS app is always considered reachable, and calling this method from your Watch app wakes up the iOS app in the background as needed."
Has anyone had this problem? The only way to get data is to have the parent app already open.
Another weird thing is the watch app changes the uitableview for the parent app. When the -(IBAction)yesterdaySales:(id)sender is called on the watch, it changes the parent app UITableView instead of the watch tableview.
InterfaceController.m
#import "InterfaceController.h"
#import "MyRowController.h"
#import "ftDateParser.h"
#import WatchKit;
#import <WatchConnectivity/WatchConnectivity.h>
#interface InterfaceController() <WCSessionDelegate>
{
IBOutlet WKInterfaceDevice *it;
BOOL tday;
IBOutlet WKInterfaceLabel *lblCompany;
}
#end
#implementation InterfaceController
#synthesize myTable = _myTable;
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
// Configure interface objects here.
if([WCSession isSupported]){
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
//[self requestInfoPhone];
[self getToday];
}
- (void)willActivate {
// This method is called when watch view controller is about to be visible to user
[super willActivate];
}
- (void)didDeactivate {
// This method is called when watch view controller is no longer visible
[super didDeactivate];
}
-(void)requestInfoPhone{
NSDictionary *dic = #{#"request":#"ySales"};
[[WCSession defaultSession] sendMessage:dic
replyHandler:^(NSDictionary *replyInfo){
NSLog(#"The Reply: %#", replyInfo);
NSDictionary *location = replyInfo;
NSString *name = location[#"label"];
NSString *totalSales = location[#"totalSales"];
// NSString *test2 = location[#"rowText"];
NSMutableArray *sales = [[NSMutableArray alloc]init];
NSMutableArray *storeNames = [[NSMutableArray alloc]init];
sales = location[#"rowText"];
storeNames = location[#"storeNames"];
[self loadTable:sales names:storeNames company:name];
[_labelName setText:name];
[_labelTotalSales setText:totalSales];
tday = YES;
}
errorHandler:^(NSError *error){
NSLog(#"%#", error);
}
];
}
-(void)loadTable:(NSMutableArray*)tester names:(NSMutableArray*)names company:(NSString *)company{
[_myTable setNumberOfRows:[tester count] withRowType:#"row"];
[_labelName setText:company];
for (int i = 0; i < [tester count]; i++) {
MyRowController *vc = [_myTable rowControllerAtIndex:i];
[vc.testLabel setText:[ftDateParser currencyFormat: tester[i]]];
[vc.nameLabel setText:[ftDateParser parseName:names[i]]];
}
[_myTable scrollToRowAtIndex:(0)];
}
-(IBAction)yesterdaySales:(id)sender{
if (tday) {
[_ydaySales setTitle:#"Today Sales"];
[self requestInfoPhone];
}
else{
[_ydaySales setTitle:#"Yesterday Sales"];
[self getToday];
}
}
-(void)getToday{
NSDictionary *dic = #{#"request":#"todaySales"};
[[WCSession defaultSession] sendMessage:dic
replyHandler:^(NSDictionary *replyInfo){
NSDictionary *location = replyInfo;
NSString *name = location[#"label"];
NSString *totalSales = location[#"totalSales"];
// NSString *test2 = location[#"rowText"];
NSMutableArray *sales = [[NSMutableArray alloc]init];
NSMutableArray *storeNames = [[NSMutableArray alloc]init];
sales = location[#"rowText"];
storeNames = location[#"storeNames"];
[self loadTable:sales names:storeNames company:name];
[_labelName setText:name];
[_labelTotalSales setText:totalSales];
tday = YES;
}
errorHandler:^(NSError *error){
NSLog(#"%#", error);
}
];
}
#end
Parent.m
-(void)setUpAppForWatch{
done = NO;
if([WCSession isSupported]){
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
-(void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *,id> *)message replyHandler:(void (^)(NSDictionary<NSString *,id> * _Nonnull))replyHandler{
/*UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier identifier = UIBackgroundTaskInvalid;
dispatch_block_t endBlock = ^ {
if (identifier != UIBackgroundTaskInvalid) {
[application endBackgroundTask:identifier];
}
identifier = UIBackgroundTaskInvalid;
};
identifier = [application beginBackgroundTaskWithExpirationHandler:endBlock];*/
[self setUpAppForWatch];
[self getTheDate];
startDate = todayDay;
endDate = tomorrow;
//[self getTodaySalesforWatch];
NSString *currency = [ftDateParser currencyFormat:totalSales];
NSDictionary *dic = #{#"label": [NSString stringWithFormat:#"%#", #"Town Crier, Inc."],
#"totalSales": currency,
#"rowText": storeSalesData,//[NSString stringWithFormat:#"%#", currency]
#"storeNames":storeNames
};
NSString *request = [message objectForKey:#"request"];
if ([request isEqualToString:#"todaySales"]) {
[self getTodaySalesforWatch];
}
else if ([request isEqualToString:#"ySales"]){
[self connectToWebService];
}
if (done) {
replyHandler(dic);
}
}
Edit:
Maybe the changes to the parent app were happening before, but I didn't know cause the app was running in the background. Still can't get it to wake the parent app.
You don't link to the source of the quote at the top of your question but it must be referring to the openParentApplication method of WatchKit 1. Devices running WatchOS 2.0 cannot call openParentApplication.
The method you're implementing in the code in your question is for a WCSession, which only works for immediate communication between a WatchKit app extension and an iOS app that are both running at the same time. This method does not cause your iOS app to launch, neither in the background nor in the foreground. Other asynchronous communication methods must be used if both apps are not running at the time.

MKViewAnnotation custom annotations losing order when added in MKMapView

I need to show on my MkMapView about 10 locations and respective custom annotations images (depending from the values loaded by a JSON parsing). As suggested in previous answers I have created a custom annotation class to store some data but, again, I cannot get the RIGHT ORDER: the custom images on each map locations don't respect the right sequence of respective parsed values, while in a UITableView its all perfect. This is the simplified code:
The example of correspondence:
if parsed valuesID is 100 ---> annotation image must be 100.png
if parsed valuesID is 200 ---> annotation image must be 200.png
if parsed valuesID is 300 ---> annotation image must be 300.png
The viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
map.showsUserLocation = true;
map.mapType = MKMapTypeStandard;
#define MakeLocation(lat,lon) [[CLLocation alloc] initWithLatitude:lat longitude:lon]
locations= #[ MakeLocation(lat1,lon1), MakeLocation(lat2,lon2), MakeLocation(lat3,lon3), MakeLocation(lat4,lon4), MakeLocation(lat5,lon5), MakeLocation(lat6,lon6), MakeLocation(lat7,lon7), MakeLocation(lat8,lon8), MakeLocation(lat9,lon9), MakeLocation(lat10,lon10) ];
}
The parseMethod called by a UIButton:
- (IBAction)parseMethod {
[map removeAnnotations:map.annotations];
// THE COMPLEX CODE TO PARSE VALUES of valuesID
...
... // so here I have the full array of valuesID
...
// THE CONTROL FOR THE END OF COMPLETE PARSING (blocks, cycle, ... )
[self addAnnotations]; // here i'm sure to call method AFTER THE END of complete parsing
}
The MyAnnotation2.h custom class:
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#interface MyAnnotation2 : NSObject <MKAnnotation>
#property (nonatomic, assign) CLLocationCoordinate2D coordinate;
#property (nonatomic, assign) int valuesIDMyAnnotation2;
#end
The MyAnnotation2.m custom class:
#import "MyAnnotation2.h"
#implementation MyAnnotation2
#synthesize coordinate;
#synthesize valuesIDMyAnnotation2;
#end
The addAnnotations method (called AFTER the COMPLETE END of parsing):
- (void)addAnnotations {
[table reloadData]; // UITableView with rows populated with locations coordinates and respective valuesID
[table scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
for (int l=0; l<[locations count]; l++) {
annotation2 = [[MyAnnotation2 alloc] init]; // create MyAnnotation2 istance to assign custom properties
annotation2.valuesIDMyAnnotation2 = [[valuesID objectAtIndex:l] intValue];
annotation2.coordinate = [locations[l] coordinate];
[map addAnnotation: annotation2]; // here we call delegate with all necessary data to add annotations, both location coordinate and corresponding valuesID
NSLog(#"%d - COORDINATES: %f - %f",annotation2.valuesIDMyAnnotation2,annotation2.coordinate.latitude, annotation2.coordinate.longitude);
}
}
The UITableView delegate:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:
UITableViewCellStyleSubtitle reuseIdentifier:#"Cell"];
}
cell.textLabel.text = [NSString stringWithFormat:#"%#",[coordinates objectAtIndex:indexPath.row]]; // here coordinates are values from each location
if ([[valuesID objectAtIndex:indexPath.row] intValue] == 100) {
UIImage *image = [UIImage imageNamed:#"100.png"];
[cell.imageView setImage:image];
}
if ([[valuesID objectAtIndex:indexPath.row] intValue] == 200) {
UIImage *image = [UIImage imageNamed:#"200.png"];
[cell.imageView setImage:image];
}
if ([[valuesID objectAtIndex:indexPath.row] intValue] == 300) {
UIImage *image = [UIImage imageNamed:#"300.png"];
[cell.imageView setImage:image];
}
return cell
}
Finally, the viewForAnnotation delegate:
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>) annotation {
if ( ! [annotation isKindOfClass:[MyAnnotation2 class]])
{
((MKUserLocation *)annotation).title = #"My position";
return nil;
}
MKAnnotationView *pinView= [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"pinView"];
MyAnnotation2 *myPin = (MyAnnotation2 *)annotation;
if (myPin.valuesIDMyAnnotation2 == 100) {
pinView.image = [UIImage imageNamed:#"100.png"];
}
if (myPin.valuesIDMyAnnotation2 == 200) {
pinView.image = [UIImage imageNamed:#"200.png"];
}
if (myPin.valuesIDMyAnnotation2 == 300) {
pinView.image = [UIImage imageNamed:#"300.png"];
}
[pinView setFrame:CGRectMake(0, 0, 25, 25)];
return pinView;
}
EDIT - example of NSLogs results (code from addAnnotations method):
100 - COORDINATES lat1 - lon1 // here I expect annotation images100.png on location1
200 - COORDINATES lat2 - lon2 // ...
100 - COORDINATES lat3 - lon3
300 - COORDINATES lat4 - lon4
100 - COORDINATES lat5 - lon5
200 - COORDINATES lat6 - lon6
100 - COORDINATES lat7 - lon7
300 - COORDINATES lat8 - lon8
300 - COORDINATES lat9 - lon1
200 - COORDINATES lat10 - lon10
RESULTS:
On the UITableView its all PERFECT, I can see the right correspondence between locations coordinates and custom images, and also NSLog() gives the right correspondence of both location and valuesID. On the MKMapView, instead, custom annotation images are not added in the right sequence so I have right annotations images but in the wrong locations. Please, help me again to resolve this issue, thanks!
Put a break point on each of the pinView.image lines and when it is setting the image200.png, check what the coordinates are (you might need to NSLog them, I've never been great at digging deep in the debugger data). If you've got a mismatch there look through the rest of your code of anything else that could be changing the values of the locations, anything at all, and put a break point there. If that breakpoint gets triggered between the parseMethod and viewForAnnotation then you might have your culprit.

GMMGeoTileImageData error with MapKit

In my viewcontroller I use Maps and I load a list of pins.
When I move the map or zoom in or out it, my app crashes and displays this error:
[GMMGeoTileImageData isEqualToString:]: unrecognized selector sent to instance 0x862d3b0
This is my code of the view controller:
- (void)viewDidLoad
{
statoAnn = [[NSMutableString alloc] initWithFormat:#"false"];
//bottone annulla per tornare indietro
UIBarButtonItem *annullaButton = [[[UIBarButtonItem alloc] initWithTitle:#"Annulla" style:UIBarButtonItemStylePlain target:self action:#selector(backView)] autorelease];
self.navigationItem.leftBarButtonItem = annullaButton;
//inizializzo la mappa
mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 416)];
mapView.delegate = self;
mapView.mapType = MKMapTypeStandard;
[self.view addSubview:mapView];
[self setGmaps:arrData];
[super viewDidLoad];
}
/** inizializzo l'annotation del poi mappa **/
- (void) setGmaps:(NSMutableArray*)inputData {
// setto la lat e lng
CLLocationDegrees latitude;
CLLocationDegrees longitude;
CLLocationCoordinate2D poiLocation;
arrAnn = [[NSMutableArray alloc] init];
for(int i=0; i<[inputData count]; i++) {
//ricavo la lat e lng del pin
latitude = [[[inputData objectAtIndex:i] objectForKey:#"latitude"] doubleValue];
longitude = [[[inputData objectAtIndex:i] objectForKey:#"longitude"] doubleValue];
// setto la location del poi
poiLocation.latitude = latitude;
poiLocation.longitude = longitude;
//[[[CLLocation alloc] initWithLatitude:latitude longitude:longitude] autorelease];
//setto il pin
Annotation *ann = [[Annotation alloc] initWithCoordinate:poiLocation];
ann.title = [[inputData objectAtIndex:i] objectForKey:#"label"];
[arrAnn addObject:ann];
[ann release];
}
if (nil != self.arrAnn) {
[self.mapView addAnnotations:arrAnn];
//self.ann = nil;
self.arrAnn = nil;
}
}
/** setto il pin nella mappa ***/
- (void)setCurrentLocation:(CLLocation *)location {
MKCoordinateRegion region = {{0.0f, 0.0f}, {0.0f, 0.0f}};
region.center = location.coordinate;
region.span.longitudeDelta = 0.1f;
region.span.latitudeDelta = 0.1f;
[self.mapView setRegion:region animated:YES];
[self.mapView regionThatFits:region];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapViewTemp viewForAnnotation:(id <MKAnnotation>)annotation {
MKPinAnnotationView *view = nil; // return nil for the current user location
view = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:#"identifier"];
if (nil == view) {
view = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"identifier"] autorelease];
view.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
[view setPinColor:MKPinAnnotationColorPurple];
[view setCanShowCallout:YES];
[view setAnimatesDrop:YES];
if (![statoAnn isEqualToString:#"true"]) {
CLLocation *location = [[CLLocation alloc] initWithLatitude:annotation.coordinate.latitude
longitude:annotation.coordinate.longitude];
[self setCurrentLocation:location];
statoAnn = [NSMutableString stringWithFormat:#"true"];
}
return view;
}
In viewForAnnotation, this line:
statoAnn = [NSMutableString stringWithFormat:#"true"];
sets statoAnn to an autoreleased string.
When the method exits, release is called on statoAnn and it no longer owns the memory it was pointing to. When the method is called again when you zoom or move the map, the memory that statoAnn was pointing to is now used by something else (GMMGeoTileImageData in this case). That object is not an NSString and doesn't have an isEqualToString: method and you get the error you are seeing.
To fix this, set statoAnn so the value is retained like you are doing in viewDidLoad. For example, you could change it to:
statoAnn = [[NSMutableString alloc] initWithFormat:#"true"];
You could also declare statoAnn as a property (#property (nonatomic, copy) NSString *statoAnn) and just set it using self.statoAnn = #"true";. The property setter will do the retaining for you.
However, you don't need to use a string to hold a "true" and "false" value. It's much easier and efficient to use a plain BOOL and you won't have to worry about retain/release since it's a primitive type and not an object.
The other thing is that viewForAnnotation is not the right place to be setting the map view's region in the first place. You can do that in viewDidLoad after the annotations are added.
Another thing: At the top of viewForAnnotation, you have the comment "return nil for the current user location" but that code doesn't do that. It just initializes the view to nil. To actually do what the comment says, you need this:
MKPinAnnotationView *view = nil;
// return nil for the current user location...
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
Finally, if the dequeueReusableAnnotationViewWithIdentifier does return a view (if view != nil), you need to set view.annotation to the current annotation since the re-used view may have been for a different annotation.

UIScrollView with pages enabled and device rotation/orientation changes (MADNESS)

I'm having a hard time getting this right.
I've got a UIScrollView, with paging enabled. It is managed by a view controller (MainViewController) and each page is managed by a PageViewController, its view added as a subview of the scrollView at the proper offset. Scrolling is left-right, for standard orientation iPhone app. Works well. Basically exactly like the sample provided by Apple and also like the Weather app provided with the iPhone.
However, when I try to support other orientations, things don't work very well. I've supported every orientation in both MainViewController and PageViewController with this method:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
However, when I rotate the device, my pages become quite skewed, and there are lots of drawing glitches, especially if only some of the pages have been loaded, then I rotate, then scroll more, etc... Very messy.
I've told my views to support auto-resizing with
theView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
But to no avail. It seems to just stretch and distort my views.
In my MainViewController, I added this line in an attempt to resize all my pages' views:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * ([self.viewControllers count]), self.scrollView.frame.size.height);
for (int i = 0; i < [self.viewControllers count]; i++) {
PageViewController *controller = [self.viewControllers objectAtIndex:i];
if ((NSNull *)controller == [NSNull null])
continue;
NSLog(#"Changing frame: %d", i);
CGRect frame = self.scrollView.frame;
frame.origin.x = frame.size.width * i;
frame.origin.y = 0;
controller.view.frame = frame;
}
}
But it didn't help too much (because I lazily load the views, so not all of them are necessarily loaded when this executes).
Is there any way to solve this problem?
I have successfully achieved this using below method:
.h file code:
#interface ScrollViewController2 : UIViewController <UIWebViewDelegate, UIScrollViewDelegate> {
NSMutableArray *views;
int currentPage;
IBOutlet UIScrollView *scrollView;
BOOL bolPageControlUsed;
int intCurrIndex;
NSMutableArray *arrayContentData;
NSMutableArray *viewControllers;
}
#property (nonatomic, retain) IBOutlet UIScrollView *scrollView;
#property (nonatomic, retain) NSMutableArray *arrayContentData;
#property (nonatomic, retain) NSMutableArray *viewControllers;
#property (nonatomic) BOOL bolPageControlUsed;
#property (nonatomic) int intCurrIndex;
-(void)bindPages;
- (void)setUpScrollView;
- (void)alignSubviews;
- (NSURLRequest *)getPageFromDocumentsDirectory:(NSString *)pstrPageName;
-(void)initiateScrollView;
-(void)loadScrollViewWithPage:(int)page;
============================================================================================
.m file
#synthesize scrollView;
#synthesize arrayContentData, viewControllers, bolPageControlUsed, intCurrIndex;
- (void)viewDidLoad {
[super viewDidLoad];
[self bindPages];
//[self setUpScrollView];
[self initiateScrollView];
}
#pragma mark -
#pragma mark Bind Pages
-(void)bindPages{
self.arrayContentData = [[NSMutableArray alloc] init];
[self.arrayContentData addObject:#"1.html"];
[self.arrayContentData addObject:#"2.html"];
[self.arrayContentData addObject:#"3.html"];
[self.arrayContentData addObject:#"4.html"];
[self.arrayContentData addObject:#"5.html"];
[self.arrayContentData addObject:#"6.html"];
[self.arrayContentData addObject:#"1.html"];
[self.arrayContentData addObject:#"2.html"];
[self.arrayContentData addObject:#"3.html"];
[self.arrayContentData addObject:#"4.html"];
[self.arrayContentData addObject:#"5.html"];
[self.arrayContentData addObject:#"6.html"];
[self.arrayContentData addObject:#"1.html"];
[self.arrayContentData addObject:#"2.html"];
[self.arrayContentData addObject:#"3.html"];
[self.arrayContentData addObject:#"4.html"];
[self.arrayContentData addObject:#"5.html"];
[self.arrayContentData addObject:#"6.html"];
[self.arrayContentData addObject:#"1.html"];
[self.arrayContentData addObject:#"2.html"];
[self.arrayContentData addObject:#"3.html"];
[self.arrayContentData addObject:#"4.html"];
[self.arrayContentData addObject:#"5.html"];
[self.arrayContentData addObject:#"6.html"];
}
#pragma mark -
#pragma mark Get Filename from Document Directory
- (NSURLRequest *)getPageFromDocumentsDirectory:(NSString *)pstrPageName {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [paths objectAtIndex:0];
NSString *yourFilePath = [NSString stringWithFormat:#"%#/Html/%#", documentDirectory, pstrPageName];
NSURL *url = [NSURL fileURLWithPath:yourFilePath];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
return requestObj;
}
#pragma mark -
#pragma mark ScrollView Methods
-(void)initiateScrollView{
views = [[NSMutableArray alloc] initWithCapacity:[self.arrayContentData count]];
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (unsigned i = 0; i < [self.arrayContentData count]; i++) {
[controllers addObject:[NSNull null]];
}
self.viewControllers = controllers;
[controllers release];
scrollView.contentSize = CGSizeMake([self.arrayContentData count]*scrollView.bounds.size.width,
scrollView.bounds.size.height);
scrollView.delegate = self;
if(self.intCurrIndex == 0){
[self loadScrollViewWithPage:self.intCurrIndex];
}
}
-(void)loadScrollViewWithPage:(int)page{
if (page < 0) return;
if (page >= [self.arrayContentData count]) return;
// replace the placeholder if necessary
NSString *strContentName = [self.arrayContentData objectAtIndex:page];
//UIImageView *controller = [viewControllers objectAtIndex:page];
UIWebView *controller = [viewControllers objectAtIndex:page];
if ((NSNull *)controller == [NSNull null]) {
UIView *v = [[UIView alloc] initWithFrame:scrollView.bounds];
v.backgroundColor = [UIColor colorWithHue:arc4random()/(float)0x100000000
saturation:0.75
brightness:1.0
alpha:1.0];
controller = [[UIWebView alloc] initWithFrame:v.bounds];
controller.delegate = self;
controller.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
controller.center = CGPointMake(v.bounds.size.width/2, v.bounds.size.height/2);
[controller loadRequest:[self getPageFromDocumentsDirectory:strContentName]];
[v addSubview:controller];
[controller release];
[scrollView addSubview:v];
[views addObject:v];
[viewControllers replaceObjectAtIndex:page withObject:controller];
[v release];
}
[self alignSubviews];
/*
// add the controller's view to the scroll view
if (nil == controller.superview) {
CGRect frame = scrollView.frame;
frame.origin.x = frame.size.width * page;
//frame.origin.y = 0;
controller.frame = frame;
[scrollView addSubview:controller];
}*/
}
-(void)scrollViewDidScroll:(UIScrollView *)sender{
// We don't want a "feedback loop" between the UIPageControl and the scroll delegate in
// which a scroll event generated from the user hitting the page control triggers updates from
// the delegate method. We use a boolean to disable the delegate logic when the page control is used.
if (self.bolPageControlUsed) {
// do nothing - the scroll was initiated from the page control, not the user dragging
return;
}
// Switch the indicator when more than 50% of the previous/next page is visible
currentPage = scrollView.contentOffset.x / scrollView.bounds.size.width;
[self loadScrollViewWithPage:currentPage];
}
// At the end of scroll animation, reset the boolean used when scrolls originate from the UIPageControl
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.bolPageControlUsed = NO;
}
#pragma mark -
#pragma mark setUp ScrollView
- (void)setUpScrollView {
// Set up some colorful content views
views = [[NSMutableArray alloc] initWithCapacity:[self.arrayContentData count]];
for (int i = 0; i < [self.arrayContentData count]; i++) {
UIView *v = [[UIView alloc] initWithFrame:scrollView.bounds];
v.backgroundColor = [UIColor colorWithHue:arc4random()/(float)0x100000000
saturation:0.75
brightness:1.0
alpha:1.0];
NSString *strContentName = [self.arrayContentData objectAtIndex:i];
UIWebView *controller = [[UIWebView alloc] initWithFrame:v.bounds];
controller.delegate = self;
controller.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
controller.center = CGPointMake(v.bounds.size.width/2, v.bounds.size.height/2);
[controller loadRequest:[self getPageFromDocumentsDirectory:strContentName]];
[v addSubview:controller];
[controller release];
[scrollView addSubview:v];
[views addObject:v];
[v release];
}
[self alignSubviews];
[scrollView flashScrollIndicators];
}
#pragma mark -
#pragma mark Align Scroll Subviews
- (void)alignSubviews {
// Position all the content views at their respective page positions
scrollView.contentSize = CGSizeMake([self.arrayContentData count]*scrollView.bounds.size.width,
scrollView.bounds.size.height);
NSUInteger i = 0;
for (UIView *v in views) {
v.frame = CGRectMake(i * scrollView.bounds.size.width, 0,
scrollView.bounds.size.width, scrollView.bounds.size.height);
for (UIWebView *w in v.subviews) {
[w setFrame:v.bounds];
}
i++;
}
}
#pragma mark -
#pragma mark UIWebView delegate
- (void)webViewDidStartLoad:(UIWebView *)webView {
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
}
#pragma mark -
#pragma mark Orientation
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return YES;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration {
currentPage = scrollView.contentOffset.x / scrollView.bounds.size.width;
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
duration:(NSTimeInterval)duration {
[self alignSubviews];
//NSLog(#"%f", currentPage * scrollView.bounds.size.width);
scrollView.contentOffset = CGPointMake(currentPage * scrollView.bounds.size.width, 0);
}
I hope, it will be helpful to all.
Cheers.
Is it necessary to have a separate UIViewController (PageViewController) for every page in your UIScrollView? Why not let your MainViewController take care of this.
Resizing your views (and your UI in general) after rotating the device is much easier when you build your UI in Interface Builder.
I'm not absolutely sure I understand you right.. however, some thoughts:
The frame property is one thing (A), how the view contents are displayed in there is another (B). The frame CGRect is the (theoretical) boundary of your view in the superview (parent view) .. however, your View does not necessarily need to fill that whole frame area.
Regarding (A):
Here we have the UIView's autoresizingMask property to set how the frame is resized when the superview is resized. Which happens when you change the orientation. However, you can usually rely on the default settings (worked for me so far).
Regarding (B):
How the view contents are distributet in the view frame is specified by UIView's property contentMode. With this property, you can set that the aspect ratio needs to stay intact. Set it to UIViewContentModeScaleAspectFit for example, or something else..
see here:
http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIView_Class/UIView/UIView.html#//apple_ref/doc/uid/TP40006816-CH3-SW99
PS: I wrote "theoretical", because your view contents may also exceed those frame boundaries - they are only limiting the view when UIView's clipsToBounds property is set to YES. I think it's a mistake that Apple set this to NO by default.
In addition to what Efrain wrote, note that the frame property IS NOT VALID if the view transform is other than the identity transform -- i.e., when the view is rotated.
Of course, you accounted for the fact that your views need to be at a new offset position, right?