iOS AddressBook Adding multiple people to a group - ios

I am attempting to create a feature with my app that allows people to be easily added and/or removed from a group on iOS. The code below works perfectly if I:
1) add one person at a time
2) remove one person at a time
If however I select multiple people to add and/or remove simultaneously only the first action - first add if any or first remove if no additions - is committed to the address book. This method can be called over and over again with any one individual action and it will succeed. I cannot determine why I cannot get it to save multiple adds/deletes within the context of a single call to save. It should be pointed out that no error is giving and stepping through the code the booleans used to determine success indicate that everything worked perfectly.
I have attempted various combinations of adjusting code, for example: create a local address book, retrieve the groupRef, save the address book all within each of the for loops. I've also investigated reverting the address book after saving it to no avail. Thoughts or experiences?
-(IBAction)save:(id)sender
{
#synchronized(self)
{
CFErrorRef error = NULL;
BOOL success = NO;
BOOL successfulSave = NO;
ABAddressBookRef localAddressBook = ABAddressBookCreateWithOptions(NULL, &error);
group = ABAddressBookGetGroupWithRecordID(localAddressBook, groupID);
for (CFIndex addItemCount = 0; addItemCount < [theAddList count]; ++addItemCount)
{
NSNumber *addID = [theAddList objectAtIndex:addItemCount];
ABRecordRef thePersonID = ABAddressBookGetPersonWithRecordID(localAddressBook, [addID intValue]);
success = success || ABGroupAddMember(group, thePersonID, &error);
}
for (CFIndex removeItemCount = 0; removeItemCount < [theRemoveList count]; ++removeItemCount)
{
NSNumber *removeID = [theRemoveList objectAtIndex:removeItemCount];
ABRecordRef thePersonID = ABAddressBookGetPersonWithRecordID(localAddressBook, [removeID intValue]);
success = success || ABGroupRemoveMember(group, thePersonID, &error);
}
if (success)
{
successfulSave = ABAddressBookSave(localAddressBook, &error);
}
if (!(successfulSave && success))
{
UIAlertView *addressBookUpdateAlert = [[UIAlertView alloc] initWithTitle:#"AddressBook Error" message:#"An update to your address book could not be processed at this time. Try again shortly." delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil];
// we can't update a GUI element from a background thread - appears sensitivity didn't appear until iOS 8
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
[addressBookUpdateAlert show];
}];
}
else
{
[[self navigationController] popViewControllerAnimated:YES];
}
}
}

I can't explain it, but I made a minor change which should have done a better job of trapping an error and instead seems to caused to code to magically start working - which by the way is usually code for you over wrote memory somewhere.
Anyway I did the following and it started working:
Set success = YES before entering the loop
Changed the || test to an && test in both loops
All I changed and yet somehow it just started working...weird...I'm off to test/hammer the thing in case I'm seeing things...

Related

NSString value not getting accessed by if statement

