Setting/getting global variables in objective-C - objective-c

I am writing an app which is a sort of dictionary - it presents the user with a list of terms, and when clicked on, pops up a dialog box containing the definition. The definition itself may also contain terms, which in turn the user can click on to launch another definition popup.
My main app is stored in 'myViewController.m'. It calls a custom UIView class, 'CustomUIView.m' to display the definition (this is the dialog box that pops up). This all works fine.
The text links from the CustomUIView then should be able to launch more definitions. When text is tapped in my CustomUIView, it launches another CustomUIView. The problem is, that this new CustomUIView doesn't have access to the hash map which contains all my dictionary's terms and definitions; this is only available to my main app, 'myViewController.m'.
Somehow, I need to make my hash map, dictionaryHashMap, visible to every instance of the CustomUIView class. dictionaryHashMap is created in myViewController.m when the app opens and doesn't change thereafter.
I don't wish to limit the number of CustomUIViews that can be opened at the same time (I have my reasons for doing this!), so it would be a little resource intensive to send a copy of the dictionaryHashMap to every instance of the CustomUIView. Presumably, the solution is to make dictionaryHashMap a global variable.
Some of my code:
From myViewController.m:
- (void)viewDidLoad
{
self.dictionaryHashMap = [[NSMutableDictionary alloc] init]; // initialise the dictionary hash map
//... {Code to populate dictionaryHashMap}
}
// Method to pop up a definition dialog
- (void)displayDefinition:(NSString *) term
{
NSArray* definition = [self.dictionaryHashMap objectForKey:term]; // get the definition that corresponds to the term
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
[definitionPopup setTitle: term];
[definitionPopup setMessage: definition];
[definitionPopup show];
}
// Delegation for sending URL presses in CustomUIView to popupDefinition
#pragma mark - CustomUIViewDelegate
+ (void)termTextClickedOn:(CustomUIView *)customView didSelectTerm:(NSString *)term
{
myViewController *t = [[myViewController alloc] init]; // TODO: This instance has no idea what the NSDictionary is
[t displayDefinition:term];
}
From CustomUIView.m:
// Intercept clicks on links in UIWebView object
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
[myViewController termTextClickedOn:self didSelectTerm:request];
return NO;
}
return YES;
}
Any tips on how to make the dictionaryHashMap visible to CustomUIView would be much appreciated.
I have tried making the dictionaryHashMap global by doing the following:
Changing all instances of 'self.dictionaryHashMap' to 'dictionaryHashMap'
Adding the line 'extern NSMutableDictionary *dictionaryHashMap;' to CustomUIView.h
Adding the following outside of my implementation in myViewController.m: 'NSMutableDictionary *dictionaryHashMap = nil;'
However, the dictionaryHashMap remains invisible to CustomUIView. As far as I can tell, it actually remains a variable which is local to myViewController...

It's not resource-intensive to pass around the reference (pointer) to dictionaryHashMap. A pointer to an object is only 4 bytes. You could just pass it from your view controller to your view.
But I don't know why you even need to do that. Your view is sending a message (termTextClickedOn:didSelectTerm:) to the view controller when a term is clicked. And the view controller already has a reference to the dictionary, so it can handle the lookup. Why does the view also need a reference to the dictionary?
Anyway, if you want to make the dictionary a global, it would be more appropriate to initialize it in your app delegate, in application:didFinishLaunchingWithOptions:. You could even make the dictionary be a property of your app delegate and initialize it lazily.
UPDATE
I didn't notice until your comment that termTextClickedOn:didSelectTerm: is a class method. I assumed it was an instance method because myViewController starts with a lower-case letter, and the convention in iOS programming is that classes start with capital letters. (You make it easier to get good help when you follow the conventions!)
Here's what I'd recommend. First, rename myViewController to MyViewController (or better, DefinitionViewController).
Give it a property that references the dictionary. Whatever code creates a new instance of MyViewController is responsible for setting this property.
Give CustomUIView properties for a target and an action:
#property (nonatomic, weak) id target;
#property (nonatomic) SEL action;
Set those properties when you create the view:
- (void)displayDefinition:(NSString *)term {
NSArray* definition = [self.dictionaryHashMap objectForKey:term];
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
definitionPopup.target = self;
definitionPopup.action = #selector(termWasClicked:);
...
In the view's webView:shouldStartLoadWithRequest: method, extract the term from the URL request and send it to the target/action:
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
NSString *term = termForURLRequest(request);
[self.target performSelector:self.action withObject:term];
return NO;
}
return YES;
}
In the view controller's termWasClicked: method, create the new view controller and set its dictionary property:
- (void)termWasClicked:(NSString *)term {
MyViewController *t = [[MyViewController alloc] init];
t.dictionary = self.dictionary;
[t displayDefinition:term];
}

Create a class that will be used as singleton. Example.

