Here's what I'm doing:
- (void)doCreateGroup {
[[self contentView] endEditing:true];
NSString * newString = [[[[self contentView] groupNameField] text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString * firstError = nil;
if ([newString length] == 0) {
firstError = #"Missing group name";
}
NSError * groupsError = nil;
NSArray * groups = [self.contactStore groupsMatchingPredicate:nil error:&groupsError];
for (CNGroup * group in groups) {
if ([group.name isEqualToString:newString]) {
firstError = #"Group already exists";
}
}
if (firstError) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:firstError] animated:YES completion:nil];
return;
}
CNMutableGroup * newGroup = [CNMutableGroup new];
[newGroup setName:newString];
CNSaveRequest *saveRequest = [CNSaveRequest new];
[saveRequest addGroup:newGroup toContainerWithIdentifier:nil];
NSError * error = nil;
[self.contactStore executeSaveRequest:saveRequest error:&error];
if (error) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:[error localizedDescription]] animated:YES completion:nil];
} else {
CNSaveRequest *saveRequest2 = [CNSaveRequest new];
NSArray * groupsAgain = [self.contactStore groupsMatchingPredicate:nil error:&groupsError];
CNGroup * gotGroup;
for (CNGroup * group in groupsAgain) {
if ([group.name isEqualToString:newString]) {
gotGroup = group;
}
}
for (CNContact * contact in self.selectedContactsArray) {
[saveRequest2 addMember:contact toGroup:gotGroup];
}
NSError * error1 = nil;
[self.contactStore executeSaveRequest:saveRequest2 error:&error1];
if (error) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:[error1 localizedDescription]] animated:YES completion:nil];
} else {
[[self navigationController] dismissViewControllerAnimated:true completion:nil];
}
}
}
this works to create a CNGroup and then add contacts to said CNGroup. Works for all contacts EXCEPT for unified contacts. I've tried everything possible to make this work and it just doesn't. It likely has something to do with the unified CNContact's identifier since that identifier is only stored in temp memory so it can't be added to a CNGroup since it doesn't really haver a REAL CNContact identifier. Contacts framework is a mess! Any help would be appreciated. I've also filed a tech support request with Apple.
EDIT:
One way to get around this is to use Address Framework that is now deprecated. I can add as many unified contacts to Address groups by doing this.
ABRecordRef group = ABGroupCreate();
ABAddressBookAddRecord(addressBook, group, nil);
ABRecordSetValue(group, kABGroupNameProperty,#"My Groups", nil);
for (int i=0;i < nPeople;i++) {
ABRecordRef ref = CFArrayGetValueAtIndex(allPeople,i);
ABGroupAddMember(group, ref, nil);
ABAddressBookSave(addressBook, nil);
}
this does save everything in the contact book to a group, all visible contacts that is. so it does store the Unified contact into the group. if you unlink the contacts while they are in a group, both contacts stay within the group. so the old framework works to solve this. just seems ridiculous that it can't be solved with new Contacts framework. Again, I may be missing something with the new Contacts framework, so if this is possible with the current Contacts framework in iOS please let me know.
i figured it out. this is a mess
step one:
NSMutableArray * finalArray = [NSMutableArray array];
NSMutableArray * unifiedContacts = [NSMutableArray array];
NSMutableArray * fullContacts = [NSMutableArray array];
CNContactFetchRequest * request = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];
[request setSortOrder:CNContactSortOrderGivenName];
[self.contactStore enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
[unifiedContacts addObject:contact];
}];
CNContactFetchRequest * request2 = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];
[request2 setUnifyResults:false];
[request2 setSortOrder:CNContactSortOrderGivenName];
[self.contactStore enumerateContactsWithFetchRequest:request2 error:nil usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
[fullContacts addObject:contact];
}];
for (CNContact * contctUn in unifiedContacts) {
NSMutableArray * nestedContacts = [NSMutableArray array];
for (CNContact * contct in fullContacts) {
if ([contctUn isUnifiedWithContactWithIdentifier:contct.identifier]) {
[nestedContacts addObject:contct];
}
}
if (nestedContacts.count) {
[finalArray addObject:#{#"contact" : contctUn, #"linked" : nestedContacts}];
} else {
[finalArray addObject:#{#"contact" : contctUn}];
}
}
self.mainArray = [finalArray mutableCopy];
this pulls in all contacts from unified contacts and then pulls in all un-unified contacts, splices the groups together and saves them as dictionaries with "linked" being an array of linked contacts if the contact is indeed linked to the contact in question.
step 2: create a group ... this is pretty simple, no need to show the code since this is pretty easy
step 3:
for (id obj in self.filteredSearchArray) {
if ([obj valueForKey:#"linked"]) {
for (id obj2 in [obj valueForKey:#"linked"]) {
[self.selectedContactsArray addObject:obj2];
}
}
}
CNSaveRequest *saveRequest2 = [CNSaveRequest new];
for (CNContact * contact in self.selectedContactsArray) {
[saveRequest2 addMember:contact toGroup:[newGroup copy]];
}
NSError * error1 = nil;
[self.contactStore executeSaveRequest:saveRequest2 error:&error1];
self.selectedContactsArray is the array that contains the contacts you want in the group. it contains all contacts you want in the group in addition it contains the sublinked contacts if a contact you want in the group is linked to a user.
when this save request executes the group now contains the unified contact.
this is a mess. Contacts Framework in iOS is a mess, but this works. No app that creates groups for contacts has solve this, so here's the million dollar solution.
That seems odd indeed. As at least a workaround, have you tried to fetch the selected contacts with a CNContactFetchRequest that has its unifyResults set to false?
I mean, I don't know where you get the selectedContactsArray from, I assume either you can modify an existing request that gave you that data accordingly or you have to somehow refetch the contacts again. That's probably really, really ugly, as you would have to construct a fetch request with a predicate or key set that is guaranteed to match the same contacts (and only those contacts) plus said unifyResults member set to false.
I'd imagine something like this (sorry for using swift, it's a little compacter for me right now, I hope that's okay):
let allMyIds: [String] = self.selectedContactsArray.map { $0.identifier }
let predicate: NSPredicate = CNContact.predicateForContacts(withIdentifiers: allMyIds)
let fetchRequest = CNContactFetchRequest(keysToFetch: someKeys)
// not sure what you'd need here for someKeys...
// I assume it would have to be a key definitely present in all contacts you
// are interested in, e.g. name? I might be wrong though...
fetchRequest.unifyResults = false
_ = self.contactStore.enumerateContacts(with: fetchRequest, usingBlock: { contact, errorPointer in
// your group adding/save preparation code here
})
I admit I am not that familiar with the Contacts framework, so I can't say whether that is really feasible. Especially the set of keys you'd have to provide to the enumerate... method might be tricky if you don't have a key that's guaranteed to be part of all contacts you want.
I apologize for such a half-baked answer, but maybe it can at least give you a new impulse.
Related
I'm trying out the "Sample Blog App" on Parse Server for iOS and cannot figure out what is the smartes way to fetch all child objects of another class (together with the parent objects).
The "Sample Blog App" (which creates automatically when you create a new account) contains the classes Comment and Post. The Comment class contains a relation to the Post class as shown below (from the dashboard), but there is no relation in the opposite direction.
Now, I want to fetch all posts and all the comments related to each post. The code below does that, but I'm assuming there must be a smarter way...? If you know how, please share. Thanks in advance!
- (void)fetchPosts {
NSString *commentsKey = #"comments";
NSString *postKey = #"post";
PFQuery *query = [PFQuery queryWithClassName:#"Comment"];
[query includeKey:postKey];
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if (error == nil) {
NSMutableArray *array = [[NSMutableArray alloc]init];
for (PFObject *comment in objects) {
PFObject *post = [comment objectForKey:postKey];
NSDictionary *existingPostDict = [[array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K = %#", #"post.objectId", post.objectId]] firstObject];
if (existingPostDict) {
// update comments
NSArray *comments = [[existingPostDict objectForKey:commentsKey] arrayByAddingObject:comment];
// create new dictionary and replace the old one
NSDictionary *newPostDict = [[NSDictionary alloc]initWithObjectsAndKeys:[existingPostDict objectForKey:postKey], postKey, comments, commentsKey, nil];
[array replaceObjectAtIndex:[array indexOfObject:existingPostDict] withObject:newPostDict];
}
else {
// first post, create a new dict
NSDictionary *newPostDict = [[NSDictionary alloc]initWithObjectsAndKeys:post, postKey, #[comment], commentsKey, nil];
[array addObject:newPostDict];
}
}
self.posts = array; // assuming: #property (nonatomic, strong) NSArray *posts;
}
else {
NSLog(#"Error fetching posts: %#", error.localizedDescription);
}
}];
}
Instead of using include on your query you should use whereKey:equals: and pass the post object as the second argument. This will filter and return only the comment objects that contain that have that post as their value for post
One problem I see with your query is that there is a possibility this will not fetch every post in the database. If a post has 0 comments, none of the Comment objects will have a reference to it and thus you will not receive it.
Therefore you should actually do a query on "Post" and in its completion do a query on "Comment". This way you will not miss any posts with 0 comments. When you do this, you will not need to include the "post" key in the Comment query. This has multiple benefits.
First, each include is also another query for that object. So each new Comment object will create another query in the backend. You will get rid of this automatically.
Second, for a "Post" with multiple comments, you will be querying for the same post multiple times and that same post will be returned multiple times which consumes unnecessary bandwidth.
After getting Posts and Comments separately just combine them.
Apart from that I would do the combining like so which I find more readable but that is just personal preference.
- (void)fetchPosts {
NSString *commentsKey = #"comments";
NSString *postKey = #"post";
PFQuery *query = [PFQuery queryWithClassName:#"Comment"];
[query includeKey:postKey];
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if (error == nil) {
NSMutableArray *array = [[NSMutableArray alloc]init];
NSMutableDictionary *d = [NSMutableDictionary dictionary];
for (PFObject *comment in objects) {
PFObject *post = [comment objectForKey:postKey];
if (d[post.objectId]) {
[d[post.objectId][commentsKey] addObject:comment];
}
else{
d[post.objectId] = [NSMutableDictionary dictionary];
d[post.objectId][postKey]=post;
d[post.objectId][commentsKey] = [NSMutableArray arrayWithObject:comment];
}
}
for (NSString *key in [d allKeys]) {
[array addObject:d[key]];
}
self.posts = array; // assuming: #property (nonatomic, strong) NSArray *posts;
}
else {
NSLog(#"Error fetching posts: %#", error.localizedDescription);
}
}];
}
This is how I did it, using findObjectsInBackground together with continueWithSuccessBlock: methods (for better error handling one can choose continueWithBlock: instead):
- (void)fetchPosts {
/**
create "post" and "comment" queries and use a BFTask-method from
Bolts.framework to chain downloading tasks together (bundled with Parse SDK)
*/
NSMutableArray *posts = [NSMutableArray new];
PFQuery *postQuery = [PFQuery queryWithClassName:#"Post"];
[[[postQuery findObjectsInBackground] continueWithSuccessBlock:^id(BFTask * task) {
[posts addObjectsFromArray:task.result];
PFQuery *commentsQuery = [PFQuery queryWithClassName:#"Comment"];
return [commentsQuery findObjectsInBackground];
}] continueWithSuccessBlock:^id(BFTask * task) {
/**
loop through posts and filter out comments with the same objectId in post,
then create a dictionary with post and related comments. done! :)
*/
NSMutableArray *postsAndComments = [NSMutableArray new];
for (PFObject *post in posts) {
NSArray *comments = [task.result filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"%K == %#", #"post.objectId", post.objectId]];
[postsAndComments addObject:#{#"post":post, #"comments":comments}];
}
/**
note: BFTask-blocks not running in main thread!
*/
dispatch_async(dispatch_get_main_queue(), ^{
self.posts = postsAndComments; // assuming: #property (nonatomic, strong) NSArray *posts;
});
return nil;
}];
}
I'm trying to implement a scoreboard via GameKit, and I'm running into a problem where my query is only returning the scores for the localPlayer. I've set up two test users in itunes connect and am seeing this issue with both of them, it only returns the highest score for whichever player is currently logged in and doesn't return the other player's score at all. I have a table view which is populated by two arrays, one for player scores, one for player names.
- (void)loadGlobalHighScores
{
GKLeaderboard *leaderboard = [[GKLeaderboard alloc] init];
leaderboard.identifier = #"MyLeaderBoardIdentifier";
leaderboard.playerScope = GKLeaderboardPlayerScopeGlobal;
leaderboard.timeScope = GKLeaderboardTimeScopeAllTime;
leaderboard.range = NSMakeRange(1, 100);
[leaderboard loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) {
NSMutableArray *playerIDs = [NSMutableArray array];
NSMutableArray *playerScores = [NSMutableArray array];
for (GKScore *score in scores)
{
[playerIDs addObject:score.playerID];
[playerScores addObject:[NSNumber numberWithLongLong:score.value]];
}
self.playerScores = playerScores;
[GKPlayer loadPlayersForIdentifiers:playerIDs withCompletionHandler:^(NSArray *players, NSError *error) {
NSMutableArray *playerNames = [NSMutableArray array];
for (GKPlayer *player in players)
{
[playerNames addObject:player.displayName];
}
self.playerNames = playerNames;
[self.highScoresTable reloadData];
}];
}];
}
As far as I can tell I'm doing everything right, I'm not sure whether this is some issue with test user accounts or what.
Hi I need to download over 2000 records from azure , the maximum you can download is 1000 at the time , so I need to use a completion handler to download 200 at the time.
They posted this code as an example but I don't know how to use.
If I copy this to Xcode there is an error
(bool)loadResults() - Error " Expect Method Body "
Returning data in pages
Mobile Services limits the amount of records that are returned in a single response. To control the number of records displayed to your users you must implement a paging system. Paging is performed by using the following three properties of the MSQuery object:
BOOL includeTotalCount
NSInteger fetchLimit
NSInteger fetchOffset
In the following example, a simple function requests 20 records from the server and then appends them to the local collection of previously loaded records:
- (bool) loadResults() {
MSQuery *query = [self.table query];
query.includeTotalCount = YES;
query.fetchLimit = 20;
query.fetchOffset = self.loadedItems.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.loadedItems addObjectsFromArray:items];
//set a flag to keep track if there are any additional records we need to load
self.moreResults = (self.loadedItems.count < totalCount);
}
}];
}
thanks for your help.
If you are getting Error " Expect Method Body " then you copied it into your code incorrectly and there is a formatting issue.
If you want to load data with paging in a single call, I would do something like this:
in your .h file declare
typedef void (^CompletionBlock) ();
#property (nonatomic, strong) NSMutableArray *results;
in your .m file
- (void)loadData
{
self.results = [[NSMutableArray alloc] init];
MSClient *client = [MSClient clientWithApplicationURLString:#"YOUR_URL" applicationKey:#"YOUR_KEY"]
MSTable *table = [client tableWithName:#"YOUR_TABLE"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"YOUR_SELECT_FILTER"];
MSQuery *query = [[MSQuery alloc] initWithTable:table predicate:predicate];
//note the predicate is optional. If you want all rows skip the predicate
[self loadDataRecursiveForQuery:query withCompletion:^{
//do whatever you need to do once the data load is complete
}];
}
- (void)loadDataRecursiveForQuery:(MSQuery *)query withCompletion:(CompletionBlock)completion
{
query.includeTotalCount = YES;
query.fetchLimit = 1000; //note: you can adjust this to whatever amount is optimum
query.fetchOffset = self.results.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.results addObjectsFromArray:items];
if (totalCount > [results count]) {
[self loadDataRecursiveForQuery:query withCompletion:completion];
} else {
completion();
}
}
}];
}
Note: I haven't tested this code, but it should work more or less.
I'm using the following query to get some data from a Parse.com class.
What I would like to do is extract the Rating object from the NSArray rateObjects. How would I go about doing this?
thanks for any help
PFQuery *rateQuery = [PFQuery queryWithClassName:#"Rating"];
[rateQuery whereKey:#"photo" equalTo:self.photo];
[rateQuery includeKey:#"photo"];
rateQuery.cachePolicy = kPFCachePolicyNetworkElseCache;
rateQuery.limit = 20;
[rateQuery findObjectsInBackgroundWithBlock:^(NSArray *rateObjects, NSError *error)
{
if( !error )
{
NSLog(#"rateObject %#", rateObjects);
}
}
];
Here's the NSLog output:
rateObject (
"<Rating:w9ENTO29mA:(null)> {\n ACL = \"<PFACL: 0x1e0a5380>\";\n Rating = 4;\n fromUser = \"<PFUser:uV2xu0c3ec>\";\n photo = \"<Photo:Rv4qqrHUPr>\";\n toUser = \"<PFUser:uV2xu0c3ec>\";\n user = \"<PFUser:uV2xu0c3ec>\";\n}",
"<Rating:t3pjtehYR0:(null)> {\n ACL = \"<PFACL: 0x1e0f9f90>\";\n Rating = 5;\n fromUser = \"<PFUser:uV2xu0c3ec>\";\n photo = \"<Photo:Rv4qqrHUPr>\";\n toUser = \"<PFUser:uV2xu0c3ec>\";\n user = \"<PFUser:uV2xu0c3ec>\";\n}"
)
Your NSArray will contain PFObjects, which you can treat in a similar way to a dictionary. In the query you ran above you received two rating objects back. If that's not what you wanted (you only wanted a single object) you may want to revisit how you're querying your data.
Assuming your Rating class in Parse contains a key called Rating you would access it like this:
[rateObject objectForKey:#"Rating"]
You can also use the new modern literal syntax if you like - rateObject[#"rating"]
You'll need to iterate through your array to view all the rating objects that have been returned, so you'll probably end up with something like this:
for (id item in rateObjects) {
int ratingVal = [[item objectForKey:#"Rating"] intValue];
NSLog(#"Rating: %d", ratingVal);
}
You may find Parse's iOS documentation helpful - and if you're not sure what the code above actually does, you may want to review arrays and how they work in Objective-C.
Try to use this:
[rateQuery findObjectsInBackgroundWithBlock:^(NSArray *rateObjects, NSError *error)
{
if( !error )
{
NSMutableArray *data = [[NSMutableArray alloc]init];
[data addObjectsFromArray:rateObjects];
NSArray *rating_data = [data valueForKey:#"Rating"];
NSLog(#"%#",[rating_data objectAtIndex:0]);
}
}
];
I hope this will help you.
I'm doing my head in trying to figure out an issue I'm having with core-data. I understand what the error means, and I've had it before (and fixed it) but I can't figure out why I'm getting it now.
My app has the following structure :
MPModel -> MPPlace and MPModel -> MPProject
Where MPModel is a subclass of NSManagedObject and MPPlace and MPProject are subclasses of MPModel.
The data model has a relationship between MPPlace and MPProject where MPPlace has-many MPProjects and MPProject belongs-to MPPlace.
When the app loads, it fetches a number of MPPlace objects which works perfectly. When a user selected a place and then selects the projects option, the app attempts to retrieve the list of projects. This is where the app fails however, with the following error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'Failed to process pending changes before save. The context is still dirty after 100
attempts. Typically this recursive dirtying is caused by a bad validation method,
willSave, or notification handler.'
Each MPModel contains a number of custom methods, including saveAllLocally which simply saves an array of objects to the persistence store if they don't already exist, and loadNestedResources which fetches new objects from the server that are related to the current object. I am finding that the failure is occurring with the loadNestedResources method but I can't figure out why.
The loadNestedResources method looks like this :
- (void) loadNestedResourcesOfType:(Class)type atURL:(NSString *)resURL withBlock:(MPFindObjectsBlock)block
{
if (![type isSubclassOfClass:[MPModel class]]) {
block(nil, nil);
} else {
NSString *url = [NSString stringWithFormat:#"%#%#/%#%#", kMP_BASE_API_URL, [self.class baseURL], self.objID, [type baseURL]];
MPRequest *request = [[MPRequest alloc] initWithURL:url];
// Attempt to see if we already have this relation
NSString *relation = [[MPUtils stripClassName:type].lowercaseString stringByAppendingString:#"s"];
NSSet *relatedObjects = [self valueForKey:relation];
if (relatedObjects && relatedObjects.count > 0) {
// We have some objects so lets exclude these from our request
NSMutableArray *uuids = [NSMutableArray arrayWithCapacity:0];
for (MPModel *obj in relatedObjects) {
[uuids addObject:obj.uuid];
}
[request setParam:[uuids componentsJoinedByString:#";"] forKey:#"exclude_uuids"];
}
[MPUser signRequest:request];
[request setRequestMethod:#"POST"];
[request submit:^(MPResponse *resp, NSError *error) {
if (error) {
if (relatedObjects.count > 0) {
block([relatedObjects allObjects], nil);
} else {
block(nil, error);
}
} else {
// Combine all of our objects
NSMutableArray *allObjects = [[type createListWithResponse:resp] mutableCopy];
if (allObjects.count > 0) {
[allObjects addObjectsFromArray:[relatedObjects allObjects]];
// Make sure they're now all saved in the persistence store
NSArray *savedObjects = [MPModel saveAllLocally:allObjects forEntityName:NSStringFromClass(type)];
for (NSObject *obj in savedObjects) {
[obj setValue:self forKey:[MPUtils stripClassName:self.class].lowercaseString];
}
// Set them as our related objects for this relationship
[self setValue:[NSSet setWithArray:savedObjects] forKey:relation];
[MPModel saveAllLocally:savedObjects forEntityName:NSStringFromClass(type)];
block(allObjects, nil);
} else {
block([relatedObjects allObjects], nil);
}
}
}];
}
}
The methods runs perfectly right up until the second call to saveAllLocally which is where I get the error. The MPModel class also uses the willSave method, which has the following :
- (void) willSave
{
NSDate *now = [NSDate date];
if (!self.updated_at) {
self.updated_at = now;
}
if (!self.created_at) {
self.created_at = now;
}
if (!self.uuid) {
self.uuid = [self createUUID];
}
if (!self.last_sync) {
self.last_sync = now;
}
if ([self isUpdated] && self.changedValues.count > 0) {
if (!self.attribute_updated_at) {
NSDictionary *attributes = [self.entity attributesByName];
NSMutableDictionary *dates = [NSMutableDictionary dictionaryWithCapacity:0];
for (NSString *attr in attributes.allKeys) {
[dates setObject:now forKey:attr];
}
[self setAttributeUpdatedDates:dates];
}
if (_updatedAtSet) {
_updatedAtSet = NO;
} else {
if ([self.changedValues.allKeys containsObject:#"last_sync"]) {
self.updated_at = [self.changedValues objectForKey:#"last_sync"];
} else {
self.updated_at = [NSDate date];
}
_updatedAtSet = YES;
NSDictionary *changed = [self changedValues];
NSMutableDictionary *dates = [[self attributeUpdatedDates] mutableCopy];
for (NSString *key in changed.allKeys) {
[dates setObject:now forKey:key];
}
[self setAttributeUpdatedDates:dates];
}
}
}
From what I can gather, this should be fine as it shouldn't be setting any more values if the _updatedAtSet variable is set to true, however it is still breaking and I cannot figure out why!
Please can someone help me
Thanks
Have solved it.
I just moved the _updatedAtSet = NO; into the didSave method rather than where it is and it's working fine now. Thanks anyway