My environment is Yosemite 10.10.5 with Xcode 7.2 using ARC.
In a simple test program, I am attempting various ways to dismiss a NSViewController and all of them are showing problems with memory handling.
In my primary view controller, I have the following code. (The notification pieces are there to test various ways of dismissing the presented controller.)
- (IBAction)showFirstReplacement:(id)sender {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(dismissWithNotification:) name:#"removeFirst" object:nil];
NSStoryboard *sb = [self storyboard];
FirstReplacement *controller = [sb instantiateControllerWithIdentifier:#"first_replacement"];
[self presentViewControllerAsSheet:controller];
}
- (void)dismissWithNotification:(NSNotification *)notification {
NSViewController *controller = [notification object];
[self dismissViewController:controller];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Inside FirstReplacement, I have:
- (IBAction)dismiss:(id)sender {
[self dismissViewController:self];
// [[NSNotificationCenter defaultCenter] postNotificationName:#"removeFirst" object:self];
// [[self presentingViewController] dismissViewController:self];
}
Uncommenting any one of the three lines in this method produces the correct visual results but.... Depending on which of the calls I enable inside dismiss:, I get different results when profiling. Using self dismissViewController:, I see no leaks but FirstReplacement objects are not deallocated. Using either of the other two approaches gets rid of the dismissed FirstReplacement but leaks one 16-byte malloc block and one NSMutableArray every time a view controller is dismissed.
According to Instruments, the leaks are related to a method called [NSViewController _addPresentedViewController:].
Are there other clean-up steps necessary to prevent these leaks (or memory bloat in the non-leak case)?
The view controller that presents another view controller is also responsible for dismissing it. So none of the lines in FirstReplacement's dismiss method are correct. Instead, you should be creating a delegate in FirstReplacement so it can notify its delegate (the primary view controller) that it should be dismissed.
FirstReplacement.h
#class FirstReplacement;
#protocol FirstReplacementDelegate <NSObject>
- (void)firstReplacementShouldDismiss:(FirstReplacement *)controller;
#end
#interface FirstReplacement : NSViewController
#property (nonatomic, weak) id<FirstReplacementDelegate> delegate;
#end
FirstReplacement.m
- (IBAction)dismiss:(id)sender {
[self.delegate firstReplacementShouldDismiss:self];
}
Then in your primary view controller:
- (IBAction)showFirstReplacement:(id)sender {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(dismissWithNotification:) name:#"removeFirst" object:nil];
NSStoryboard *sb = [self storyboard];
FirstReplacement *controller = [sb instantiateControllerWithIdentifier:#"first_replacement"];
controller.delegate = self;
[self presentViewControllerAsSheet:controller];
}
- (void)firstReplacementShouldDismiss:(FirstReplacement *)controller {
[self dismissViewController:controller];
}
While it may seem like posting a notification is the same as a delegate, it is not. The difference is that when dismissWithNotification fires, you are still executing the code from FirstReplacement::dismiss. NSNotificationCenter::postNotificationName does not finish executing until all observers have finished executing their selectors. So even though the dismissal code is executing in the primary view controller, it still being run from the dismiss method.
If you are still not convinced, override FirstReplacement::dealloc to print a log statement. You will see that dealloc is not called using any of your methods, but will be called using delegation.
Related
I have a custom class for NSButton called MyButton where I post a notification for a quicksave
MyButton.m:
-(void)mouseDown:(id)sender{
[super mouseDown:sender];
[super mouseUp:sender];
[[NSNotificationCenter defaultCenter] postNotificationName:#"quickSave" object:nil userInfo:nil];
}
in AppDelegate I get the notification for the quick save:
AppDelegate.m:
- (IBAction)saveAction:(id)sender{
NSLog(#"Saving...");
NSError *error = nil;
if (![[self managedObjectContext] commitEditing]) {
NSLog(#"%#:%# unable to commit editing before saving", [self class], NSStringFromSelector(_cmd));
}
if (![[self managedObjectContext] save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
-(void)awakeFromNib{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(saveAction:) name:#"quickSave" object:nil];
}
Via the NSLog "Saving..." I see that the saveAction is called 2 times. Why?
P.S: the notification calls 2 times every function I insert in the selector: field, so maybe it has to do with the -(void)awakeFromNib{...} because I see that it's called twice (there are two different self inside the awakeFromNib).
UPDATE: I've "solved" the problem binding in Interface Builder the Application as an AppDelegate delegate and then adding the [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(saveAction:) name:#"quickSave" object:nil]; inside the -(void)applicationDidFinishLaunching:(NSNotification *)aNotification{...}. I don't know if it's a real solution and obviously it is not an answer to my question (why awakeFromNib is called 2 times), but it might be useful to someone. Does anyone have a clue?
UPDATE2: the right managedobjectcontext is the one called in awakeFromNib the second time, the first one (identical in awakeFromNib and applicationDidFinishLaunching) is wrong.
My app is a statusbar app, the first awakeFromNib is called when i start the app and the second one when a preference window is opened.
The log message indicates that two different instances of AppDelegate receive a single notification. Probably you instantiated AppDelegate twice. Ensure you are not manually doing [[AppDelegate alloc] init] or something, and not putting more than one AppDelegate object in NIB.
I am trying to set the position of a UIScrollView by using contentOffset as such:
- (void) navigateToTableViewPosition:(CGPoint)contentOffset {
NSLog(#"Position set method gets called...");
NSLog(#"%#", NSStringFromCGPoint(contentOffset));
[mainScrollView setContentOffset:contentOffset animated:YES];
}
I call this method from another view controller before I dismiss it, and everything checks out. I pass the argument correctly, and the method gets called (checked it with NSLog), but the scroll view does not move...
What is funny is that when I call this method from the view controller, in which it is located, it works fine. Only when I call it from another view controller, it stops working.
Just for future reference, here is the calling method:
MainViewController *mainView = [[MainViewController alloc] init];
[mainView navigateToTableViewPosition:contentOffset];
Content offset is a CGPoint I set beforehand. It doesn't matter here; besides, it gets passed correctly anyways.
Try this, You have to send notification from other viewcontroller when you want to change ..
[[NSNotificationCenter defaultCenter] postNotificationName:#"changepostion" object:NSStringFromCGPoint(CGPointMake(contentOffset.x, contentOffset.y))];
in mainviewcontroller
-(void)viewDidLoad
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(navigateToTableViewPosition:) name:#"changepostion" object:nil];
}
- (void) navigateToTableViewPosition:(NSNotification *)notification
{
contentOffset =CGPointFromString([notification object]);
NSLog(#"Position set method gets called...");
NSLog(#"%#", NSStringFromCGPoint(contentOffset));
[mainScrollView setContentOffset:contentOffset animated:YES];
}
You can't set the properties of a view which is not visible. If you are using iOS5+ you can implement the offset setting in the completion in the view dismiss completion block.
Use delegate for backward messaging in view controllers.
Refer Basic Delegate Example link for more reference.
Your are making new instance of viewcontroller which will call method but will have no effect.
I need help with delete an array with a button from the SecondViewController in the view controller class.
At the moment I have this:
- (IBAction)deleteArrayFromSeconViewController:(id)sender // is the Button which should
// delete the array
{
self.textLabel2.text = #""; // this work fine
ViewController *vc = [[ViewController alloc]init];
[vc.textViewArray removeAllObjects]; // do not remove the objects?
}
What should I do to overtake an order from the SecondViewControllerClass in the ViewControllerClass?
I tried also this in the SecondViewControllerClass:
- (IBAction)deleteArrayFromSeconViewController:(id)sender
{
self.textLabel2.text = #"";
ViewController *vc = [[ViewController alloc]init];
[vc deleteTheArray];
}
to call this function in the ViewControllerClass:
- (void) deleteTheArray
{
[textViewArray removeAllObjects];
}
I'm not at all sure that this is the best way to do this, but you could post a custom NSNotification in your first view controller, and then pick it up in your second. Code Example:
//Post in your first view controller when wanting to delete the array
[[NSNotificationCenter defaultCenter] postNotificationName:#"DeleteArrayNotification" object:nil userInfo:nil /*include any items your reciever might wish to access*/];
and then in your second view controller, you could add yourself as an observer in the -(void)awakeFromNib Method. Put in your awake from nib method:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(deleteArrayNotification:) name:#"DeleteArrayNotification" object:nil];
And then implement the deleteArrayNotification: method:
- (void)deleteArrayNotification:(NSNotification *)notification {
[array removeAllObjects];
[array release]; //Delete this line if your project is using ARC
}
I am highly doubtful that this is good coding practice to implement NSNotification like this, but I thought it might be of use!
More information can be found under the Apple developer documentation, under NSNotification and NSNotificationCenter.
Hope I can help!
Ben
I'm trying to use this code but Xcode returns an error because the method I'm trying to call in the selector:#selector() is in another class. Thanks for your help!
AppDelegate.m:
-(void)applicationDidBecomeActive:(UIApplication *)application{
[..]
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(myMethodHere) name:UIApplicationDidBecomeActiveNotification object:nil];
}
MainViewController.m:
-(void)myMethodHere{
[..]
}
The problem is that you use
addObserver:self
which means that it looks for the function in the current class. Instead do something like
addObserver:instanceOfOtherClass
Update
Add the call to the init method of MainViewController
// MainViewController.m
- (id)init;
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(someMethod) name:UIApplicationDidBecomeActiveNotification object:nil];
}
return self;
}
Make sure to remove yourself in dealloc
- (void)dealloc;
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
By doing it this way from the very moment the object comes in to existence it is ready to receive notifications and then when it is being deallocated it will safely remove itself.
A good pattern to follow is to make the class that is doing the observing responsible for registering for notifications. This keeps encapsulation well and removes some risk of sending notification to deallocated instances.
Rationale
You need to balance your calls for registering for notifications and unregistering for notifications otherwise a message may be called on a deallocated object which could be hard to track down.
If I have a class that needs to be notified of an event the likely hood is I will register for the notifications in the init method and then unregister for the notifications in the dealloc (init and dealloc are just examples of times I often do this, not necessarily the best place in every example, do what makes sense in your case).
The issue is your use of
addObserver:self
The observer needs to be an instance class that contains the method you want to call, so create that first and then add the notification. Something like.
-(void)applicationDidBecomeActive:(UIApplication *)application{
[..]
SomeClass *newObject = [[SomeClass alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:newObject selector:#selector(someMethodContainedInSomeclass) name:UIApplicationDidBecomeActiveNotification object:nil];
}
I have defined the controller to receive the events.
#interface salesViewController : UIViewController
<UITextFieldDelegate>{
However, none of my events are not firing.
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
//this is not getting called
}
In Interface Builder I assigned the TextField delegate to the salesView.
What am I missing?
You have to set the delegate properly. You observe the protocol, but you need to do this:
#interface YourController : UIViewController<UITextFieldDelegate> {
IBOutlet UITextField* field;
}
#end
#implementation YourController
-(void)viewDidLoad
{
[field setDelegate:self];
}
And you will receive the events. Alternatively, you can set the delegate in Interface Builder as well, along with doing it programmatically in loadView, allocating the field and setting the delegate.
Additionally, try to use NSNotificationCenter as little as possible. Notifications are somewhat obsolete unless there isn't really a direct path between you and the object in question. Just a small comment on the answer above.
what are you trying to accomplish? textFieldDidBeginEditing is messaged whenever the user selects the text field. If you are trying to update a label or something as the user makes edits, you need to setup an observer w/ NSNotificationCenter and watch for the notification that is fired whenever this happens.If you take this approach, make sure to remove the observer once you are done with it
for example:
#pragma mark
#pragma mark -
#pragma mark Notification Observers
- (void)addObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(textFieldDidChange:) name:#"UITextFieldTextDidChangeNotification" object:nil];
}
- (void)removeObservers {
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"UITextFieldTextDidChangeNotification" object:nil];
}
if you need to keep tabs on multiple text fields, do something like this for your selector:
- (void)textFieldDidChange:(NSNotification*)aNotification {
UITextField *textField = (UITextField *)[aNotification object];
if([textField isEqual:usernameTextField])
{
[user setUsername:usernameTextField.text];
}
else if([textField isEqual:phoneNumberTextField])
{
[user setPhoneNumber:phoneNumberTextField.text];
}
}