I have a UITableView which modally presents a UIViewController when a cell is tapped. The UIViewController receives data from a model object corresponding to the tapped cell, and displays an interface to edit those data. When the user completes the edits, a button tap dismisses the UIViewController, and writes the edits to the model object.
Will the following code present any memory or design problems?
In presenting UITableView subclass implementation, acting as delegate for presented UIViewController:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UINavigationController *navigationController = segue.destinationViewController;
navigationController.delegate = this;
navigationController.dataModel = someDataModel;
}
// delegate callback
- (void) onViewControllerDone: (UIViewController *)controller {
[self.tableView reloadData];
}
In presented UIViewController subclass implementation:
- (IBAction) done: (id)sender {
// directly modify dataModel passed into UIViewController with data from UI
[self.dataModel.someProperty setString: self.textView.text];
[self.delegate onViewControllerDone:self];
}
Something smells funny about passing the data model into the view, and letting the view make the changes. I'm new to Objective-C / iOS development, and not sure if there's a better/preferred way to do this?
In my opinion, there isn't any flaw in what you're doing though there may be a more elegant approach to your problem. One option would be passing back the information to the original controller via delegation, so that for instance once you're done editing a certain field, the delegate is notified of this immediately and makes the required changes to the data model. This way, there's no passing of the model reference back and forth and you actually make sure that only one controller is responsible for editing it's contents. Something like:
- (void)doneEditing {
if ([_delegate respondsToSelector:#selector(fieldChanged:)]) {
[_delegate fieldChanged:self.newFieldValue];
}
}
Related
I'm trying to make a form that one of the filed takes value from a two level selections' result.
The main progress will something like:
EditViewController ===> CategoryViewController (which embedded inside a NavigationController by storyboard and popped up as a modal view) ===> SubCategoryViewController (Which will be pushed to NavigationController).
Now I have a problem. After user tap to select a value in SubCategoryViewController, I'm supposed to dismiss SubCategoryViewController and return the value to EditViewController. But I don't know exactly how.
Please suggest any solution.
Thank you.
EDIT:
Every one of those view controllers should have a public property for a weak reference to a model object that represents whatever is being edited.
So every ____ViewController.h file would have:
#property (weak, nonatomic) CustomItem *item.
in its interface (assuming a strong reference is somewhere in some data store or array of all the items).
When EditViewController is preparing for the segue to show CategoryViewController modally, it should assign that same reference to CategoryViewController's item property after assigning any data entered in EditViewController's form to item:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//TODO: assign data from controls to item, for example:
//self.item.title = self.titleField.text;
CategoryViewController *vc = (CategoryViewController *)segue.destinationViewController
vc.item = self.item; //pass the data model to the next view controller
}
Likewise for the segue from CategoryViewController to SubCategoryViewController. This ensures that every ViewController is editing the same object in memory. When you dismiss SubCategoryViewController (assuming somewhere in all of this CategoryViewController was already dismissed), viewWillAppear: will be called on EditViewController- there you can refresh any changes made in the modal views to the item property, just like you would when first displaying the view (it's actually the same method that's called):
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.titleField.text = self.item.title;
self.categoryLabel.text = self.item.category;
self.subcategoryLabel.text = self.item.subcategory;
//etc....
}
In my app, I am listing core data entries in a tableview. I want to allow the user to edit records using a detail view presented modally as a form view. I am observing peculiar behavior when editing records.
The flow:
User loads tableview with records. -working
User selects a record for editing. -working
User edits record in a view controller presented as a modal form view. -working
User saves edits and dismisses form view. -working
Tableview shows correct changes for the previously edited record. -working
User repeats steps 2 - 4 selecting a different record to edit. -working
Tableview shows correct data for all records. -Not Working.
At step 7, the tableview reverts the display of the first edited record to its original state. Subsequent record edits result in all previous edits reverting to their original state. If the tableview is dismissed and reloaded, the records are correct, showing all the edits.
I have used [tableview reload] in the tableview's ViewWillAppear method, but it does not seem to be fired when the modal form view controller is dismissed.
In my tableviewcontroller code:
-(void)viewWillAppear:(BOOL)animated
{
[self.tableView reloadData];
}
Searching around, I have not found a solution and am hoping someone can point me in the right direction.
Thanks!
When you presenting a modal view, main view controller's view never disappears. So, after you dismiss your modal view, viewWillAppear() won't be called.
You could try to implement a custom delegate function in your modal view, and set it in the main view controller, when your data is updated, fire the delegate function to reload your tableView, which is at your main viewController.
Understand what is delegate function in iOS, and
how to create a delegate function is like this:
In your ModalView.h:
// define the protocol for the delegate
#protocol ModelViewDelegate <NSObject>
- (void) didUpdateData;
#end
#interface ModalView: ViewController {
//create a delegate instance
id delegate;
}
// define delegate instance
#property (nonatomic, assign) id <ModelViewDelegate> delegate;
In your modalView.m:
#synthesize delegate;
and then inside your function, put the delegate function at place where you need to fire, for example:
- (void) updateDataIntoDatabase{
....
//Update work done.
[self.delegate didUpdateData];
//dismiss your modalView;
}
So, in your MainViewController.h,
#import ModalView.h
and
#interface ModalView: ViewController <ModelViewDelegate > {
...
}
Inside your MainViewController.m, you will get a warning saying that you need to implement the delegate function which you already declared. So, set the delegate function, and do the things that you like to do:
- (void) didUpdateData{
[self.tableView reloadData];
}
DON'T forget to set your modelView delegate to self after you instantiate the modalView instance. If not your delegate function won't be fired.
modalView.delegate = self;
Use completion block when you trigger the Modal viewcontroller:
-(void)editModal:(id)sender
{
KDSecondViewController *secondVC = [[KDSecondViewController alloc] init];
[self presentViewController:secondVC animated:YES completion:^{
//-- reload your table view when user dismiss the modal view
[self.tableView reloadData];
}];
}
I have 2 ViewControllers
ViewControllerWithCollectionView (FIRST) and ModalViewControllerToEditCellContent (SECOND)
I segue from FIRST to SECOND modally. Edit cell. Return.
After dismissing SECOND controller, edited cell doesn't get updated until i call
[collection reloadData]; somewhere manually.
Tried to put it in viewWillAppear:animated:, when i check log, it's not called (after dismissing SECOND)
I've tried various solutions, but i can't brake thru (maybe I'm just too exhausted). I sense that I'm missing something basic.
EDIT dismiss button
- (IBAction)modalViewControllerDismiss
{
self.sticker.text = self.text.text; //using textFields text
self.sticker.title = self.titleText.text;// title
//tried this also
CBSStickerViewController *pvc = (CBSStickerViewController *)self.stickerViewController;
//tried passing reference of **FIRST** controller
[pvc.cv reloadData];//called reloadData
//nothing
[self dismissViewControllerAnimated:YES completion:^{}];
}
It's tough to tell from the posted code what's wrong with the pointer to the first view controller that you passed to the second. You should also be able to refer in the second view controller to self.presentingViewController. Either way, the prettier design is to find a way for the first view controller to learn that a change has been made and update it's own views.
There are a couple approaches, but I'll suggest the delegate pattern here. The second view controller can be setup to have the first view controller do work for it, namely reload a table view. Here's how it looks in almost-code:
// SecondVc.h
#protocol SecondVcDelegate;
#interface SecondVC : UIViewController
#property(weak, nonatomic) id<SecondVcDelegate>delegate; // this will be an instance of the first vc
// other properties
#end
#protocol SecondVcDelegate <NSObject>
- (void)secondVcDidChangeTheSticker:(SecondVc *)vc;
#end
Now the second vc uses this to ask the first vc to do work for it, but the second vc remains pretty dumb about the details of the first vc's implementation. We don't refer to the first vc's UITableView here, or any of it's views, and we don't tell any tables to reload.
// SecondVc.m
- (IBAction)modalViewControllerDismiss {
self.sticker.text = self.text.text; //using textFields text
self.sticker.title = self.titleText.text;// title
[self.delegate secondVcDidChangeTheSticker:self];
[self dismissViewControllerAnimated:YES completion:^{}];
}
All that must be done now is for the first vc to do what it must to be a delegate:
// FirstVc.h
#import "SecondVc.h"
#interface FirstVc :UIViewController <SecondVcDelegate> // declare itself a delegate
// etc.
// FirstVc.m
// wherever you decide to present the second vc
- (void)presentSecondVc {
SecondVc *secondVc = // however you do this now, maybe get it from storyboard?
vc.delegate = self; // that's the back pointer you were trying to achieve
[self presentViewController:secondVc animated:YES completion:nil];
}
Finally, the punch line. Implement the delegate method. Here you do the work that second vc wants by reloading the table view
- (void) secondVcDidChangeTheSticker:(SecondVc *)vc {
[self.tableView reloadData]; // i think you might call this "cv", which isn't a terrific name if it's a table view
}
On iPad simulator, I have a ViewController A that presents an UIPopoverController whose contentViewController is ViewController B, inside which I have a button to dismiss the UIPopoverController.
When it is dismissed, I need to update the view of ViewController A based on some field in ViewController B.
In order to do this, I am declaring ViewController A as a property (weakref) of ViewController B so that within ViewController B where it dismisses the popover, I can say:
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;
self.viewControllerA.textLabel.text = self.someField
Is this the correct way of doing it? Since there is no callback when we dismiss the popover pragmatically, I can't think of any better solution.
Anybody has a better idea? Passing view controllers around just seems awkward to me.
The best way is use of Delegation, just declare the delegate in your controller B like
#protocol ControllerSDelegate <NSObject>
-(void) hidePopoverDelegateMethod;
#end
and call this on action for passing the data and dismiss of controller like
if (_delegate != nil) {
[_delegate hidePopoverDelegateMethod];
}
and
in your controller A you can handle this delegate call
-(void) hidePopoverDelegateMethod {
[self.paymentPopover dismissPopoverAnimated:YES];
if (self.paymentPopover) {
self.paymentPopover = nil;
}
[self initializeData];
}
I think, delegates or sending NSNotification will make better.
Note:
A change of the execution sequence will do more perfection to your current code.
self.viewControllerA.textLabel.text = self.someField
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;
I noticed some strange behavior when passing data to a popover in iOS 5. The Popovers viewDidLoad method is called before prepareForSegue is called:
In Storyboard a segue connects a button of FirstViewController to PopoverViewController, which is embedded in a Navigation Controller.
For testing the two methods are logged:
/* FirstViewController.m */
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"showPopover"]) {
NSLog(#"FirstViewController: prepareForSegue");
UINavigationController *navigationController = segue.destinationViewController;
PopoverViewController *popoverVC = (PopoverViewController *)navigationController.topViewController;
popoverVC.myProperty = #"Data to be passed";
}
}
and in the other ViewController:
/* PopoverViewController.m */
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"PopoverViewController: viewDidLoad");
}
Under iOS 6 the behavior is as expected:
2013-02-25 09:03:53.747 FirstViewController: prepareForSegue
2013-02-25 09:03:53.751 PopoverViewController: viewDidLoad
Under iOS 5 viewDidLoad of the PopoverViewController is called before prepareForSegue:
2013-02-25 09:05:28.723 PopoverViewController: viewDidLoad
2013-02-25 09:05:28.726 FirstViewController: prepareForSegue
This is strange and makes it hard to pass data to the Popover which can be used in viewDidLoad. Is there any solution to this?
I solved the problem now using the viewWillAppear: method instead of viewDidLoad. I think this is the better method for configuring views anyway (as the view could be already loaded and the view should be configured on every appear).
The viewWillAppear: method is loaded after the prepareForSegue in iOS 5 and iOS 6.
However, for those needing viewDidLoad the solution suggested by tkanzakic is the one that works then.
create a custom setter for your property and perform the operations you need from there, I use to do it in this way:
- (void)setCity:(GLQCity *)city
{
if (_city != city) {
_city = city;
[self configureView];
}
}
- (void)configureView
{
if (self.city) {
...
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
...
[self configureView];
}
I don't know anything about the differences between iOS 5 and 6, but I had run into similar confusions in the past. If you follow the general rules of thumb:
prepareForSegue is called before destination VC's viewDidLoad
viewDidLoad is called only after ALL outlets are loaded
Therefore, do not attempt to reference any destination VC's outlets in source VC's prepareForSegue.
Then, you would naturally arrived at either solutions - implement viewDidAppear vs. viewDidLoad, or set destination VC's properties only vs. touching any of its outlets.
Another lesson learned with regards to prepareForSegue is to avoid redundant processing. For example, if you already segue a tableView cell to a VC via storyboard, you could run into similar race condition if you attempt to process BOTH tableView:didSelectRowAtIndexPath and prepareForSegue. One can avoid such by either utilizing manual segue or forgo any processing at didSelectRowAtIndexPath.