I have this slideshow app that has 3 images, I've set up some strings in my .h file and I'm trying to get the ratings of the 3 images stored in core data.
I've written the code to store it and I know it works, my problem is that for the first two images I have to have the code outside of the final image which also contains the code to stop & save the ratings to core data.
Because they are outside of the stopslideshow code, the values set to rating1 and rating2 aren't transferring to the second if statement. I'm hoping this will all make sense once you see the code below...
I found another question similar to this and it said you need to declare the strings outside of both if statements first, but that hasn't worked for me.
- (void) SlideShowEngineDidNext:(SlideShowEngine *)slideShow {
countImg += 1;
// If no rating selected on first or second image then set rating to "Null"
if ((_slideshow.currentIndex == 1) || (_slideshow.currentIndex == 2)) {
if (_Sam1.enabled == YES || _Sam2.enabled == YES || _Sam3.enabled == YES || _Sam4.enabled == YES || _Sam5.enabled == YES) {
rating = #"Null";
NSLog(#"SlideShowEngineDidNext, index: %d\nUser did not rate.\nRating: %#", slideShow.currentIndex, rating);
} else {
if (_slideshow.currentIndex == 1) {
_rating1 = rating;
}
if (_slideshow.currentIndex == 2) {
_rating2 = rating;
}
NSLog(#"SlideShowEngineDidNext, index: %d\nRating: %#", slideShow.currentIndex, rating);
}
}
if (countImg == slideShow.images.count) {
// If user didn't select a rating on last image then set the rating to "Null"
if (_slideshow.currentIndex == 0) {
if (_Sam1.enabled == YES || _Sam2.enabled == YES || _Sam3.enabled == YES || _Sam4.enabled == YES || _Sam5.enabled == YES) {
rating = #"Null";
NSLog(#"SlideShowEngineDidNext, index: %d\nUser did not rate.\nRating: %#", slideShow.currentIndex, rating);
} else {
_rating3 = rating;
NSLog(#"SlideShowEngineDidNext, index: %d\nRating: %#", slideShow.currentIndex, rating);
}
}
// Stop the slideshow
[_slideshow stop];
// Display alert to end slideshow
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Survey Completed!" message:#"Press OK to exit!" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
NSLog(#"rating1: %#", _rating1);
NSLog(#"rating2: %#", _rating2);
NSLog(#"rating3: %#", _rating3);
// Save ratings
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSManagedObject* currentSurvey = [NSEntityDescription insertNewObjectForEntityForName:#"Ratings" inManagedObjectContext:context];
self.currentSurvey = currentSurvey;
[[self currentSurvey] setValue:_rating1 forKey:#"image1"];
[[self currentSurvey] setValue:_rating2 forKey:#"image2"];
[[self currentSurvey] setValue:_rating3 forKey:#"image3"];
NSError *error;
[[[self currentSurvey] managedObjectContext] save:&error];
}
}
From the NSLogs for rating1, 2 and 3 I'd get from this:
2014-01-11 17:06:35.145 Lab10 - Multimedia[41533:70b] rating1: (null)
2014-01-11 17:06:35.145 Lab10 - Multimedia[41533:70b] rating2: (null)
2014-01-11 17:06:35.146 Lab10 - Multimedia[41533:70b] rating3: 5
so from the if statement with currentIndex 1 and currentIndex2, the strings are being given their value but it's not transferring to the 2nd if statement where the slideshow ends and the data is saved.
if I try to save the data outside the ending if statement, then it just saves the first rating, then the first and second, and then all 3 ratings, all in different objects.
I just can't figure out the correct code placement, if there is any... to get this to work!
Thanks in advance.
Every time you run through this code you are creating a new NSManagedObject, essentially a new row in the database. Is that your intention?
Second, you are not capturing your errors on the save. This is bad. An error could be occurring and you would not know about it.
Have you walked through this code in the debugger and checked each value as you go through? Is the context !nil for example?
In trying to follow your conditional logic it appears that there are a lot of states you are not capturing and I suspect that is what is causing your nil values. You really need to walk through this, line by line, in the debugger and watch what the code is doing.
Your string assignments are just pointing to the same memory address all the time so whenever you change the value of rating any assignment also changes. Try using the following string assignments
_rating1 = [NSString stringWithString:rating];
_rating2 = [NSString stringWithString:rating];
_rating3 = [NSString stringWithString:rating];
See more here on Objective-C strings here http://www.techotopia.com/index.php/Working_with_String_Objects_in_Objective-C
THIS ANSWER WAS SUPPLIED BY THE OP:
Found the answer here
I had a problem with my NSStrings losing their value after being assigned, thanks for the guys who posted.
Found the answer here
I had a problem with my NSStrings losing their value after being assigned, thanks for the guys who posted.

Copying from "id" variable

I have created a class NetCalculator which I am calling when a button is pressed. The method calculate network it gets 2 NSStrings and returns an id object (either "Network" object or "UIAlertView". Then I am checking which object is and I present the data. When I am using the UIAlertView the app is crashing after showing 2-3 alerts.
Any ides why this happens? On terminal it doesnt show any error just some random hexadecimal.
-(IBAction)calculate:(id)sender {
id result;
Network *network = [[Network alloc]init];
NetCalculator *netCalculated = [[NetCalculator alloc] init];
result = [netCalculated calculateNetworkWithIP:ipLabel.text andSubnet:subnetLabel.text];
if([result isKindOfClass:[Network class]]){
network = result;
NSLog(#"network %#",network.networkIP);
}
else if([result isKindOfClass:[UIAlertView class]]) {
UIAlertView *alert;
alert = result;
[alert show];
}
};
Your code is quite strange to me. Your method calculateNetworkWithIP could return a Network result or a UIAlertView result. I wouldn't follow such an approach.
If the problem relies on memory you should show us hot that method is implemented.
Anyway, I would propose some changes (The following code does not take into account ARC or non ARC code). In particular, I would modify the calculateNetworkWithIP to return a Network result. An error will populated if a problem arises and it is passed as an argument.
- (Network*) calculateNetworkWithIP:(NSString *)ip subnet:(NSString*)subnet error:(NSError**)error
If all is ok, the result would be be a Network and so print it or reused it somewhere. Otherwise an NSError would be returned and based on that create and show an alert view.
So, here pseudo code to do it.
NetCalculator *netCalculated = [[NetCalculator alloc] init];
NSError* error = nil;
Network* networkResult = [netCalculated calculateNetworkWithIP:ipLabel.text subnet:subnetLabel.text error:&error];
if(error != nil) {
// create and show an alert view with the error you received
} else {
// all ok so, for example, save the result in a instance variable
}
To follow a similar approach you can take a look at why is "error:&error" used here (objective-c).

iOS Error: request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_

I'm using Nico Kreipke's FTPManager (click here to go to GiHub) to download some data from an FTP address.
The code works if it's run before the user's first interaction, after that it will usually fail (about 9 out of 10).
When it fails, the following message is written (0x_ are actually valid addresses):
request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_
That message isn't written by neither my code nor by FTPManager, but by Apple's. On its GitHub, I've found some one with the same error, but the source of it could possible be the same as mine. (That person wasn't using ARC.)
If I try to print the objects of those addresses with the pocommand, the console writes that there's no description available.
Also, the memory keeps adding up until the app receives a memory warning, and soon after the OS terminates it.
By pausing the app when that message appears, I can see that the main thread is in a run loop.
CFRunLoopRun();
The Code
self.ftpManager = [[FTPManager alloc] init];
[self downloadFTPFiles:#"192.168.2.1/sda1/1668"];
ftpManageris a strong reference.
The downloadFTPFiles: method:
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer* server = [FMServer serverWithDestination: basePath username:#"test" password:#"test"];
NSArray* serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary * sDataI = serverData[i];
NSString* name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber* type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString * nextDestination = [basePath stringByAppendingPathComponent: name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
[self.ftpManager downloadFile:name toDirectory:[NSURL fileURLWithPath:NSHomeDirectory()] fromServer:server];
}
}
}
What I've Done
I've tried running that code on several places:
The app delegate's application:didFinishLaunchingWithOptions:;
The viewDidLoad, viewWillAppear: and viewDidAppear: of the a view controller loaded just after the app launches and a view controller presented later.
By an action triggered with a button event.
The download of the data is always well performed when executed by the delegate or a view controller loaded with the app (with an exception). But when run after the user's first interaction with the app, it'll most likely fail with the mentioned error.
The exception for view controllers loaded before the user's first interaction is when the call is in either the viewWillAppear: or viewDidAppear: methods. When it's called a second time (for example, a tab of a tab bar controller) it'll also, most likely, fail.
The Question
Does anyone have an idea of what may be happening, or if I'm doing something wrong? Or any alternative solution, maybe?
Any help to solve this problem will be welcomed.
Thanks,
Tiago
I ended up sending the downloadFile:toDirectory:fromServer: message inside a dispatch_async block. I've also created an FTPManage for every file downloaded.
It worked, but I have no idea why.
I'm leaving this answer to whomever crosses with this problem.
If anyone can let me know why this technique worked, please comment bellow so I can update the answer.
Here's the new way I'm downloading each file:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
Again, If you know why this worked, let me know.
Thanks.
Full Method
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer *server = [FMServer serverWithDestination:basePath username:#"test" password:#"test"];
NSArray *serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary *sDataI = serverData[i];
NSString *name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber *type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString *nextDestination = [basePath stringByAppendingPathComponent:name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
}
}
}

Start something asynchronously, then wait on it later only if needed

I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.

cocoalibspotify error when attempting to play certain tracks

I'm attempting to play the top tracks from the result of an SPArtistBrowse using cocoalibspotify. Most of the time this works flawlessly, but occasionally I get the following error:
Error Domain=com.spotify.CocoaLibSpotify.error Code=3 "The track cannot be played"
This happens only for specific tracks, and for affected tracks it is consistent and repeatable (e.g. the top track for Armin van Buren, spotify:track:6q0f0zpByDs4Zk0heXZ3cO, always gives this error when attempting to play using the code below). The odd thing is, if I use the simple player sample app and enter an affected track's URL, the track plays fine; so my hunch is it has something to do with the track being loaded from an SPArtistBrowse.
Here is the code I am using to play tracks:
- (void)playTrack
{
SPTrack *track = [self.artistBrowse.topTracks objectAtIndex:self.currentTrackIndex];
[SPAsyncLoading waitUntilLoaded:track then:^(NSArray *tracks) {
[self.playbackManager playTrack:track callback:^(NSError *error) {
if (error) {
self.currentTrackIndex++;
if (self.currentTrackIndex < self.artistBrowse.topTracks.count) {
[self playTrack];
} else {
[self.activityIndicator stopAnimating];
self.activityIndicator.alpha = 0;
self.nowPlayingLabel.text = #"Spotify Error";
}
} else {
[self.activityIndicator stopAnimating];
self.activityIndicator.alpha = 0;
self.nowPlayingLabel.text = track.name;
// Set "Now Playing" info on the iOS remote control
MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
[dic setValue:track.name forKey:MPMediaItemPropertyTitle];
[dic setValue:self.artistLabel.text forKey:MPMediaItemPropertyArtist];
infoCenter.nowPlayingInfo = dic;
}
}];
}];
}
The artist browse should affect anything - a track is a track. However, if you can reliably reproduce it, please fork CocoaLibSpotify and add a failing unit test to the unit test suite - that way we can fix it.
It's also possible that the Spotify playback service was unavailable right at the wrong time, but that's a fairly rare occurrence.

Resources