Updating UI Elements with Controller Constructor - objective-c

I have an NSSlider (slider) and an NSLabel (label) on a window. I also have a class "Controller" that updates the label whenever the slider's value is changed.
The default position of the slider is 0.5, I'm trying to get is where Controller's constructor updates the label upon program launch.
The following is the implementation file for my attempt to do this. Everything works fine except the label is always 0 when I start the program.
#implementation Controller
{
}
-(id)init
{
NSLog(#"initializing...");
[self updateLabel];
return self;
}
- (IBAction)sliderChanged:(id)sender
{
[self updateLabel];
}
- (void)updateLabel
{
label.text = [NSString stringWithFormat:#"%.1f", slider.value];
}
#end
In the console I see the text "initializing...", but the label is never updated. What am I missing?

The controller may be getting initialized (where is your call to [super init]?), but that doesn't mean the outlets are hooked up. The proper way to do that would be to rely on a viewDidLoad, windowDidLoad, or awakeFromNib method.

you should achieve this with bindings and without any "glue code" in controllers.
Here's some reference on how to use them: http://cocoadevcentral.com/articles/000080.php

Related

Cocoa Subclassing weirdness

I'm trying to understand how Subclassing works in Cocoa.
I've created a new Cocoa Application Project in XCode 5.1.
I drag a new Custom View onto the main window.
I create a new Objective-C class CustomViewClass and set it as a Subclass of NSView. This generates the following :
CustomViewClass.h
#import <Cocoa/Cocoa.h>
#interface CustomViewClass : NSView
#end
CustomViewClass.m
#import "CustomViewClass.h"
#implementation CustomViewClass
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
NSLog(#"Custom View initialised");
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
// Drawing code here.
}
#end
Note that I added the NSLog(#"Custom View initialised"); line so I can track what is going on.
In interface Builder, I select the Custom View and within the Idenditiy Inspecter set it's custom Class to CustomView. Then I run the Application.
As expected I get a Custom View initialised message in the Console.
I do exactly the same with an NSTextField adding it to the window, creating a new class TextFieldClass and the NSTextField custom Class is to TextFieldClass. I also add a NSLog(#"Text Field initialised"); in the same place as above to track things.
However when I run the App, I only get the Custom View initialised message in the Console and not the NSLog(#"Text Field initialised");message.
So initially I think that NSTextField doesn't recieve the initWithFrame message when it is created. So I add an initialiser to TextFieldClass :
- (id)init {
self = [super init];
if (self) {
// Initialization code here.
NSLog(#"Text Field initialised");
}
return self;
}
However this still doesn't seem to get called.
I assumed therefore that NSTextField just wasn't being subclassed. However, when I add this method to TextFieldClass :
-(void)textDidChange:(NSNotification *)notification {
NSLog(#"My text changed");
}
Run the app and lo and behold, every time I type in the text field I get the My text changed message in the Console.
So my question is, what is going on here? How does the NSTextField get initialized and how can you override it's initialiser?
Why does the Custom View seem to act differently to the NSTextField?
Source code here
For your first question, NSTextFiled gets initialised via
- (id)initWithCoder:(NSCoder *)aDecoder
In this case, you have dragged a NSTextField from the palette and then changed the class to your custom text field class in the identity inspector. Hence the initWithCoder: will be called instead of initWithFrame:. The same is true for any object (other than Custom View) dragged from the palette
Instead, if you drag "Custom View" from the palette and change the class to your custom text field class, the initWithFrame: will be invoked.
The CustomViewClass you have created is the second case, hence initWithFrame: is invoked. The TextFieldClass is the first case, hence initWithCoder: is invoked.
If you use the Interface Builder in XCode, you should use awakeFromNib to initialise your subclass.
- (void)awakeFromNib
{
// Your init code here.
}
If you want to use your subclass programatically and using the interface builder, then use code like this:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initView];
}
return self;
}
- (void)awakeFromNib
{
[self initView];
}
- (void)initView
{
// Your init code here
}

textShouldEndEditing in NSOutlineTableView is getting called twice

I just implemented following method that suppose to take some action after the value of a NSTextField is changed in my NSOutlineView
-(BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor
{
NSLog(#"end editing");
NSTextField* tf = (NSTextField*)control;
if(selectedItem && [selectedItem isKindOfClass:[HSCategoryClass class]])
{
HSCategoryClass* c = selectedItem;
c.name = tf.stringValue;
// request the update from DB
[[NSNotificationCenter defaultCenter] postNotificationName:#"updatingCategoryName"
object:c
userInfo:#{#"sender":self}];
}
return YES;
}
However, when I'm done editing and hit enter key or navigate anywhere outside of the text field this method is getting called twice instead of just once.
Does anyone know why is this?!
Any kind of help is highly appreciated!
That routine does not signify that editing has ended. Instead, it's called to find out if it should end (hence the name of the method). It can be called by the framework any number of times, and you shouldn't be relying on it for this purpose.
Instead override the NSOutlineView's textDidEndEditing: method.
Be sure to call super.
So you'd subclass the NSOutlineView and in your subclass:
- (void)textDidEndEditing:(NSNotification *)aNotification
{
// do your stuff
[super textDidEndEditing:aNotification];
}

Presenting modal dialogs from XIB in Cocoa: best/shortest pattern?

Below is my typical WindowController module for presenting a modal dialog (could be settings, asking username/password, etc) loaded from a XIB. It seems a bit too complex for something like this. Any ideas how this can be done better/with less code?
Never mind that it's asking for a password, it could be anything. What frustrates me most is that I repeat the same pattern in each and every of my XIB-based modal window modules. Which of course means I could define a custom window controller class, but before doing that I need to make sure this is really the best way of doing things.
#import "MyPasswordWindowController.h"
static MyPasswordWindowController* windowController;
#interface MyPasswordWindowController ()
#property (weak) IBOutlet NSSecureTextField *passwordField;
#end
#implementation MyPasswordWindowController
{
NSInteger _dialogCode;
}
- (id)init
{
return [super initWithWindowNibName:#"MyPassword"];
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self.window center];
}
- (void)windowWillClose:(NSNotification*)notification
{
[NSApp stopModalWithCode:_dialogCode];
_dialogCode = 0;
}
- (IBAction)okButtonAction:(NSButton *)sender
{
_dialogCode = 1;
[self.window close];
}
- (IBAction)cancelButtonAction:(NSButton *)sender
{
[self.window close];
}
+ (NSString*)run
{
if (!windowController)
windowController = [MyPasswordWindowController new];
[windowController loadWindow];
windowController.passwordField.stringValue = #"";
if ([NSApp runModalForWindow:windowController.window])
return windowController.passwordField.stringValue;
return nil;
}
The application calls [MyPasswordWindowController run], so from the point of view of the user of this module it looks simple, but not so much when you look inside.
Set tags on your buttons to distinguish them. Have them both target the same action method:
- (IBAction) buttonAction:(NSButton*)sender
{
[NSApp stopModalWithCode:[sender tag]];
[self.window close];
}
Get rid of your _dialogCode instance variable and -windowWillClose: method.
-[NSApplication runModalForWindow:] will already center the window, so you can get rid of your -awakeFromNib method.
Get rid of the invocation of -[NSWindowController loadWindow]. That's an override point. You're not supposed to call it. The documentation is clear on that point. It will be called automatically when you request the window controller's -window.
Get rid of the static instance of MyPasswordWindowController. Just allocate a new one each time. There's no point in keeping the old one around and it can be troublesome to reuse windows.

How can I get noticed when subclassed NSSecureTextField get focus each time?

I subclassed NSSecureTextField, and overrided -(BOOL)becomeFirstResponder,but it only works well when my custom NSSecureTextField get focus at the first time.
I think it is not a problem of NSSecureTextField. It is a problem of how a control get focused.
Create a new project and only drag two NSSecureTextField on your view. Set the custom class of one of them to MySecureTextField defined below and keep the other one default. Run the project and change focus between the two NSSecureTextField, you will see the "Get focus" printed when the custom one get focus each time.
Back to your program, please check if the NSSecureTextField is lost focus? Does the resignFirstResponser called?
#import "MySecureTextField.h"
#implementation MySecureTextField
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (BOOL)becomeFirstResponder
{
NSLog(#"Get focus");
return [super becomeFirstResponder];
}
#end

UINavigationBar topItem/items seems to double-pop on back

I am managing my own UINavigationBar. I need to do this due to extensive skinning. The documentation for UINavigationController warns that there are limitations to skinning the UINavigationBar when used with a UINavigationController.
I have put in extensive logging and from everything I can tell, pushing the "Back" button in the UINavigationController pops two items off of of the stack instead of one. I get a single delegate callback telling me that it is removing the logical item, but it actually removes that one and one more.
The item added to the UINavigationController in awakeFromNib should never be removed. It is being removed for some reason.
There are two similar questions, but neither have satisfactory answers. The two questions are:
UINavigationBar .items accessor doesn't return the current UINavigationItem
UINavigationBar seems to pop 2 items off stack on "back"
- (void)awakeFromNib {
[headerView setDelegate: self];
[headerView pushNavigationItem: tableDisplay animated: NO];
}
- (void) selectedStory: (NSNotification *)not {
[headerView pushNavigationItem: base animated: NO];
NSLog(#"Selected story: %#", base);
}
- (void) baseNav {
NSLog(#"Current items: %#", [headerView items]);
BaseInnerItem *current = (BaseInnerItem *)[headerView topItem];
[self addSubview: [current view]];
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPushItem: (UINavigationItem *)item {
return YES;
}
- (BOOL)navigationBar: (UINavigationBar *)navigationBar shouldPopItem: (UINavigationItem *)item {
return YES;
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
NSLog(#"didPushItem: %#", item);
[self baseNav];
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
NSLog(#"didPopItem: %#", item);
[self baseNav];
}
Edited to add relevant debugging from a single run:
2010-10-13 02:12:45.911 Remix2[17037:207] didPushItem: <TableDisplay: 0x5d41cc0>
2010-10-13 02:12:45.912 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>"
)
2010-10-13 02:12:49.020 Remix2[17037:207] didPushItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:49.021 Remix2[17037:207] Current items: (
"<TableDisplay: 0x5d41cc0>",
"<WebDisplay: 0x591a590>"
)
2010-10-13 02:12:49.023 Remix2[17037:207] Selected story: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.498 Remix2[17037:207] didPopItem: <WebDisplay: 0x591a590>
2010-10-13 02:12:59.499 Remix2[17037:207] Current items: (
)
You always have to call [super awakeFromNib] when your subclass implements that method, per the documentation for -awakeFromNib:
You must call the super implementation of awakeFromNib to give parent classes the opportunity to perform any additional initialization they require
Importantly, however, ...
I don't understand why you have to actually manage your own navigation bar. If you subclass UINavigationBar and only override certain drawing or layout methods such as -drawRect:, -layoutSubviews, etc., then all of the logic behind managing the navigation bar in a navigation controller will just fall back on the original UINaviationBar class.
I've had to do extensive view customization for almost every major UIKit class, but I always left the complicated logic to the original classes, overriding only drawing methods to customize the look and feel.
Incidentally, it's actually much easier to skin an entire app without subclassing at all if all you're doing is using custom image assets. By setting a layer's contents property, you can either customize the look and feel of a UIView-based class on an as-needed basis or throughout your entire app:
#import <QuartzCore/QuartzCore.h>
...
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage * navigationBarContents = [UIImage imageNamed:#"navigation-bar"];
self.navigationController.navigationBar.layer.contents =
(id)navigationBarContents.CGImage;
}
You can set the contents for any class that inherits from UIView: navigation bars, toolbars, buttons, etc. It's a lot easier to manage this way without having to subclass at all.
This appears to be a bug in the implementation of -[UINavigationBar items]
When called from inside the -navigationBar:didPopItem: delegate method, it will omit the last object. You can check this by calling [navigationBar valueForKey:#"_itemStack"] to retrieve the underlying array and see that the expected items are still there.
Adding a dispatch_async inside -navigationBar:didPopItem:method successfully works around the issue in my app.