I'm making watch app for iOS application. I get data from parent application in watch main InterfaceController and pass it to other InterfaceController for creating table. Here code of configuring table:
- (void)configureTableWithData:(NSArray*)dataObjects {
[self.table setNumberOfRows:[dataObjects count] withRowType:#"rowType"];
for (NSInteger i = 0; i < self.table.numberOfRows; i++) {
RowType* row = [self.table rowControllerAtIndex:i];
NSObject* object = [dataObjects objectAtIndex:i];
[row.titleName setText:[object valueForKey:#"CharCode"]];
[row.bottomValue setText:[object valueForKey:#"Value"]];
}
}
When I select row, I want to transfer data back to first page. It's need for changing some label on first page. I'm doing it with transfer data to parent app and return it back to main InterfaceController
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex {
//Here must be dictionary, where I put row
[WKInterfaceController openParentApplication:data reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"data transfer");
}];
[self dismissController];
}
How can I get data of row? (row.titleName, row.value) May be it's stupid question, I am still just a beginner, but I can get it. I tried to print on console row.titleName and row.bottomValue and of course I've get nothing. (sorry for my english, not my mother tongue) What did I miss?
I have same issue with watch kit in tableview. For now we only set the text in cell label not get back so you have to do like this
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex {
//Here must be dictionary, where I put row
NSDictionary* object = [dataObjects objectAtIndex:rowIndex];
NSLog("used object as you want");
[WKInterfaceController openParentApplication:object reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"data transfer");
}];
[self dismissController];
}
Related
I am using Azure Mobile Service as a backend for an iOS application. I have set up everything to work with offline sync which allows me to view, add, or modify data even when there is no network connection. I am running into a problem when I add a new object into a table. The add works well locally but when I synchronize data it creates a duplicate item on the local database with a slightly different objectId. The created item is not duplicated on the server side.
Here's how I am setup. By the way, thanks to #TheBasicMind for posting this model.
Here's a link to his explanation of the model: enter link description here
Here's what I do to setup the sync context and sync table:
// Initialize the Mobile Service client with your URL and key
MSClient *client = self.hpc.client;
NSManagedObjectContext *context = self.hpc.syncContext;
MSCoreDataStore *store = [[MSCoreDataStore alloc] initWithManagedObjectContext:context];
client.syncContext = [[MSSyncContext alloc] initWithDelegate:syncDelegate dataSource:store callback:nil];
// Add a Mobile Service filter to enable the busy indicator
self.client = [client clientWithFilter:self];
// Create an MSSyncTable instance to allow us to work with the Athlete table
self.syncAthleteTable = [self.client syncTableWithName:#"Athlete"];
Here's how I add a record for the moment:
NSDictionary *newItem = #{#"firstname": firstname, #"lastname": lastname, #"laterality" : laterality};
[self.athletesService addItem:newItem completion:^{
NSLog(#"New athlete added");
}];
-(void)addItem:(NSDictionary *)item completion:(CompletionBlock)completion
{
// Insert the item into the Athlete table
[self.syncAthleteTable insert:item completion:^(NSDictionary *result, NSError *error)
{
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
The add works as expected and it is added in a UITableView as I have an NSFetchedResultsController listening on my Main Context.
Here's where the problem occurs. When I synchronize data with the server using this function:
-(void)syncData:(CompletionBlock)completion
{
// push all changes in the sync context, then pull new data
[self.client.syncContext pushWithCompletion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self pullData:completion];
}];
}
-(void)pullData:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
// Pulls data from the remote server into the local table.
// We're pulling all items and filtering in the view
// query ID is used for incremental sync
[self.syncAthleteTable pullWithQuery:query queryId:#"allAthletes" completion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self refreshDataOnSuccess:completion];
}];
}
- (void) refreshDataOnSuccess:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
[query readWithCompletion:^(MSQueryResult *results, NSError *error) {
[self logErrorIfNotNil:error];
NSLog(#"Data that pulled from local store: ");
for ( NSDictionary *dict in results.items ) {
NSLog(#"%# %#", [dict objectForKey:#"firstname"], [dict objectForKey:#"lastname"] );
}
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
After the synchronization the NSFetchedResultsChangeInsert is called a second time for the same record with a slightly different objectID. Here's an example of the first and second objectIDs:
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE9
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE11
I am stuck here.
Any help is highly appreciated. Thank you!
In the past, when I've seen this happen, its because the "id" field the client is sending was being changed or ignored by the server logic.
Locally the store finds the object in core data using that field, so a change to it could result in the client SDK thinking it needs to insert a new object and not update an existing one.
One easy way to confirm this, is by using the tableOperation:complete: method on the data delegate and comparing the "id" column between the item originally and that being returned by operation execute.
I'm looking for help with trying to update a progress indicator using the MRProgress framework. I was able to set up the progress indicator, but I have no idea how to calculate and update its progress. I'm using CloudKit and trying to show the progress when saving a CKRecord. Could someone provide me some direction? Thanks in advance!
self.hud = [MRProgressOverlayView showOverlayAddedTo:self.myCollectionView animated:YES];
self.hud.mode = MRProgressOverlayViewModeDeterminateCircular;
self.hud.titleLabelText = #"Uploading...";
// prepare the CKRecord and save it
[self.ckManager saveRecord:[self.ckManager createCKRecordForImage:self.imageDataAddedFromCamera] withCompletionHandler:^(CKRecord *record, NSError *error) {
if (!error && record) {
NSLog(#"INFO: Record saved successfully for recordID: %#", record.recordID.recordName);
// need to get the recordID of the just saved record before adding the CID to the CIDArray
self.imageDataAddedFromCamera.recordID = record.recordID.recordName;
[self.imageLoadManager addCIDForNewUserImage:self.imageDataAddedFromCamera]; // update the model with the new image
// update number of items since array set has increased from new photo taken
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
//[MRProgressOverlayView dismissAllOverlaysForView:self.view animated:YES];
[self.hud dismiss:YES];
[self.hud removeFromSuperview];
} else {
NSLog(#"ERROR: Error saving record to cloud...%#", error.localizedDescription);
[self alertWithTitle:#"Yikes!" andMessage:#"We encountered an issue trying to upload your photo to the cloud."];
}
}];
Update: Converted cloudkit methods from convenience API to CKOperations in my CKManager class. I can see the progress updating through logging, but I don't see how to get it back to the viewcontroller. If I were to add it to the completion handler, wouldn't that only send it back once everything is completed? Here's my updated code...
CKManager.h
- (void)saveRecord:(NSArray *)records withCompletionHandler:(void (^)(NSArray *records))completionHandler;
CKManager.m
- (void)saveRecord:(NSArray *)records withCompletionHandler:(void (^)(NSArray *))completionHandler {
NSLog(#"INFO: Entered saveRecord...");
CKModifyRecordsOperation *saveOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:nil];
saveOperation.perRecordProgressBlock = ^(CKRecord *record, double progress) {
if (progress <= 1) {
NSLog(#"Save progress is: %f", progress);
}
};
saveOperation.completionBlock = ^ {
NSLog(#"Save operation completed!");
completionHandler(records);
};
[self.publicDatabase addOperation:saveOperation];
}
if you want to show the progress of an operation, then you have to use the CKModifyRecordsOperation and use perRecordProgressBlock method.
I load a "Rooms" UICollectionView with specific images that the logged in user has selected in a previous view controller, by populating the "imageFilesArray" and telling the UICollectionViewCells to use its data:
-(void) retrieveSelectedImagesForRooms
{
//parse query where we search the selectedImage array column and return any entry where the array contains the logged in user objectid
PFQuery *getRooms = [PFQuery queryWithClassName:#"collectionViewData"];
[getRooms whereKey:#"selectedImage" equalTo:[PFUser currentUser].objectId];
[getRooms findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
imageFilesArray = [[NSArray alloc] initWithArray:objects];
[roomsCollection reloadData];
}
}];
}
The next page has to show the specific lights that user has selected for that previously selected room image. So I add the row's objectid I've just selected to a new column on Parse, called "clickedRoom", when the room is selected:
-(void)selectedRoom:(PFObject*)object
{
[object addUniqueObject:object.objectId forKey:#"clickedRoom"]; //put object id into clickedRoom column on Parse to save what room you specifically chose so that the light images correspond to only that room
[object saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error)
{
if (!error){}
else{}
}];
}
- (void)collectionView:(UICollectionView*)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self selectedRoom:[imageFilesArray objectAtIndex:indexPath.row]];
[self performSegueWithIdentifier:#"myLights" sender:self];
}
Now, in the "Lights" page I need to show ONLY the light images that have the selected room's objectid in that "clickedRoom" column. I believe it's the same principle as how I retrieve the room images, but I can't figure out what I should be querying for, something like:
-(void) retrieveCorrespondingImagesForLights
{
PFQuery *getLights = [PFQuery queryWithClassName:#"collectionViewData"];
[getLights whereKey:#"clickedRoom" equalTo:**MY-PREVIOUSLY-SELECTED-ROW**.objectid];
[getLights findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
imageFilesArray = [[NSArray alloc] initWithArray:objects];
[myLightsCollection reloadData];
}
}];
}
Any suggestions please?!
The retrieveCorrespondingImagesForLights is in a different view controller than your roomsCollection, correct? If so, then you will need to pass the object id of the selected room to the new view controller that is segued to at [self performSegueWithIdentifier:#"myLights" sender:self];
Take a look here Pass Index Number between UITableView List segue
In your case, you should add a property to your destination view controller (I'll call it LightsViewController) to capture the object, or objectId if that's all you need for the query. I would suggest something like this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"myLights"]) {
// note that "sender" will be the cell that was selected
UICollectionViewCell *cell = (UICollectionViewCell*)sender;
NSIndexPath *indexPath = [roomsCollection indexPathForCell:cell];
LightsViewController *vc = (LightsViewController*)[segue destinationViewController];
vc.selectedObject = indexPath.row;
}
}
Then, in retrieveCorrespondingImagesForLights:
PFQuery *getLights = [PFQuery queryWithClassName:#"collectionViewData"];
[getLights whereKey:#"clickedRoom" equalTo:self.selectedObject.objectid];
EDIT*
Without understanding your exact implementation details, it seems like you are trying to use Parse to pass data between your view controllers when you'd be better suited to do it natively in your app. Maybe I'm misunderstanding your issue.
I've finished implementing a swipe feature similar to tinder but running into problems when saving objects.
I have 2 columns in the currentUser's row in my DB. One column hold an array of acceptedUsers (users have been liked) and the other is a rejectedUsers column that holds an array of rejected users (users that have been left swiped).
This is how my DB is updated upon swipe:
-(void)cardSwipedLeft:(UIView *)card;
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
[currentUserImagesRow addUniqueObject:swipedUserId forKey:#"rejectedUsers"];
[currentUserImagesRow saveInBackground];
This works fine when I left about 2+ seconds between swipes. However fast swiping causes some saves to fail.
Is there a better way to do this without spoiling the users experience of the app?
I've saved multiple rows to my database before using for loops and this has always worked for me. I thought parse.com would be able to handle the speed of the saving.
I'm using both swift and objective-c for this project.
Thanks for your time
Its a fun problem. I think the way to go is to decouple the swiping and the saving a little bit more. Start with a collection of what needs saving...
#property(nonatomic, strong) NSMutableArray *toSave;
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self.toSave addObject: currentUserImagesRow];
[self doSaves];
- (void)doSaves {
// we get called because of user interaction, and we call ourselves
// recursively when finished. keep state so these requests don't pile up
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
[PFObject saveAllInBackground:self.toSave block:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
// remove just the elements that were saved, remaining aware that
// new ones might have arrived while the last save was happening
NSMutableArray *removes = [#[] mutableCopy];
for (PFObject *object in self.toSave) {
if (!object.isDirty) [removes addObject:object];
}
[self.toSave removeObjectsInArray:removes];
[self doSaves];
}];
}
}
Now, instead of processing single saves, we can handle small batches. A user swipe causes a single save, and we block additional requests until the current one is complete. During the current request, we let more saves queue up as the user continues to interact. We call ourselves recursively after a save in case one or more records were queued. If none were, the recursive call ends immediately.
EDIT - Saving just one object is easier, just do the same blocking trick and recursive call at the end, but no need to track or save batches...
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self doSaves];
- (void)doSaves {
if (self.busySaving) return;
if (currentUserImagesRow.isDirty) {
self.busySaving = YES;
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
[self doSaves];
}];
}
}
Danh led me to my answer. There was an issue on my side which was causing the problem mentioned in the comments above. The same ID was being used after each swipe because I didn't remove it from the array that held my user objects. Below is how I solved the issue and used the Danh's answer to find my solution.
-(void)cardSwipedRight:(UIView *)card
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
// I save the swiped users id and the key for the column it will
// be saved in e.g. if liked then "acceptedUsers" if notLiked then
// "rejectedUsers" so that the doSave method saves to correct column
NSArray *array = #[swipedUserId, #"acceptedUsers"];
[self.toSave addObject: array];
//remove user from array since he/she is now saved
[userBeingSwipedArray removeObjectIdenticalTo:[userBeingSwipedArray objectAtIndex:0]];
[self doSaves];
and then:
- (void)doSaves {
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
NSArray *arrayWithSwipedUsersIdAndKeyForColumn = [self.toSave objectAtIndex:0];
[currentUserImagesRow addUniqueObject:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:0] forKey:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:1]];
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
//remove object that was just saved seeing as it is no longer needed
[self.toSave removeObjectIdenticalTo:arrayWithSwipedUsersIdAndKeyForColumn];
[self doSaves];
}];
}
}
Saving now works 100% of the time providing there is an internet connection. I can swipe as fast as I wish and objects always get saved.
I have two issues here.
1) The data in my UITableView does not load when I first open up its ViewController. The proper data does end up appearing, but but only after I add more data to it on another ViewController, and then come back to the TableViewController. Even when doing that, the UITableView is always one item behind, meaning that when I first load the app I will see nothing in my TableView, then if I add an item, for example called "hat" in a second ViewController, I will come back to the TableViewController and see all the items I'd added previously, but I will have to add another item, for example called "chair" to my table in order to see "hat" in my table.
2) While I am able to have users successfully add items to my Parse database and then view the items they've added in the UITableView (albeit with the roadblock addressed in issue 1 above), I am unable to successfully populate the UIImage in each cell in the TableView with the photo file of each cell's corresponding item. The photos are being successfully saved to Parse, so it is definitely a problem querying for them properly, or keying into the exact place they are stored, or simply configuring my subclassed TableViewCell to display them properly.
ItemsTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationItem setHidesBackButton:YES];
}
-(void)viewWillAppear:(BOOL)animated
{
PFQuery *query = [PFQuery queryWithClassName:#"giveItem"];
[query whereKey:#"giver" equalTo:[PFUser currentUser]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.myGiveItems = [[NSMutableArray alloc]init];
for (PFObject *object in objects) {
PFGiveItem *newGiveItem = [[PFGiveItem alloc]init];
newGiveItem.giveItemName = object[#"giveItemTitle"];
// return photo files for each of the objecs
PFFile *giveItemImageFile = object[#"imageFile"];
[giveItemImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *giveItemImageForCell = [UIImage imageWithData:imageData];
newGiveItem.giveItemImage = giveItemImageForCell;
};
}];
[self.myGiveItems addObject:newGiveItem];
}
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.myGiveItems.count;
}
-(void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section
{
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
}
- (JFGiveItemCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
JFGiveItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil){
cell = [[JFGiveItemCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
}
PFGiveItem *giveItem = self.myGiveItems[indexPath.row];
cell.giveItemLabel.text = giveItem.giveItemName;
cell.giveItemImageView.image = giveItem.giveItemImage;
return cell;
}
My Parse backend is structured as follows
I will be very receptive to any guidance on how to more properly structure my database to be less redundant and more powerful.
Class 1 "Item" with rows:
"itemTitle," a user-input string from the AddItem ViewController
"owner," the user who adds the item (and photo)
"itemPhoto," a pointer to an object of Class 2, "ItemPhoto"
Class 1 "Item" with rows:
"imageOwner," the user who adds the item (and photo)
"imageName," a string the same as the itemTitle from Class 1
"imageFile," a File uploaded by the user
All of these items are saved successfully, but for your better understanding, here is the code I use to do this.
AddItemViewController
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSString *nameForGiveItem = self.giveItemTitleTextField.text;
NSData *giveItemImageData = UIImagePNGRepresentation(self.giveItemImage);
PFFile *giveItemImageFile = [PFFile fileWithName:nameForGiveItem data:giveItemImageData];
PFObject *giveItemPhoto = [PFObject objectWithClassName:#"giveItemPhoto"];
giveItemPhoto[#"imageOwner"] = [PFUser currentUser];
giveItemPhoto[#"imageName"] = nameForGiveItem;
giveItemPhoto[#"imageFile"] = giveItemImageFile;
[giveItemPhoto saveInBackground];
PFObject *giveItem = [PFObject objectWithClassName:#"giveItem"];
giveItem[#"giveItemTitle"] = self.giveItemTitleTextField.text;
giveItem[#"giver"] = [PFUser currentUser];
[giveItem setObject:giveItemPhoto forKey:#"giveItemPhoto"];
[giveItem saveInBackground];
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
return YES;
}
For 1)
In viewWillAppear you call [self.tableView reloadData]; outside of the block. That means you actually call it before the block has finished executing - meaning before the data was loaded.
Move it to after the for block but make sure it is executed on the main thread - otherwise it will not influence the appearance of the ui - meangin the table will not be acualized either.
For 2)
The solution for 1) should fix 2) as well. You load your images asynchronously, which is perfect. But when that is finished you do not reload the table. When you manage to get it reloaded once all the data has been loaded then the cell images will be refreshed as well.
3)
You may want to add any type of view/spinner/progress indicator or just a regular lable that indicates to the user that some data is still loading ...