How to load multiple unrelated objects with RestKit? - objective-c

I need to load mulitple objects that don't have relationships but located on the same remote API using RestKit into CoreData (in other words download remote objects to use them locally in iOS)
So I've setup my Client and objectManager:
client = [RKClient clientWithBaseURL:[NSURL URLWithString:baseUrl]];
[client setUsername:#"someUsername"];
[client setPassword:#"somePassword"];
objectManager = [RKObjectManager managerWithBaseURLString:baseUrl];
[objectManager setClient:client];
Then I setup some mappings for one Object#1:
RKManagedObjectMapping* companyMapping = [RKManagedObjectMapping mappingForEntityWithName:#"Company" inManagedObjectStore:objectManager.objectStore];
companyMapping.primaryKeyAttribute = #"backendID";
[companyMapping mapKeyPath:#"id" toAttribute:#"backendID"];
[companyMapping mapKeyPath:#"company_name" toAttribute:#"companyName"];
Then I setup some mappings for Object #2:
RKManagedObjectMapping* eventMapping = [RKManagedObjectMapping mappingForEntityWithName:#"Event" inManagedObjectStore:objectManager.objectStore];
eventMapping.primaryKeyAttribute = #"backendID";
[eventMapping mapKeyPath:#"id" toAttribute:#"backendID"];
[eventMapping mapKeyPath:#"description" toAttribute:#"eventDescription"];
After that I set objectMapping to mappingProvider:
[objectManager.mappingProvider setObjectMapping:companyMapping forResourcePathPattern:remoteObjectPath];
[objectManager.mappingProvider setObjectMapping:eventMapping forResourcePathPattern:remoteObjectPath];
And lastly I call "loadObjects..." for both of these objects one after another:
[objectManager loadObjectsAtResourcePath:remoteCompaniesObjectPath delegate:self];
[objectManager loadObjectsAtResourcePath:remoteEventsObjectPath delegate:self];
What happens after this that data from both Companies and Events is somehow loaded into the Events table, so table data is completely unusable.
If I run only loadObjectsAtResourcePath:remoteCompaniesObjectPath then everything works as expected remote Company object downloads into it's own table in the CoreData.
While it does make sense why it's happening (data For Companies starting to load and gets new mapping, so it loads to the wrong table) the QUESTION is:
How to call loadObjectsAtResourcePath:remoteObjectPath methods to load multiple unrelated objects so they would properly loaded into their respective tables in the CoreData???

Well I think there is nothing There for restkit to distinguish between two object types company and events.
There are two ways of going about this. Both are straightforward, upto you what to choose:
You provide a rootkeypath to restkit for both company and events. And then you modify your api to include a rootkeypath in the json. Something like below..
["company":{
"Backendid":1
"company name":"abc"
}
{"events":{
{Backendid":1
"description":"party"
}]
In this case restkit will know which is an event and what is a company and will automatically map to the right table.. Checkout rkgithub example for more tips.
You set your online api to have 2 different urls for both. So then the different URL will be make sure restkit doesn't get tables mixed up.
Also something to ask here, does your events column have a company name column as well? Because I would have thought restkit will give you an error as from your mapping there doesn't seem to be a column named company name in an events table. Are you trying to do some sort of to-many relationship here? Where company has-many events type of thing.

Your mapping provider needs a tweak -- set each mapping with its respective URL.
Change mapping from:
[objectManager.mappingProvider setObjectMapping:companyMapping forResourcePathPattern:remoteObjectPath];
[objectManager.mappingProvider setObjectMapping:eventMapping forResourcePathPattern:remoteObjectPath];
To:
[objectManager.mappingProvider setObjectMapping:companyMapping forResourcePathPattern: remoteCompaniesObjectPath];
[objectManager.mappingProvider setObjectMapping:eventMapping forResourcePathPattern:remoteEventsObjectPath];

Related

Retrieve Records from Quickblox API

I am using Quickblox SDK in my Application and designed Custom Objects in the Quickblox Admin panel.Here i am having some complex relationship between Custom Objects and not able to figure out how to retrieve the records using the Quickblox API.
Problem:
There are two tables 1.User 2.Group, i am successfully fetching all the groups created by a specific using by using parent_id key as mention in Quickblox API for maintaining relationships between tables.
I want to retrieve all the groups created by the user and also the groups in which the user is added as a member which are created by other Users.How can i solve this? In my group table i am having a column that is an array of userIds to represent all the group members.
Can anyone tell me how to resolve this.How i can query and retrieve.I am really poor at DB knowledge and moreover Quickblox is a having different approach in the DB Design.
NSMutableDictionary *getRequest = [NSMutableDictionary dictionary];
[getRequest setObject:user_id forKey:#“user_ids_column_name[in]"];
[getRequest setObject:user_id forKey:#"ParentID"];
QBRequestErrorBlock errorBlock = ^(QBResponse *response) {
NSLog(#"error: %#", response.error);
};
[QBRequest objectsWithClassName:#“Group” extendedRequest:getRequest successBlock:^(QBResponse *response, NSArray *objects, QBResponsePage *page){
NSLog(#"Success");
} errorBlock:errorBlock];

Deleting all table records from core data database using relationship

I'm using core data for my application i have 4-5 tables one of which is userProfile table. i have implemented logout in the app. if user logs out of the app im deleting user profile and will be inserting new if logged in with other user account. i want to delete all the records from database on userprofile delete. im using relationship for this but it is not deleting other records from the db even if user profile record has been deleted.
one thing i would like to mention is all the data is coming from service. and i am using cascade delete rule for relationship created between userprofile table and other tables.
You have two different ways to achieve this.
The first is to delete the store and create it again. This means access the store in the file system and delete the sql file, for example. For example, you can find how to achieve it in the following discussion: Delete/Reset all entries in Core Data?.
The second is solution is to create a cascade relationship in UserProfile entity that will link the other ones. In the latter you must set up an inverse relationship (nullify would be the correct approach). For further info see my answer at Setting up a parent-child relationship in Core Data.
Said this, and based on my experience, I would discourage to save user info (e.g. passwords) in Core Data. Instead, adopt the Keychain for this. There are libraries that wrap the Keychain access in an easy manner (e.g. SSKeychain).
I tried using relationship using cascade rule but didn't worked for me so I used Delete/Reset all entries in Core Data method. Used following code for this.
NSError * error;
NSURL * storeURL = [[AppDelegate.managedObjectContext persistentStoreCoordinator] URLForPersistentStore:[[[AppDelegate.managedObjectContext persistentStoreCoordinator] persistentStores] lastObject]];
[AppDelegate.managedObjectContext lock];
[AppDelegate.managedObjectContext reset];//to drop pending changes
//delete the store from the current managedObjectContext
if ([[AppDelegate.managedObjectContext persistentStoreCoordinator] removePersistentStore: [[[AppDelegate.managedObjectContext persistentStoreCoordinator] persistentStores] lastObject] error:&error])
{
// remove the file containing the data
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//recreate the store like in the appDelegate method
[[[AppDelegate managedObjectContext] persistentStoreCoordinator] addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options: #{NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES} error:&error];
}
[AppDelegate.managedObjectContext unlock];
//that's it !
This worked for me.

Dealing with duplicate contacts due to linked cards in iOS' Address Book API

Some beta-users of my upcoming app are reporting that the list of contacts contain a lot of duplicate records. I'm using the result from ABAddressBookCopyArrayOfAllPeople as the data source for my customized table view of contacts, and it baffles me that the results are different from the iPhone's 'Contacts' app.
When looking more closely at the Contacts app, it seems that the duplicates originate from entries with "Linked Cards". The screenshots below have been obfuscated a bit, but as you see in my app on the far right, "Celine" shows up twice, while in the Contacts app on the left there's only one "Celine". If you click the row of that single contact, you get a "Unified Info" card with two "Linked Cards" (as shown in the center, I didn't use Celine's contact details because they didn't fit on one screenshot):
The issues around "Linked Cards" have quite a few topics on Apple's forums for end users, but apart from the fact that many point to a 404 support page, I can't realistically go around fixing all of my app's users' address books. I would much rather like to deal with it elegantly and without bothering the user. To make matters worse, it seems I'm not the only one with this issue, since WhatsApp is showing the same list containing duplicate contacts.
Just to be clear about the origins of the duplicate contacts, I'm not storing, caching or otherwise trying to be smart about the array ABAddressBookCopyArrayOfAllPeople returns. So the duplicate records come directly from the API call.
Does anyone know how to deal with or detect these linked cards, preventing duplicate records from showing up? Apple's Contacts app does it, how can the rest of us do so too?
UPDATE: I wrote a library and put it on Cocoapods to solve the issue at hand. See my answer below
One method would be to only retrieve the contacts from the default address book source:
ABAddressBookRef addressBook = ABAddressBookCreate();
NSArray *people = (__bridge NSArray *)ABAddressBookCopyArrayOfAllPeopleInSource(addressBook, ABAddressBookCopyDefaultSource(addressBook));
But that is lame, right? It targets the on-device address book, but not extra contacts that might be in Exchange or other fancy syncing address books.
So here's the solution you're looking for:
Iterate through the ABRecord references
Grab each respective "linked references" (using ABPersonCopyArrayOfAllLinkedPeople)
Bundle them in an NSSet (so that the grouping can be uniquely identified)
Add that NSSet to another NSSet
Profit?
You now have an NSSet containing NSSets of linked ABRecord objects. The overarching NSSet will have the same count as the number of contacts in your "Contacts" app.
Example code:
NSMutableSet *unifiedRecordsSet = [NSMutableSet set];
ABAddressBookRef addressBook = ABAddressBookCreate();
CFArrayRef records = ABAddressBookCopyArrayOfAllPeople(addressBook);
for (CFIndex i = 0; i < CFArrayGetCount(records); i++)
{
NSMutableSet *contactSet = [NSMutableSet set];
ABRecordRef record = CFArrayGetValueAtIndex(records, i);
[contactSet addObject:(__bridge id)record];
NSArray *linkedRecordsArray = (__bridge NSArray *)ABPersonCopyArrayOfAllLinkedPeople(record);
[contactSet addObjectsFromArray:linkedRecordsArray];
// Your own custom "unified record" class (or just an NSSet!)
DAUnifiedRecord *unifiedRecord = [[DAUnifiedRecord alloc] initWithRecords:contactSet];
[unifiedRecordsSet addObject:unifiedRecord];
CFRelease(record);
}
CFRelease(records);
CFRelease(addressBook);
_unifiedRecords = [unifiedRecordsSet allObjects];
I've been using ABPersonCopyArrayOfAllLinkedPeople() in my app for some time now. Unfortunately, I've just discovered that it doesn't always do the right thing. For example, if you have two contacts that have the same name but one has the "isPerson" flag set and the other does not, the above function won't consider them "linked". Why is this an issue? Because Gmail(exchange) sources don't support this boolean flag. If you try to save it as false, it will fail, and the contact you saved in it will come back on the next run of your app as unlinked from the contact you saved in iCload (CardDAV).
Similar situation with social services: Gmail doesn't support them and the function above will see two contacts with the same names as different if one has a facebook account and one does not.
I'm switching over to my own name-and-source-recordID-only algorithm for determining whether two contact records should be displayed as a single contact. More work but there's a silver lining: ABPersonCopyArrayOfAllLinkedPeople() is butt-slow.
The approach that #Daniel Amitay provided contained nuggets of great value, but unfortunately the code is not ready for use. Having a good search on the contacts is crucial to my and many apps, so I spent quite a bit of time getting this right, while on the side also addressing the issue of iOS 5 and 6 compatible address book access (handling user access via blocks). It solves both the many linked cards due to incorrectly synched sources and the cards from the newly added Facebook integration.
The library I wrote uses an in-memory (optionally on-disk) Core Data store to cache the address book record ID's, providing an easy background-threaded search algorithm that returns unified address book cards.
The source is available on a github repository of mine, which is a CocoaPods pod:
pod 'EEEUnifiedAddressBook'
With the new iOS 9 Contacts Framework you can finally have your unified contacts.
I show you two examples:
1) Using fast enumeration
//Initializing the contact store:
CNContactStore* contactStore = [CNContactStore new];
if (!contactStore) {
NSLog(#"Contact store is nil. Maybe you don't have the permission?");
return;
}
//Which contact keys (properties) do you want? I want them all!
NSArray* contactKeys = #[
CNContactNamePrefixKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactFamilyNameKey, CNContactPreviousFamilyNameKey, CNContactNameSuffixKey, CNContactNicknameKey, CNContactPhoneticGivenNameKey, CNContactPhoneticMiddleNameKey, CNContactPhoneticFamilyNameKey, CNContactOrganizationNameKey, CNContactDepartmentNameKey, CNContactJobTitleKey, CNContactBirthdayKey, CNContactNonGregorianBirthdayKey, CNContactNoteKey, CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactImageDataAvailableKey, CNContactTypeKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactPostalAddressesKey, CNContactDatesKey, CNContactUrlAddressesKey, CNContactRelationsKey, CNContactSocialProfilesKey, CNContactInstantMessageAddressesKey
];
CNContactFetchRequest* fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:contactKeys];
[fetchRequest setUnifyResults:YES]; //It seems that YES is the default value
NSError* error = nil;
__block NSInteger counter = 0;
And here i loop through all unified contacts using fast enumeration:
BOOL success = [contactStore enumerateContactsWithFetchRequest:fetchRequest
error:&error
usingBlock:^(CNContact* __nonnull contact, BOOL* __nonnull stop) {
NSLog(#"Unified contact: %#", contact);
counter++;
}];
if (success) {
NSLog(#"Successfully fetched %ld contacts", counter);
}
else {
NSLog(#"Error while fetching contacts: %#", error);
}
2) Using unifiedContactsMatchingPredicate API:
// Contacts store initialized ...
NSArray * unifiedContacts = [contactStore unifiedContactsMatchingPredicate:nil keysToFetch:contactKeys error:&error]; // Replace the predicate with your filter.
P.S You maybe also be interested at this new API of CNContact.h:
/*! Returns YES if the receiver was fetched as a unified contact and includes the contact having contactIdentifier in its unification */
- (BOOL)isUnifiedWithContactWithIdentifier:(NSString*)contactIdentifier;
I'm getting all sources ABAddressBookCopyArrayOfAllSources, moving the default one ABAddressBookCopyDefaultSource to the first position, then iterate through them and getting all people from source ABAddressBookCopyArrayOfAllPeopleInSource skipping ones I've seen linked before, then getting linked people on each ABPersonCopyArrayOfAllLinkedPeople.

RestKit: two separate feeds, two different object types. One object manager?

I'm working with RestKit for the first time and am pretty happy, unfortunately the current project has thrown me for a loop.
First, I have two feeds. One for news and one for photos...
News feed...
[
{"id":1234, "title":"Top story"},
{"id":1235, "title":"Next story"},
{"id":1236, "title":"Final story"}
]
Photos feed...
[
{"id":1919, "url":"president.jpg"},
{"id":1920, "url":"celebrity.jpg"},
{"id":1921, "url":"sports.jpg"}
]
...in my Core Data model I have these entities created that match these attributes. I have also setup my mappings for RestKit...
RKManagedObjectMapping* newsMapping = [RKManagedObjectMapping mappingForEntityWithName:#"News"];
newsMapping.primaryKeyAttribute = #"id";
[newsMapping mapKeyPath:#"id" toAttribute:#"id"];
[newsMapping mapKeyPath:#"title" toAttribute:#"article_id"];
[objectManager.mappingProvider setMapping:newsMapping forKeyPath:#"News"];
RKManagedObjectMapping* photoMapping = [RKManagedObjectMapping mappingForEntityWithName:#"Photo"];
photoMapping.primaryKeyAttribute = #"id";
[photoMapping mapKeyPath:#"id" toAttribute:#"id"];
[photoMapping mapKeyPath:#"url" toAttribute:#"url"];
[objectManager.mappingProvider setMapping:photoMapping forKeyPath:#"Photo"];
...in the above code I'm aware that the specified keyPaths aren't present in the feeds, but providing empty keyPaths just seemed to overwrite the mappings and RestKit seemed to ignore the absent keyPAths in the feeds. Next, in each of the view controllers where I'm displaying the objects I run some pretty similar code...
RKObjectManager* objectManager = [RKObjectManager sharedManager];
// objectManager.inferMappingsFromObjectTypes = YES;
[objectManager loadObjectsAtResourcePath:#"json/news" delegate:self block:^(RKObjectLoader* loader) {
loader.objectMapping = [objectManager.mappingProvider objectMappingForClass:[News class]];
}];
...enough background. Together all this code loads zero news and zero photos. If I comment out the photos mappings, all of my news items populate and if I comment out the news mappings then my photos appear in the store.
RestKit can't match together each feed's content with its mapping. The keypaths specified in setMapping:forKeyPath: won't work because the feeds don't have anything specified. I thought maybe objectManager.inferMappingsFromObjectTypes = YES would have RestKit inspect what each feed had and choose the proper mappings, but it didn't. Also, I assumed objectMappingForClass: would automatically pickup the correct mappings, but no. Does anyone have any ideas how to make this work?
UPDATE: Actually, even though RestKit is reporting that no items had been loaded when the app runs the first time, if I stop and run again I now see all 20 news items and 20 photos loaded from cache. I confirmed this by running it once, seeing the message "Caching all 0 News objects to thread local storage" and then immediately opening the .sqlite file and seeing all 20 items as expected.
This comment: https://groups.google.com/d/msg/restkit/b0K0nCha8D0/VadnqDVB1-oJ sent me down the right path. I had a project with an existing Core Data stack and once I removed all related to code to that OTHER set of stores and contexts, etc. Everything worked a charm. What an awesome framework.

Migrating entities and parent entities

I have an entity A which has two attributes. Entity B has A as parent and has an additional 3 attributes. The changes in the new version don't affect entities A and B.
How can I migrate objects of entity B to a new version of my data model, including the attributes from entity A?
I tried using two entity mappings: one for A and one for B, but 'A attributes' aren't migrated. Alternatively I would add A's attributes to the mapping to migrate B, but there I can't selected the right attributes (in Xcode 4).
Edit:
I'm not referring to a regular relationship between two entities, but inheritance:
Edit 2:
Just to be sure, I created a new project to test with. Herein, I added only the two entities as seen above. In my awakeFromNib I do a fetch request and if no results are returned, I add a new entity:
NSManagedObject *newAccount = [[NSManagedObject alloc] initWithEntity:entityDesc insertIntoManagedObjectContext:[self managedObjectContext]];
// Account
[newAccount setValue:#"TheName" forKey:#"name"];
[newAccount setValue:[NSDecimalNumber decimalNumberWithMantissa:5 exponent:2 isNegative:NO] forKey:#"currentBalance"];
// BankDebitAccount
[newAccount setValue:#"TheAccountNumber" forKey:#"accountNumber"];
[newAccount setValue:#"TheBankName" forKey:#"bankName"];
[newAccount setValue:[NSDecimalNumber decimalNumberWithMantissa:6 exponent:1 isNegative:YES] forKey:#"openingBalance"];
In my second version of the data model, I added a new entity and I enabled automatic migration via
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:dict error:&error]) {
The migration does indeed happen, and the three properties from BankDebitAccount successfully are migrated. The currentBalance property from Account is reset to 0 and the name property isn't visible in the XML file anymore (and thus, is equal to nil).
Edit 3:
I just tried opening this newly made test project in Xcode 3(.2.4). When I open the mapping model in there and select my child entity's mapping, I can actually add an attribute mapping for the parent's attributes:
So, I guess that would make this a bug in Xcode 4.
I am not sure what went wrong.
I just created this data model version 1, I hope this gets as close to your case as possible:
Then I created this version 2, no changes to Parent and Child only one new entity:
The I created a mapping model and this is what it automatically suggested:
Let's have a look at the differences:
Only one change: the new entity Neighbour.
Can you post some pics of your situation?
Note This is XCode3
Turns out this was a bug in the version of Xcode I was using at the time and was resolved in Xcode 4.2.