I trying to build app like native Apple Notes. And I have a question\problem.
In apple notes when you open already existing note again and delete all text from it - note deleting and data from core data deleting too. How can I do it? First view of app - List of notes, and second view - note. I can't understand how delete that object, what I need. For example: when my segue from list to note look like that:
-(void) prepareForSegue:(UIStoryBoardSegue *)segue sender:(id)sender{
NoteDetailVC *destViewController = segue.destinationViewController;
if([[segue identifier] isEqualToString:#"ShowNote"]){
NSManagedObject *selectedNote = [self.notes objectAtIndex[[self.tableView indexPathForSelectedRow] row]];
destViewController.selectedNoteInfo = selectedNote;
}
}
And in NoteDetailVC I interact with data some like that:
if (selectedNoteInfo){
// bla bla bla code
}
On create I use setValue: command and else. I understand how dismiss controller without saving data before I set new value. But don't understand how delete already existing object from core data. How check what index I need and etc? Help please! :-)
And sorry for my English again :)
Here is the approach I would take, given I am understanding your question correctly.
When the user chooses to create a new note, create your NSManagedObject to represent that.
Note *newNote = [NSEntityDescription insertNewObjectForEntityForName:#"Note" inManagedObjectContext:self.context];
When they go back to the list or press Done... In prepareForSegue:sender: check the contents of the note.
if (note.contents.length == 0) {
[self.context deleteObject:note];
}
[self.context save:&error]
Related
I'm creating contact application learning how to use ReactiveCocoa with MVVM. I want to implement edit mode for the contact details table, so that when user taps Done button, I save these changes to the model.
Currently I have this structure:
List Of Contacts table has VC and VM, each of its cells has custom view and VM.
When user taps on cell, I open details table.
Contact Details Table has VC and VM, and again each cell has it's own custom view and VM.
When user taps Done button in Contact Details I can observe that in it's VM and construct new Contact object, but how can I notify List Of Contacts about this change?
I'm very confused with such architecture at the moment. So what is a common way to solve this user case in MVVM?
Update:
I have updated the code and solved the problem, however I don't like how it is solved and the question is still up.
When user taps a cell on a contact list, new Details ViewModel is created. I pass a reference to the ContactsList ViewModel and index of selected contact in order to handle contact updates. (Later on if contact is changed, I change contacts property of contactsList view model)
ContactsList ViewController:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:#"pushContactDetails"]) {
STContactDetailsViewController *vc = (STContactDetailsViewController *)segue.destinationViewController;
vc.viewModel = [[STContactDetailsViewModel alloc] initWithContactIndex:self.viewModel.selectedContactIndex services:self.viewModel.services rootVM:self.viewModel];
}
}
Details ViewModel:
-(instancetype)initWithContactIndex:(NSNumber *)contactIndex services:(id<STViewModelServices>)services rootVM:(STContactsListViewModel *)rootVM {
//some initialisation
#weakify(self);
self.saveChanges = [[RACCommand alloc] initWithEnabled:[self executeChangeCheck] signalBlock:^RACSignal *(id input) {
#strongify(self);
return [self executeSaveChanges];
}];
}
-(RACSignal *)executeSaveChanges {
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self);
STContact *newContact = self.contact;
//Update newContact with changed values
NSMutableArray *newContacts = [NSMutableArray arrayWithArray:self.rootVM.contacts];
[newContacts replaceObjectAtIndex:_contactIndex withObject:newContact ];
self.rootVM.contacts = newContacts;
return [RACDisposable new];
}];
}
When contacts property of ContactsList ViewModel is changed, its table reloads and I can see these changes when I get back from details view.
Moreover, In Details ViewController I handle view updates of its table if contact is changed and apply them when exit tableview edit mode.
Even though, everything works at the moment, I feel bad about passing reference of the ContactsList ViewModel to the Details ViewModel. I'd be grateful, if someone could show me the right way to handle this kind of situation.
I am currently working on a project that requires a list of customers to be displayed in a UITableView, the associated cell then segues to a TabView to display a detailed customer record in a tabbed ui.
I have setup the story board with the required TableView and populated fine. The TabViews all setup and I have added a custom class to the main TabView controller which can take the ID (required to interrogate service and return further data) and Customer Name. I have also added a UIViewController for the first tab in which I need to get the ID value.
I can't seem to get hold of the ID or Company Name that is passed. I have tried importing the .h file of the UITabView. I know the UITabView .h file is being populated with the values as in the .m file I am using the Customer Name to update the title of the Navigation Bar. However, whenever I breakpoint on line that gets the ID in the .m file for the individual tab, it always returns nil.
I am using the following code to try and get this value:
companyTabController *headerData = [companyTabController alloc];
_companyName_lbl.text = headerData.companyName;
_companyID_lbl.text = headerData.ID;
I have tried several variations of the above and all to no avail.
You can also use NSUserDefaults to save the data, I think that is the simplest way to save the data throughout the app.
From the code you posted, the headerData is a new instance. So the companyName and the ID will be nil unless you assign some value to them.
Since, you mentioned that you are able update the navigation bar title, try using the same object for fetching the values in this controller as well. (Maybe you can use a singleton object)
If your segueing you have to use the prepareForSegue:sender: method as such:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
companyTabController *companyTC = [segue destinationViewController];
companyTC.companyName_lbl.text = headerData.companyName;
etc
}
if your not segueing you will have to instantiate it as such :
- (void) didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *selectedCell = [self.tableView cellForRowAtIndexPath:indexPath];
companyTabController *companyTC = [self.storyboard instantiateViewControllerWithIdentifier:#"CopmanyTabController"];
companyTC.companyName_lbl.text = selectedCell.textLabel.text or = headerData.companyName;
[self.navigationController pushViewController:companyTC animated:YES];
}
Please be gentle, this is my first ever post.
When I update a subclassed NSManagedObject and perform a save using a fetched results controller, instead of calling NSFetchedResultsChangeUpdate in "didChangeObject" it calls NSFetchedResultsChangeDelete. The save works and it updates the objects, but then immediately deletes it from the UITableView. If I quit and return, the object re-appears in the tableview with the updates so I know it is saving it to the DB.
I am developing code for a simple task app using XCODE 5. I am using an iPhone 4S and not the simulator to test it. It was working fine until I made some mods to another part of the code (I thought unconnected and now it doesn't work.
I have a sort order and when I update an object and it changed in the sort order, there was a nice animation for the moving of the UITableViewCells....now I have to force a fetch again and do [self.tableView reloadData]. This is a hack as I do not get any animations, but it is the only way I can get it to update:
I have a prepare for segue method:
- (void) prepareForSegue: (UIStoryboardSegue *) segue sender: (id) sender
{
// configure the destination view controller:
if ( [segue.destinationViewController isKindOfClass: [ShowEditTaskTableViewController class]])
{
if ([sender isKindOfClass:[UITableViewCell class]] )
{
NSIndexPath *indexPath = [self.taskTableView indexPathForCell:sender];
[self.taskTableView deselectRowAtIndexPath:indexPath animated:YES];
// Pass the selected task to the new view controller.
ShowEditTaskTableViewController *detailViewController = segue.destinationViewController;
Task *info = [self.fetchedResultsController objectAtIndexPath:indexPath];
detailViewController.editedObject = info;
}
else
{
NSIndexPath *indexPath = [self.taskTableView indexPathForCell:sender];
[self.taskTableView deselectRowAtIndexPath:indexPath animated:YES];
ShowEditTaskTableViewController *detailViewController = segue.destinationViewController;
Task *info = (Task *)[NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:self.managedObjectContext];
detailViewController.editedObject = info;
}
}
}
There is nothing special here. "Task" is my NSManagedObject.
I have a rewind (EDIT: changed reverse to rewind after comment) segue and before it is called, I set the variables which will be set stored in the editedObject.:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"addNewTaskSave"]) {
// Note: This is an unwind segue to go back to the previous screen.
self.dueDate = self.taskDatePicker.date;
self.description = self.taskDescriptionTextView.text;
self.priority = self.taskPrioritySegment.selectedSegmentIndex;
}
}
and in the RootViewController it calls:
- (IBAction)addNewTaskSave:(UIStoryboardSegue *)segue
{
ShowEditTaskTableViewController *controller = segue.sourceViewController;
controller.editedObject.shortDesc = controller.description;
controller.editedObject.priority = #(controller.priority);
controller.editedObject.dueDate = controller.dueDate;
controller.editedObject.completed = #0;
NSError *error;
if (![self.managedObjectContext save:&error]) {
...
}
.....
}
It uses the standard didChangeObject delegate methods. This worked fine until I clearly changed something.
Now after the save, it sets the NSFetchedResultsChangeDelete option and deletes the table row.
The fix is to add:
self.fetchedResultsController = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
...
}
[self.taskTableView reloadData];
However, this means that what is actually observed is just a straight update of the Table View with no animation.
Without this code, you can observe an animated delete of the table row. If I quit and restart, my edited object is there.
I have scoured SO which has been my constant companion for the last 4 weeks (the time I have been coding for IOS) and cannot find anything on this behaviour.
Any help would be greatly appreciated.
EDIT:
before the save, controller.editedObject isDeleted = NO, isUpdated = YES, so I can't see why it is setting NSFetchedResultsControllerChangeDelete.
I'm just going to discuss one piece of your code; and this may not have anything to do with the fetch results controller issue described.
Your so-called unwind/reverse segue has me stumped. I don't think prepareForSegue:sender: is relevant for an official unwind segue. So maybe you're not really using an official unwind segue (which is created by control-dragging from a button to the "exit" icon beneath a storyboard scene).
Unless you're referring to an unwind segue, there's no such thing as a "reverse" segue that takes you back to a previous screen. A push segue, for example, doesn't have a separate counterpart called a "pop" segue.
I suspect that you have segues crisscrossing between two scenes in storyboard. In other words, you created a segue from scene A to scene B, and you created a segue from scene B to scene A. If that's really the case, I would avoid doing that because it's unconventional. Maybe spend some time reviewing segues.
Ok, so I'll briefly address the fetched results controller issue. If there are user-driven changes to the data in a table view populated by a fetched results controller, then you will need to set a flag and temporarily "turn off" the fetched results controller delegate methods. This issue is mentioned briefly in the docs for NSFetchedResultsControllerDelegate protocol under "User-Driven Updates".
It appears my scouring of SO was not very good. see here https://stackoverflow.com/a/18998335/3482632
I added a UISegmentControl to filter and used the int value of its segment index.
int index = self.filterSegment.selectedSegmentIndex;
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"filter == %d", index]];
[fetchRequest setPredicate:predicate];
"filter" in my NSManagedObject subclass is an NSNumber. when I changed the NSPredicate to
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"filter == %#", #(index)]];
everything works fine.
i'm doing some testing of Core Data, let's say i have a mainViewController with a navigationBar and addButton.
Clicking on the addButton will open a detailViewController. When i press save to insert a new Object the detailVieController will close and show the table with the new data inserted.
I can think two different way to do that.
FIRST METHOD - Passing the ManagedObjectContext
In the action of the add button i create an instance of the new detailViewController and i pass the managedObjectContext to it. So will be the save button of the detailViewController that will take care of saving the context and then pop the controller.
This is the method called by the addButton in the MainViewController
-(void)addNewObject{
DetailViewController *detVC = [DetailViewController alloc]initWhit:self.managedObjectCOntext];
[self.navigationcontroller pushViewController:detVC animated:YES];
}
This method is called by the save button in the IngredientViewController
-(void)saveObject{
NSError *error;
if (![self.managedObjectContext save:&error]){
NSLog(#"Error");
}
}
SECOND METHOD - Using a delegate
In the action of addButton i create an instance of DetailViewController, i set it as delegate, so when i press the save button in the DetailViewCOntroller will call the delegate that will pass data to the main controller.
This is the method called by the addButton in the MainViewController
(void)addNewObject{
DetailViewController *detVC = [DetailViewController alloc]init];
detVC.delegate = self;
[self.navigationcontroller pushViewController:detVC animated:YES];
}
This method is called by the save button in the IngredientViewController
-(void)saveObject{
[self.delegate detailVCdidSaveObject];
}
This is the delegate implemented in the mainViewController
detailVCdidSaveObject{
NSError *error;
if (![self.managedObjectContext save:&error]){
NSLog(#"Error");
}
}
------------------------------ Passing the object
Is it best to pass raw data to the DetailViewController and create there the object or it's best to pass the instance of the object to DetailViewController that will take care of settin its data?
For Example
This way i link the object instance of the mainVC to the one DetailVC so i can easilly set its value
-(void)addObject{
DetailViewController *detailVC =[[DetailViewController alloc]init];
detailVC.delegate = self;
self.object = [NSEntityDescription insertNewObjectForEntityForName:#"Object" inManagedObjectContext:self.managedObjectContext];
detailVC.object = self.object;
[self.navigationController pushViewController:detailVC animated:YES];
}
this way i pass raw data and let the detailVC create the instance
-(void)addObject{
DetailViewController *detailVC =[[DetailViewController alloc]initWithName:#"objname"];
[self.navigationController pushViewController:detailVC animated:YES];
}
those code are just pseudocode for educational purpose. all ways works, i just want to know which do you think it's the most correct and why. thanks
I have used the first two methods and in my opinion they are both equally valid (though I personally prefer delegation). However, the third method caused problems if you give the user the option to cancel or go back in a navigation controller. If that happens, you will have an object that you never needed to create.
This sounds like a perfect use case for a NSFetchedResultsController. A NSFetchedResultsController is an object makes displaying data from core data in a UITableView a lot easier. It even tells you when the objects in core data matching a predicate change (insert, delete, update, move).
So the way I would do it is that MainViewController would have a NSFetchedResultsController that provides the data to the UITableView. When you press the add button, it would do what you have in the first method. The DetailViewController will create the new instance, set the values on it then save the managedObjectContext.
Since the MainViewController has the NSFetchedResultsController, it will automatically know that a new object have been created and it can update the UITableView to show it.
The NSFetchedResutsController documentation and the NSFetchedResutsControllerDelegate documentation show you exactly how to use it with a UITableView including code you can copy into your view controller that do the majority of the work.
The actual answer depends on your preference. In my project, I have implemented the first two methods. A definite No for the third method from my side because of same reasons as Kevin mentioned. If the user cancels the operation or some error occurs, then you will have to take care of removing the change (Perhaps write the following code in your didMoveToParentViewController method and cancel method):-
[self.managedObjectContext rollback]
Assuming of course that you do not have any other process modifying that managedObjectContext at the same time.
Now, I prefer the first two methods because :-
The first method allows me to write additional code in saveObject method. Lets say that you want to validate some properties before saving the object. These properties are only present in detailViewController. So, you cannot use a delegate in that situation without explicitly passing each and every property back to delegate function (which can get messy).
Now, assume that you are creating a object in your mainViewController and the detailViewController is only used to populate a field of the object that was created in mainViewController. In such a situation, I would use the delegate method and pass the field back to the mainViewController so that when the user saves the object in mainViewController, then the field values are saved along with it. If the user cancels mainViewController, then the field values are also not saved.
I have a split view where the top section of my split shows some questions, and the bottom section shows some other stuff. The problem is that I had it written to "push" to a new view every time the user selects a question. This is obviously less than ideal because the user can enter a situation where they have 15 copies (more or less, depending on how many times the user selects a question) of the same question to go back through.
I thought that a simple solution would be to set a BOOL for when a user selects a question, but as it turns out, this introduces a new bug where the user can select a question once, but if they go back they are out of luck. I'm kind of stuck here, and any guidance would be greatly appreciated.
Program flow:
First you need to understand a little about what I am trying to do. I am building a historical inquiry app that focuses on allowing teachers to support student learning of historical inquiry. As such, there are core questions as well as documents the students can analyze.
Based on the way the app has come along, JSLDetailViewController displays the core questions and JSL_QuestionInteraction displays the questions for analyzing the documents.
Relevant code snippet:
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0){
if(!didSelectQuestion){
[self performSegueWithIdentifier:#"questionDisplaySegue" sender:indexPath];
didSelectQuestion = TRUE;
} else {
JSLDetailViewController *detailView = [JSLDetailViewController alloc];
detailView.telegram = indexPath.row;
[detailView setDetailItem:indexPath];
}
}else if(indexPath.section == 1){
[self performSegueWithIdentifier:#"telegramQuestionDisplaySegue" sender:indexPath];
JSL_QuestionInteraction *questionView = [[JSL_QuestionInteraction alloc] init];
questionView.managedObjectContext = self.managedObjectContext;
}
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:#"questionDisplaySegue"]){
JSLDetailViewController *detailView = (JSLDetailViewController *)segue.destinationViewController;
detailView.telegram = index.row;
[detailView setDetailItem:index];
JSLDetailViewController *controller = (JSLDetailViewController *)segue.destinationViewController;
controller.managedObjectContext = self.managedObjectContext;
} else if ([segue.identifier isEqualToString:#"telegramQuestionDisplaySegue"]){
JSL_QuestionInteraction *questionView = [[JSL_QuestionInteraction alloc] init];
questionView.managedObjectContext = self.managedObjectContext;
}
}
Please let me know if you need any additional details to understand this problem.
I don't know if any of what I write here will fix your problem because I still don't really understand your structure, but I see a couple of things wrong in your posted code.
First, when you're doing segues you shouldn't be alloc init'ing anything in code, the segue instantiates the new controllers for you. It's not clear what you're doing with detailView in didSelectRowAtIndexPath:, you do an alloc with no init -- you should never ever do an alloc without an init. If that detailView is something that's already present on screen, you should get a reference to that instance and set telegram and detailItem on that.
In the "if" clause of prepareForSegue you are assigning the segue.destinationViewController to two different local variables, detailView and controller -- they both point to the same thing, so there's no reason to have them both.
In the "else" clause, once again your alloc init'ing a controller, which you shouldn't do. You probably want to get the segue's destination view controller instead.