You Should always keep your data in separate class as the mvc pattern suggest and that could be achieved by using a singleton class for all your dictionary terms and accesing them from every custom view when needed.

Related

Retain a window created in a callback

I'm wondering what's the correct way to spawn a bunch of windows from a callback, basically i'd like to hit a combination like Ctrl+Shift + Cmd + + and create a new window, which is not bound to the app delegate. Currently i have the following code in my AppDelegate.m:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
NSLog(#"Finished");
[NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^(NSEvent *event) {
NSUInteger ctrlPressed = [event modifierFlags] & NSEventModifierFlagControl;
NSUInteger shiftPressed = [event modifierFlags] & NSEventModifierFlagShift;
NSUInteger cmdPressed = [event modifierFlags] & NSEventModifierFlagCommand;
NSUInteger EqButton = 0x30;
if (ctrlPressed && shiftPressed && cmdPressed) {
if ([event keyCode] & EqButton) {
__strong ETTimerController *controller = [ETTimerController new];
[controller showWindow: self];
}
}
}];
}
This snippet creates a controller and a bound nib file, but i can't see the window, i assume it's collected by the ARC. How can i retain create and retain a new window without storing the reference within the AppDelegate instance?
Not only the window, but I think the window controller is being released here once the block exits.
Store it in a strong property of AppDelegate:
#property (nonatomic, strong) timerController *ETTimerController;
Then, instantiate it like this:
self.timerController = [ETTimerController new];
[self.timerController showWindow: self];
EDIT: As mentioned in the comments, the window controller reference needs to be owned by some persistent object (or be a static variable somewhere) otherwise it goes out of scope as soon as the method exits and ARC deallocates the object.
If you can't afford a dedicated property in the AppDelegate class (because you need to have a variable number of different window controllers, etc.), then you will need to consider what is the expected lifetime of each controller, and store a reference accordingly:
If your window controller is reused, i.e. its window is repeatedly shown and hidden but you only need at most one, make it a singleton and the static reference to the shared instance will take care of retaining it.
If you are building a document-based app, each instance of your NSDocument subclass will create all its window controllers within the makeWindowController() method, and retain them inside an array property.
If none of the above apply, you will need to think of something else that -again- will vary depending on your needs and specifications.

Nested NSCollectionView With Bindings

