UITableView edit propagation to the model in MVVM - ios

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.

Related

NSFetchedResultsController object deleted from UITableView on update

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.

Where I should I prepare my data? In awakeFromNib, viewDidLoad or something else

I am still relatively new to iOS programming. Here is a question that confused me for a long time.
So in one of the view controllers, before this view controller is pushed into the navigation item, I am passing one parameter, say userId, to it in the prepareForSegue from previous view controller. And when this view controller is loading (initialising) based on the userId from the previous view controller, I am making a network call to fetch a list of information that's related to this user and then populating this information to the model of the current view controller.
Where should I put the logic of this data preparation?
Using viewDidLoad: should be fine for common storyboard use because the storyboard does not reuse view controller. Anyway, for the completeness of my view controller usage scenario, I tend to use this pattern:
Start loading remote data asynchronously in viewWillAppear:
Stop loading remote data in viewWillDisappear:
This make sure that your data will be always updated to the current userId because the ID might be changed after viewDidLoad, e.g. in case of view controller reuse or accessing .view property before setting userId.
You should also track if your data has been loaded. For example, you could make a private boolean field named _isDataLoaded, set it to true when finish loading data and set it to false when cancelling loading data or setting new userId.
To sum it up, the pattern in my idea should be something like this:
#interface UserViewControler : UIViewController {
bool _isDataLoaded;
NSURLConnection _dataConnection;
}
#implementation UserViewController
-(void) setUserId:(int)userId {
if (_userId != userId) {
_userId = userId;
_isDataLoaded = false;
}
}
-(void) viewWillAppear:(BOOL)animated {
if (!_isDataLoaded) {
_dataConnection = // init data connection here
_dataConnection.delegate = self;
[_dataConnection start];
}
}
-(void) viewWillDisappear:(BOOL)animated {
if (_dataConnection) {
[_dataConnection cancel];
_dataConnection = nil;
_isDataLoaded = false;
}
}
// NSURLConnection call this when finish
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_isDataLoaded = true;
_dataConnection = nil;
}
// NSURLConnection call this when fail to load data
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
_isDataLoaded = false;
_dataConnection = nil;
}
It depends on what framework you use to retrieve data from remote server, but the pattern should be like this. This will ensure that:
You will load data only when the view appear.
View controller will not loading more data after disappear.
In case of same userId, data would not be downloaded again.
Support view controller reuse.
- (void) viewDidLoad {
[super viewDidLoad];
// initialize stuff
}
Although, it may be better to do the network call and gather all this information into a custom class that contains all the information, and then perform the segue. Then all you have to do in the new view controller is pulled data out of the object (which would still be done in viewDidLoad).
Arguably, this method might be better because if there's a problem with the network, you can display an error message and then not perform the segue, giving the user an easier way to reattempt the same action, or at least they'll be on the page to reattempt the same action after leaving app to check network settings and coming back.
Of course, you could just segue forward always, and segue backward if there's a network error, but I think this looks sloppier.
Also, it's worth noting that if you're presenting the information with a UICollectionView or a UITableView, the presenting logic can (should) be moved out of viewDidLoad and into the collection/table data source methods.
What I have done in the past is make custom initializers.
+(instancetype)initWithUserID:(NSString)userID;
Here is an example of the implementation.
+(instancetype)initWithUserID:(NSString *)userID {
return [[self alloc] initWithUserID:userID];
}
-(id)initWithUserID:(NSString *)userID {
self = [self initWithNibName:#"TheNameOfTheNib" bundle:nil];
if(self) {
_userID = userID;
}
//do something with _userID here.
//example: start loading content from API
return self;
}
-(void)viewDidLoad {
//or do something with userID here instead.
}
The other thing I would suggest is make a custom class that loads data and uses blocks.
Then you can do something like this
[API loadDataForUserID:userID withCompletionBlock^(NSArray *blockArray) {
//in this case I changed initWithUserID to initWithUsers
[self.navigationController pushViewController:[NextController initWithUsers:blockArray] animated:YES];
}

PFQueryTableViewController reload table when back btn pressed

Using Parse, after I'm logged in I am presented PFQueryTableViewController that displays a list of tasks and another detail view controller that allows me to edit the task detail and segue back. The issue right now is that the PFQueryTableViewController does not reflect the new changes after I finished editing and popping the task detail view off the stack. However the table view list does get updated when I go back to the login screen(view before the PFQueryTableViewController) and re-enter the table view again. I've tried the following:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
}
and also
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadObjects];
[self.tableView reloadData];
}
Yet they don't seem to take effect. My guess is that the data is updated after the view is popped off and the table view appears. I'm just wondering if anyone has any insight on this while I'm investigating. Thanks!
You could try re-querying the queryForTable method in viewDidAppear (this would naturally use an API request on every view appearance however)
- (void)viewDidAppear:(BOOL)animated {
[self queryForTable];
}
This answer assumes that you are using Local Datastore and want to see changes made in it be reflected in a PFQueryTableViewController.
Because the ParseUI classes do not implement any form of caching though the local datastore, changes made in the detail view will not appear in the PFQueryTableViewController until the save operation has completed and the tableView has fetched the new items from Parse.
One solution to your problem would be adding a category to the PFQueryTableViewController that modifies how it fetches data to include what is in the Local Datastore as well.
You should make sure the data is saved before popping your view controller.
Use Parse's save method with completion handler.
[request saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
You can use [self loadObjects] to trigger a refresh for all objects in the PFQueryTableViewController.

Delete existing data from another view

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]

io looking for insert and cancel pattern

I've been looking around for a good pattern to implement a insert then cancel pattern when working with a UINavigationBar and UITableView.
I have I have a "insert"button in my TeamsViewController navigation bar (screenshot)
Which when I run it runs this code:
-(void)insertTeam
{
if( !detailViewController ) {
detailViewController = [[TeamDetailViewController alloc] init];
}
if( !teams ) {
teams = [NSMutableArray array];
}
Team *team = [[Team alloc] init];
[teams addObject:team];
int lastIndex = [teams count];
[detailViewController setEditingTeam:[teams objectAtIndex:lastIndex - 1]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
[self presentModalViewController:navigationController animated:YES];
}
Which is great if the user fills out all the info, but if they hit cancel on the next view, there's an empty object in my arrary.
I'm sure there's a great pattern to achieve this but I've looked at all the TableView sample codes, two different ios books, and tried googling it, but haven't found a pattern for this.
My thought is something like the following:
When user cancels, set a canceled ivar in my Team object to YES
Back in my TeamsViewController, when the view appears check the last object in my teams array and see if it's property canceled is YES, if so remove that last object.
But this doesn't seem so slick and I was figuring there was some better way to achieve this. TIA.
I would be tempted to make the TeamsViewController a delegate of the TeamDetailViewController. The delegate would implement a method such as - (void)teamCreated:(Team *)team; and it would update the array. Since there seems to be no point to having a Team in the array that's incomplete, I would have the TeamDetailViewController create the Team and pass it back in the delegate call. On a cancel, there would be no need to do anything except pop the controller.

Resources