I am trying to nest NSCollection view inside of one another. I have tried to create a new project using the Apple Quick Start Guide as a base.
I start by inserting a collection view into my nib, to the view that is automatically added I drag another collection view onto it. The sub-collection view added gets some labels. Here is a picture of my nib:
I then go back and build my models:
My second level model .h is
#interface BPG_PersonModel : NSObject
#property(retain, readwrite) NSString * name;
#property(retain, readwrite) NSString * occupation;
#end
My First level model .h is:
#interface BPG_MultiPersonModel : NSObject
#property(retain, readwrite) NSString * groupName;
#property(retain,readwrite) NSMutableArray *personModelArray;
-(NSMutableArray*)setupMultiPersonArray;
#end
I then write out the implementation to make some fake people within the first level controller(building up the second level model):
(edit) remove the awakefromnibcode
/*- (void)awakeFromNib {
BPG_PersonModel * pm1 = [[BPG_PersonModel alloc] init];
pm1.name = #"John Appleseed";
pm1.occupation = #"Doctor";
//similar code here for pm2,pm3
NSMutableArray * tempArray = [NSMutableArray arrayWithObjects:pm1, pm2, pm3, nil];
[self setPersonModelArray:tempArray];
} */
-(NSMutableArray*)setupMultiPersonArray{
BPG_PersonModel * pm1 = [[BPG_PersonModel alloc] init];
pm1.name = #"John Appleseed";
pm1.occupation = #"Doctor";
//similar code here for pm2,pm3
NSMutableArray * tempArray = [NSMutableArray arrayWithObjects:pm1, pm2, pm3, nil];
return tempArray;
}
Finally I do a similar implementation in my appdelegate to build the multiperson array
- (void)awakeFromNib {
self.multiPersonArray = [[NSMutableArray alloc] initWithCapacity:1];
BPG_MultiPersonModel * mpm1 = [[BPG_MultiPersonModel alloc] init];
mpm1.groupName = #"1st list";
mpm1.personModelArray = [mpm1 setupMultiPersonArray];
(I'm not including all the code here, let me know if it would be useful.)
I then bind everything as recommended by the quick start guide. I add two nsarraycontrollers with attributes added to bind each level of array controller to the controller object
I then bind collectionview to the array controller using content bound to arrangedobjects
Finally I bind the subviews:
with the grouptitle label to representedobject.grouptitle object in my model
then my name and occupation labels to their respective representedobjects
I made all the objects kvo compliant by including the necessary accessor methods
I then try to run this app and the first error I get is: NSCollectionView item prototype must not be nil.
(edit) after removing awakefromnib from the first level model I get this
Has anyone been successful at nesting nscollection views? What am I doing wrong here? Here is the complete project zipped up for others to test:
http://db.tt/WPMFuKsk
thanks for the help
EDITED:
I finally contacted apple technical support to see if they could help me out.
Response from them is:
Cocoa bindings will only go so far, until you need some extra code to make it all work.
When using arrays within arrays to populate your collection view the
bindings will not be transferred correctly to each replicated view
without subclassing NSCollectionView and overriding
newItemForRepresentedObject and instantiating the same xib yourself,
instead of using the view replication implementation provided by
NSCollectionView.
So in using the newItemForRepresentedObject approach, you need to
factor our your NSCollectionViewItems into separate xibs so that you
can pass down the subarray of people from the group collection view to
your inner collection view.
So for your grouped collection view your override looks like this:
- (NSCollectionViewItem *)newItemForRepresentedObject:(id)object
{
BPG_MultiPersonModel *model = object;
MyItemViewController *item = [[MyItemViewController alloc] initWithNibName:#"GroupPrototype" bundle:nil];
item.representedObject = object;
item.personModelArray = [[NSArrayController alloc] initWithContent:model.personModelArray];
return item;
}
And for your inner collection subclass your override looks like this:
- (NSCollectionViewItem *)newItemForRepresentedObject:(id)object
{
PersonViewController *item = [[PersonViewController alloc] initWithNibName:#"PersonPrototype" bundle:nil];
item.representedObject = object;
return item;
}
here is a sample project that they sent back to me -
http://db.tt/WPMFuKsk
I am still unable to get this to work with my own project. Can the project they sent back be simplified further?
Please take a closer look at this answer
Short answer:
Extracting each NSView into its own .xib should solves this issue.
Extended:
The IBOutlet’s specified in your NSCollectionViewItem subclass are not connected when the prototype is copied. So how do we connect the IBOutlet’s specified in our NSCollectionViewItem subclass to the controls in the view?
Interface Builder puts the custom NSView in the same nib as the NSCollectionView and NSCollectionViewItem. This is dumb. The solution is to move the NSView to its own nib and get the controller to load the view programmatically:
Move the NSView into its own nib (thus breaking the connection between the NSCollectionViewItem and NSView).
In I.B., change the Class Identity of File Owner to the NSCollectionViewItem subclass.
Connect the controls to the File Owner outlets.
Finally get the NSCollectionViewItem subclass to load the nib:
Usefull links:
how to create nscollectionview programatically from scratch
nscollectionview tips
attempt to nest an nscollectionview fails
nscollectionview redux

NSWindowController and isWindowLoaded

I have an NSWindowController and I initialize it like this;
+ (MyWindowController *) sharedController
{
static MyWindowController *singleton = nil;
if (!singleton) singleton = [[self alloc] initWithWindowNibName: #"myWindow"];
return singleton;
}
and I show windows like this;
[[MyWindowController sharedController] showWindow: nil];
Now the problem is that I need information from some controls on that window. But I do not want to load the window if it's not yet loaded because then I can just go with the defaults. Should I use isWindowLoaded? #property to access the singleton? or what is recommended here? (If #property, then please give me the readonly, nonatomic attributes too.)
Don't store model data in views. Have the controller (probably not MyWindowController, but the one that needs the data) own the real data (if any) and fill in any defaults.
Any values you fill in in Interface Builder should be for nothing more than sizing.
For example, if I know a field must hold a number whose value is ±50000, I'll enter “-50000” and size the field accordingly, and leave the “-50000” there. The actual default is more likely to be 0 or something, and I will have that provided by the controller that owns the value (or, if the field shows a property of a model object, I'll have the default provided by each new model object).

Singleton Design

I'm creating a game that uses cards.
I have an AppController class with one instance in the nib.
The AppController instance has an NSArray instance variable called wordList.
On init, the nib's instance of AppController generates a new GameCard.
Every gamecard has an array of words containing 5 words selected at random from the the list in AppController.
Because the list is large, I'd like to read it into memory only once. Therefore, I want only one instance of AppController, as a singleton class. Every time a new GameCard is created from within AppController, it should access that same singleton instance to retrieve the wordlist.
So basically, I need a singleton AppController that creates GameCards, where each GameCard has a reference to the original AppController.
I'm not sure how to implement this. Sorry if the explanation was confusing.
A code example I found online follows (http://numbergrinder.com/node/29)
+ (AppController *)instance
{
static AppController *instance;
#synchronized(self) {
if(!instance) {
instance = [[AppController alloc] init];
}
}
return instance;
}
But when I tried to do something with it in a GameCard instance through the code below, my application took forever to launch and Xcode told me it was loading 99797 stack frames.
AppController *controller = [AppController instance];
It sounds like an infinite loop. Make sure that -[AppController init] isn't calling +[AppController instance].
Why does every card need a reference to the app controller?
If it's just to access its words, it's simpler to let each card own its words directly. Make a new method named initWithWords: the designated initializer for the GameCard class. Initialize each card with the array of its five words, and have the card own that array for its lifetime.
Removing the cards' references to the app controller would resolve the infinite loop that Tom astutely detected.
Also, if no word should appear on two cards at once, remember to take that into account when drawing from the app controller's Great Big Array Of Words, and when destroying cards (you may or may not want the words to go back into the pile for future cards).
It sounds like you're on the right track. I've never tried to put a reference to a singleton in a nib file, though. You may want to create a separate singleton class that maintains a copy of the data (DataManager, maybe?), and then call it from within your instance of AppController to fetch the words.
You may find that putting a singleton within a nib (using the code for a singleton in Stu's post) works just fine, though. Good luck!
It looks like you may be calling your class instance method from within your init method. Try something like this:
static AppController* _instance = nil;
- (id)init
{
// depending on your requirements, this may need locking
if( _instance ) {
[self release];
return _instance;
}
if( (self = [super init]) ) {
_instance = [self retain];
// do your initialization
}
return self;
}
+ (AppController*)instance
{
if( _instance ) return _instance;
else return [[AppController alloc] init];
}
This makes sure that only one instance of AppController is ever available and also that it's safe to allocate it as well as getting a copy through the instance class method. It's not thread safe, so if it's going to be accessed by multiple threads, you should add some locking around the checks to _instance.
The normal way to create an AppController/AppDelegate is to add a custom NSObject to your MainMenu/MainWindow.xib file. Set the class to be AppController. Link your UIApplication/NSApplication delegate reference to your AppController object. Then you can get your single AppController with either
(AppController*)[NSApp delegate];
or
(AppController*)[[UIApplication sharedApplication] delegate];
You never have to create it with alloc/init because it will be created when your application is launched. You don't have to worry about making it a singleton because no one will ever try to create another one. And you don't have to worry about how to access it because it will be the delegate of the UIApplication/NSApplication object.
All that said, if you need a global variable holding an array of words, then forget about the AppController and make a new singleton object which holds/reads the array. In which case you just need:
+ (NSArray *)sharedWordListArray
{
static NSArray *wordList;
if( !wordList ) {
wordList = [[NSMutableArray alloc] init];
// read array
}
return wordList;
}
If you really need thread safety, then simply call [WordList sharedWordListArray] from your app delegate's applicationDidFinishLaunching: method before starting any threads, or add an NSLock if you really want to defer the loading to later, but often its better to take the load time hit at the start of the program rather than unexpectedly when the user takes some later action.

event scope

Given
#interface Canvas:NSView {
NSNumber * currentToolType;
...
}
declared in my .h file
and in the .m file
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
currentToolType=[[NSNumber alloc]initWithInt:1];
}
return self;
}
and further down
-(void)mouseUp:(NSEvent *)event
{
NSLog(#"tool value in event: %d",[currentToolType intValue]);
//rest of code
}
-(NSBezzierPath *)drawPath:(NSRect)aRect
{
NSLog(#"tool value in draw: %d",[currentToolType intValue]);
//rest of drawPath method code that uses the value of currentToolType in a switch statment
}
-(IBAction)selectToolOne:(id)sender
{
[currentToolType release];
[currentToolType = [[NSNumber alloc]initWithInt:0];
}
-(IBAction)selectToolTwo:(id)sender
{
[currentToolType release];
[currentToolType = [[NSNumber alloc]initWithInt:1];
}
The action methods are the only place where currentToolType is changed. But, for some reason, it seems to be a different instance of currentToolType in the mouseUp. I did not write (or synthesize) accessors for the var as it is used only by itself. I noticed that initWithFrame is called twice - I'm assuming it's for the parent window and the NSView?
What am I missing?THANKS!
This is an XCode generated Document based app using COCOA and Obj-C. I'm new at both.
You mention that initWithFrame: is called twice. Your initWithFrame: should only be called once (unless you happen to have two Canvas views).
Is it possible you have the Canvas view in your nib/xib file and are also creating another in code (with alloc/initWithFrame:)?
In which case you have two Canvas objects. You probably have one hooked up to your controls and the other one is in the window (and thus responding to the mouseUp: and it is giving you the same value every time).
If you have the Canvas view setup in IB, you can fix this problem by removing your code that is creating the second one.
You've probably run in to a special case: NSNumber could have cached instances to represent commonly-used numbers.
Two observations, though:
You're wasting a whole lot of memory using NSNumber when you could be simply using NSIntegers or maybe an old-fashioned enumerated type, completely avoiding the object overhead.
You never actually showed your code for when you look at the instances of NSNumber; without it, there's not really enough information here to answer your question.