LearningCocoa
This page contains notes about my first, second, third, ... steps with Cocoa. Other related pages on this Wiki:
Documentation
Apple's developer documentation is extensive but - in my opinion - not excellent. When I come to a new subject I often feel overwhelmed by the available material. I then wish for a simple primer or exercise that walks me through a few steps, without giving me all the information, until I have the sense of a first achievement.
The companion guides try to do this, but they sometimes quite fail. The worst example so far has been the "Cocoa bindings" guide. I spent about 3 nights until I felt confident enough to attempt a simple preferences dialog. Of course it didn't help that I was also unfamiliar with Key-Value-Coding and therefore had to find out first what KVC was. Again, the extensive KVC guide was overwhelming - I always felt like I had to read on and on because I would otherwise miss some vital information.
My last point of criticism is that information is often distributed between class documentations and companion guides. I am seldom able to fully understand how to use a class from its documentation alone, and vice versa the companion guide is usually not detailed enough, either.
Mac OS X Technology Overview
http://developer.apple.com/documentation/MacOSX/Conceptual/OSX_Technology_Overview/index.html#//apple_ref/doc/uid/TP40001067
(esp. interesting is the chapter "Mac OS X Runtime Architecture", on the topic of what happens when an application is launched; this provides insight into the workings of dyld & Co.)
.nib files and custom classes
Classes
TODO: describe how to create a custom class
Instances
TODO: describe how to work with instances of a custom class -> simply create an instance that is visible as such on the "Instances" tab -> assign custom class to special objects "file's owner" and "first responder" -> how to make connections (outlets, target/action)
Implementation
TODO: describe how to implement a custom class -> awakeFromNib, special initializers when it's an NSView (?)
Object creation/destruction
An instance of your custom class is created in the regular fashion using some alloc/init combination. The object has a retain count of 1. If the object is a GUI widget, it may have a retain count >1 when the awakeFromNib message is sent.
An object that is connected to an outlet is not retained again, it keeps its retain count of 1! If you need to retain such an object, you must do so manually (i.e. in awakeFromNib, or wherever suitable).
When the user terminates the application, your custom class instance is not deallocated automatically, unless the object is some GUI widget, in which case deallocation occurs, probably because the parent window is also deallocated. If you want/need to perform proper cleanup, you need to implement a handler that reacts to application shutdown. I usually implement applicationWillTerminate in my NSApplication delegate (often a controller object):
- (void)applicationWillTerminate:(NSNotification*)aNotification { [self autorelease]; }
In the dealloc method I can then implement proper cleanup, for instance:
- (void) dealloc { // Deallocate preferences dialog controller if (m_thePreferences) [m_thePreferences autorelease]; // Deallocate application model if (m_theModel) [m_theModel autorelease]; // Etc. [...] [super dealloc]; }
Faceless
The application needs to be made faceless, i.e. it has no visible representation in the dock. To achieve this, the Info.plist file in the application bundle must contain the following key:
<key>LSUIElement</key> <string>1</string>
Note that using LSUIElement also removes the application from the "Force Quit" dialog.
I tried for about half an hour to find out how I could add a menu to the menubar. Neither Google nor the Xcode help system would provide me with any useful information (I obviously did not know what to look for), so I ended up downloading and looking at the source code of a utility that I knew was doing it right. Praise to the GPL, and thanks to Dustin Bachrach for the QuickTunes source code.
So, the obvious class for accessing the menubar (as of course anyone but me would have immediately guessed) is ... NSStatusBar:
NSStatusBar* bar = [NSStatusBar systemStatusBar]; NSStatusItem* theItem = [bar statusItemWithLength:NSVariableStatusItemLength]; [theItem retain]; [theItem setTitle: NSLocalizedString(@"Foo",@"")]; [theItem setHighlightMode:YES]; [theItem setMenu:theMenu];
??? Login item ???
??? ~/Library/Menu Extras ???
??? Extension .menu for the app ???
Key-Value Coding (KVC)
Overview
The "key-value coding" mechanism is actually something very simple: it means nothing else than that a property is identified by a name (= a string) and that its value can be accessed programmatically through an interface that only uses the property's name. The interface is defined by the NSKeyValueCoding protocol.
Under normal circumstances, a property's value is determined by invoking an accessor method, or by directly accessing a member variable. Under KVC, a client accesses an object's property value by invoking valueForKey:. A class that conforms to the NSKeyValueCoding protocol either implements the method (usually not recommended), or relies on the default implementation in NSObject.
The default implementation of valueForKey: invokes an accessor whose name exactly matches the specified key. If no such accessor exists, the default implementation tries to directly access a member variable whose name exactly matches the key.
The KVC write accessor defined by NSKeyValueCoding is called setValue:forKey:. The default implementation in NSObject first tries for a method named set<Key>, then for the member variable whose name exactly matches the key.
The actual default implementations for valueForKey: and setValue:forKey: are a little bit more complicated, but for this overview's purpose this is enough.
Key-Value Validation
NSKeyValueCoding additionally defines an interface for validating a property value. The validation infrastructure provides a class the opportunity to accept a value, provide an alternate value, or deny the new value for a property and give a reason for the error.
The general-purpose method for validation is named validateValue:forKey:error:
, however it is not recommended to override this method. Instead the class should rely on the method's default implementation which looks for and invokes a validation method whose name conforms to this convention:
validate<Key>:error:
The method signature for a validation method for a key "foo" looks like this:
- (BOOL) validateFoo:(id*)valueObject error:(NSError**)outError
There are three possible outcomes from a validation method:
- The value is valid
- The validation method returns YES
- The validation method does not alter the value or the error object
- The value is not valid and a valid value cannot be created and returned
- The validation method returns NO
- The validation method does not alter the value object
- The validation method sets the error parameter to an NSError object that indicates the reason why validation failed
- The value is not valid and a valid value can be created and returned
- The validation method returns YES
- The validation method alters the value object to refer to the newly created, valid value.
- The validation method does not alter the error object
Notes:
- Scalar values are wrapped in an
NSValue
orNSNumber
object - In the second outcome, the validation method must check that the error parameter is NULL before assigning it an NSError object
- The last outcome is typically discouraged because it can be difficult to handle this correctly and consistently, and there are also issues in regard to memory management.
- In the last outcome, the passed value object must not be modified, even if it is mutable!
Code example:
- (BOOL) validateStatusItemText:(id*)valueObject error:(NSError**)outError { NSString* text = (NSString*)*valueObject; if (nil == text || 0 == text.length) { if (outError != NULL) { NSString* errorString = @"Status item text must not be empty. Please enter at least one character."; NSDictionary* userInfoDict = [NSDictionary dictionaryWithObject:errorString forKey:NSLocalizedDescriptionKey]; *outError = [NSError errorWithDomain:errorDomain code:ErrorCodeStatusItemTextIsEmpty userInfo:userInfoDict]; } return NO; } if (0 == [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length) { if (outError != NULL) { NSString* errorString = @"Status item text must not consist of only whitespace. Please enter at least one non-whitespace character."; NSDictionary* userInfoDict = [NSDictionary dictionaryWithObject:errorString forKey:NSLocalizedDescriptionKey]; *outError = [NSError errorWithDomain:errorDomain code:ErrorCodeStatusItemTextIsWhitespace userInfo:userInfoDict]; } return NO; } return YES; }
Notes:
errorDomain
is a string constant defined somewhere else; I usually set it to my application name or the bundle identifier- The ErrorCode* names are numeric values from an enumeration defined somewhere else
Keys and Key Paths
Keys must use ASCII encoding, begin with a lowercase letter and may not contain whitespace.
A key path is a series of keys chained together using the "." character. For instance: "foo.bar" would refer to the property "bar", which is a sub-property of "foo".
Key-Value Observing (KVO)
Reference: KVO Guide.
The "key-value observing" mechanism defines an observer pattern that builds on the concept of KVC: An observer object may specify that it wants to be notified when the setter of a given key, or key path, is invoked in the specified observable. For instance:
[anObservable addObserver:anObserver forKeyPath:"foo" options:0 context:NULL]; [anObservable addObserver:anObserver forKeyPath:"bar" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
Important: The notification is sent even if the old and the new values are equal, or are the same object! This cocoa-dev post nicely explains that KVO is primarily about behaviour (i.e. the setter being called), and not necessarily about data (i.e. values that change).
To actually receive the notification, the observer must implement the following method:
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if ([keyPath isEqual:@"foo"]) { // Do something // Note: When we registered we specified "0" as options, meaning that // we get neither the old nor the new value } else if ([keyPath isEqual:@"bar"]) { id oldValue = [change objectForKey:NSKeyValueChangeOldKey]]; id newValue = [change objectForKey:NSKeyValueChangeNewKey]]; [...] } }
To get rid of the observer registration:
[anObservable removeObserver:anObserver forKeyPath:@"foo"];
Note: KVO methods are defined in the informal NSKeyValueObserving
protocol.
Cocoa bindings
Basics
Cocoa bindings rely on the "key-value coding" mechanism.
Bindings for simple GUI controls (e.g. NSTextField, NSButton (=check box)) are done via the value binding.
Bindings for NSMatrix (= radio button group) and NSPopupButton (= combo box) are done via the selectedIndex binding. It is also possible to populate the NSMatrix and NSPopupButton with entries using the contentValues.
User Defaults
Interface Builder:
- Create a preferences dialog in any .nib
- For any GUI control, choose "Bindings" section in the Inspector window
- Enable the binding
- Select "Shared User Defaults" as the binding target
- the "Shared Defaults" instance is automatically created
- Select "values" as the controller key; the GUI control is now managed by the shared NSUserDefaultsController object
- Enter the key for the property that the GUI control represents (e.g. "TreatAllFilesAsArchives")
- do other configuration if the binding is non-trivial
Program code:
- as usual register the application's preference defaults with the NSUserDefaults method registerDefaults:()
- provide the NSUserDefaultsController with initial values that it should use to give to the GUI controls that it manages
- in my opinion this is not necessary as long as a) the same default values are registered with NSUserDefaults, and b) there is no "revert to factory defaults" feature
- registering default values and providing initial values should happen as early as possible; as suggested by the Apple guides, about the best place to do this is in the class method initialize() of the NSApplication delegate
Example:
+ (void)setupDefaults { NSString* userDefaultsValuesPath; NSDictionary* userDefaultsValuesDict; NSDictionary* initialValuesDict; NSArray* resettableUserDefaultsKeys; // load the default values for the user defaults userDefaultsValuesPath = [[NSBundle mainBundle] pathForResource:@"UserDefaults" ofType:@"plist"]; userDefaultsValuesDict = [NSDictionary dictionaryWithContentsOfFile:userDefaultsValuesPath]; // set them in the standard user defaults [[NSUserDefaults standardUserDefaults] registerDefaults:userDefaultsValuesDict]; // if your application supports resetting a subset of the defaults to // factory values, you should set those values // in the shared user defaults controller resettableUserDefaultsKeys = [NSArray arrayWithObjects:@"Value1",@"Value2",@"Value3",nil]; initialValuesDict=[userDefaultsValuesDict dictionaryWithValuesForKeys:resettableUserDefaultsKeys]; // Set the initial values in the shared user defaults controller [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:initialValuesDict]; }
NSColor cannot be stored directly in NSUserDefaults, it must be archived/unarchived. Code example:
NSData* archivedColor = [NSArchiver archivedDataWithRootObject:[NSColor greenColor]]; [dictionary setObject:archivedColor forKey:@"FavoriteColor"];
To get the color into a color picker (NSColorWell) via Cocoa bindings, the binding must be used in conjunction with the value transformer "NSUnarchiveFromData".
Master-detail Interface
Interface Builder
- Create interface
- define whether you want single or multi selection
- make sure that cells in the master table view are not editable (since you want the editing to happen in the "detail" part of the interface)
- Create an instance of NSArrayController (from the "Controllers" tab in the Palette window)
- Edit the controller's attributes in the Inspector
- if it should be possible to create new instances, specify the "Object class name" (e.g. DGSMonXServer) and enable the "editable" check box
- on the "Bindings" tab, under "Controller content", change the "contentArray" setting
- bind to = the model object that provides the controller with an object collection (e.g. DGSMonXModel); you may have to create a new class and instance of that class in the .nib so that you can select that object instance in the drop-down menu
- model key path = the name of the array within the model (e.g. serverList)
- Bind "Master" GUI controls to the NSArrayController
- select each column (!) in the table and do the following
- on the Inspector's "Bindings" tab, under "value", change the "value" setting
- bind to = the NSArrayController
- controller key = arrangedObjects
- model key path = the name of the property that the column should display (e.g. serverName)
- enable/disable the "editable" check box on the Inspector's "Attributes" tab, as is appropriate
- Connect the enabled binding of the add/remove buttons to the canAdd and canRemove controller keys of the NSArrayController
- Connect the buttons to the target/actions add: and insert: of the NSArrayController
- Note: these target/actions are not normally visible in the Inspector when the NSArrayController is examined; these target/actions become available only if you control-drag the button on to the NSArrayController instance
- Make sure that the class for which the add button instantiates objects has a sensible implementation of init
- Alternatively, the NSArrayController can be subclassed and the appropriate method needs to be overwritten so that it instantiates objects using the correct initializer
- select each column (!) in the table and do the following
- Bind "Detail" GUI controls to the NSArrayController
- select each GUI control
- on the Inspector's "Bindings" tab, under "value", change the "value" setting
- bind to = the NSArrayController
- controller key = selection
- model key path = the name of the property that the column should display (e.g. serverName)
Note: When binding a GUI control to an NSArrayController, the "model key path" is automatically added to the controller's list of keys (Inspector tab "attributes"). When the GUI control's model key path is later changed, the controller retains the old key. If nobody uses the key any longer it should be manually removed from the controller.
Run loops
Every thread has its own run loop that can be accessed using the currentRunloop method of NSRunLoop.
When you run a run loop with runMode:beforeDate: your code does not return from the method call until the "beforeDate" is reached, or until "something happens". Code execution blocks inside the run loop, i.e. no (noticeable) processing power is used.
"Something happens" = An input source attached to the run loop triggers an event.
Possible input sources are NSTimer and NSPort, although some additional internal Cocoa input sources may exist. With the exception of timers, you generally don't have to care about input sources, how they are added to the run loop, and how they work. You just run the loop and do whatever you need to do - the run loop and Cocoa internals take care of everything else "automagically".
Timers
A run loop processes events from an NSTimer. You create a timer like this
// Trigger every 3 seconds NSTimeInterval triggerFrequency = 3.0; [NSTimer scheduledTimerWithTimeInterval:triggerFrequency target:checkThreadObject selector:@selector(runChecks) userInfo:nil repeats:YES];
The timer adds itself to the current thread's run loop. When the timer triggers, the method runChecks is called in the context of the thread to which the run loop belongs. The run loop method runMode:beforeDate: does not return, though, when a timer event occurs.
Note: On Mac OS X 10.4.8, Xcode 2.3, the run loop method runMode:beforeDate: returns when a non-repeating timer triggers for its first (and only) time. An unconfirmed theory of mine is that this happens because the non-repeating timer invalidates itself when it fires, thus removing the last input source from the run loop.
NSPort & Thread communication
Besides the usual approach using mutexes, locks and the like, threads may also communicate with each other using "Mach ports" or "Distributed objects". For examples how to set up such communication channels see the corresponding topics in the ADC companion guide "Multithreading Programming Topics".
Both mechanisms utilize the NSPort class for their purpose, which in turn requires that both threads are running their run loops. An application's main thread usually runs its run loop automatically because this thread manages the GUI. Other threads need to run their run loop explicitly.
The interesting point about using NSPort is that when thread 1 sends a message to thread 2, the method that is invoked in response to the message is executed in the context of thread 2! Thus, the awkward protection of resources with mutexes and locks can possibly be reduced significantly, or even eliminated completely. Of course, some of the problems arising from concurrent code execution have simply shifted to other areas, and if you are not careful you may even void any benefits you get from multi-threading. For instance, if thread 1 sends its message synchronously, but thread 2 cannot respond because it is currently doing some work, thread 1 will become blocked until thread 2 has finished its work; if thread 1 is the GUI thread, your application will no longer respond to user interaction until it de-blocks.
Sequence of events:
- thread 2 runs its run loop with runMode:beforeDate and blocks for input events
- thread 1 sends a synchronous message over an NSPort to thread 2
- because the message is synchronous, thread 1 blocks and waits for the message response
- in thread 2, NSPort generates an input event that de-blocks the run loop
- the run loop in thread 2 invokes the method that corresponds to the message
- when the method returns in thread 2, runMode:beforeDate also returns and the control loop around runMode:beforeDate has a chance to perform additional stuff
- control returns to thread 1 after the message response in thread 2 has completed; the exact time when this occurs in relation to code execution in thread 2 is unclear/undefined - after all, the two threads have again begun to run concurrently
URL Loading System
References
- Article "URL Loading System" (located under ADC Home > Reference Library > Guides > Cocoa > Networking)
Overview
The term "URL Loading System" refers to a number of Foundation framework classes available for interacting with URLs and communicating with servers using standard Internet protocols. The URL Loading System can be categorized as follows:
- URL loading
- Cache management
- Authentication and credentials
- Cookie storage
- Protocol support
Important classes:
- NSURL
- Represents an URL.
- NSURLRequest (NSMutableURLRequest)
- Represents a request for the contents of a URL. Encapsulates a URL and any protocol-specific properties (in a protocol-independent manner, I guess because NSURLRequest is a base class).
- NSURLConnection
- Provides the interface to make a connection specified by an NSURLRequest object. The connection is made asynchronously. Requires a delegate to handle the various issues related to making the connection (i.e. responding to redirects, authentication challenges, and error conditions).
- NSURLDownload
- Similar to NSURLConnection, but stores the content data to a file.
- NSCachedURLResponse
- Represents the response from a server to a request. The response has two parts: Metadata and content data. Encapsulates the response's metadata part in the form of an NSURLResponse object, and the actual content data in the form of an NSData object.
- NSURLResponse
- Represents the metadata part of the response from a server to a request. Encapsulates metadata that is common to most protocols. Subclasses store protocol-specific metadata (e.g. NSHTTPURLResponse).
- NSHTTPCookieStorage
- Provides the interface for managing the collection of NSHTTPCookie objects shared by all applications.
- NSHTTPCookie
- Represents a cookie.
- NSURLCache
- Implements the caching of responses to URL load requests by mapping NSURLRequest objects to NSCachedURLResponse objects.
- NSURLCredential
- Represents a credential that can be used for authentication.
- NSURLProtocol
- Represents a protocol. Is an abstract class intended for subclassing, in order to extend the URL Loading System with new protocols.
Asynchronous vs. synchronous download
NSURLConnection supports synchronous downloads, although quite a lot of the flexibility of handling asynchronous downloads is lost because no delegate can be specified.
A synchronous load is built on top of the asynchronous loading code made available by the class. The calling thread is blocked while the asynchronous loading system performs the URL load on a thread spawned specifically for this load request.
Working with the URL Loading System
The following list contains the steps that must be taken to implement a simple URL Loading System client:
- Create an NSURLRequest object that specifies the request
- Create an NSURLConnection object, specifying the NSURLRequest object and the delegate that will handle the connection. Note: Unless specified otherwise, the download begins immediately when the NSURLConnection object is created.
- The delegate must implement at minimum the following methods:
- connection:didReceiveResponse:
- connection:didReceiveData:
- connection:didFailWithError:
- connectionDidFinishLoading:
- If the NSURLConnection object was created successfully, create an NSMutableData object that will store the data that is incrementally provided to the delegate
- The delegate will receive the following messages in the listed order:
- connection:didReceiveResponse:
- Is sent as soon as the server has sent sufficient data to create an NSURLResponse object.
- The delegate can now examine the metadata stored in the NSURLResponse object, e.g. to determine the expected content length of the data
- The message may be sent multiple times, e.g. for redirects or (in rare cases) multi-part MIME documents. Each time the delegate receives the message, it should reset any progress indication and discard all previously received data.
- connection:didReceiveData:
- Is sent multiple times to provide the actual content data
- The delegate can incrementally store the data in the NSMutableData object that was created earlier. It might also be suitable to provide indication of progress to the user.
- connection:didFailWithError:
- Is sent if an error occurs during the download
- An NSError object is specified as part of the message
- No further messages are sent for that connection and the NSURLConnection can be released
- onnectionDidFinishLoading:
- Is sent if the download succeeds
- No further messages are sent for that connection and the NSURLConnection can be released
- connection:didReceiveResponse:
Additional delegate methods, not listed above, provide the ability to customize the handling of
- server redirects
- authorization requests
- caching of the response
Redirects
The companion guide says:
If the delegate doesn't implement connection:willSendRequest:redirectResponse:, all canonical changes and server redirects are allowed.
This is not true for Xcode 2.3, gcc 4.0.1! If the delegate does not implement the method, the redirect does not happen!!!
Cookies
Important: Cookies are shared among all applications using the URL loading system. Safari, for instance, is such an application.
Cookies are automatically stored during an NSURLConnection if the cookie accept policy (available through NSHTTPCookieStorage) is set to NSHTTPCookieAcceptPolicyAlways or (better) NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain.
Archives
References
Overview
- An archive preserves the identity of every object in the object graph that is being archived, and all the relationships it has with all the other objects in the graph. When unarchived, the rebuilt object graph should, with few exceptions, be an exact copy of the original object graph.
- Instead of storing data in a specially designed file format, an application might store its data model objects directly into an archive and thus implement Save and Open with only minimal effort
- .nib files are archives
- To support archiving, an object must implement the
NSCoding
protocol, which consists of two methods. One method encodes the object’s important instance variables into the archive and the other decodes and restores the instance variables from the archive. - Many Cocoa classes already implement
NSCoding
(e.g.NSString
,NSArray
, etc.).
Glossary
- Coder
- An instance of a concrete subclass of the abstract class
NSCoder
. Archives are read/written by coders. Different subclasses ofNSCoder
implement support for different archiving formats (e.g. keyed archives, sequential archives). - Conditional object
- An object whose encoding is deferred until at some point in the encoding process it is encoded unconditionally. If this never happens, the object is not encoded at all. This concept is used so that only parts of an object graph can be archived. Typically, conditional objects are used to encode weak, or non-retained, references to objects.
- Keyed archive
- Objects in a keyed archive are assigned names, or keys. Objects in a keyed archive can therefore be decoded non-sequentially.
- Object graph
- A collection of interlinked objects which, in the context of archiving, are encoded into an archive, or created by decoding from an archive.
- Root object
- The object that is used to initiate the archiving process. Sequential archives can only have one root object, while keyed archives may have several.
- Sequential archive
- Objects in a sequential archive must be decoded in the same sequence in which they were encoded. Sequential archives are deprecated since Mac OS X 10.2 and should no longer be used.
A few more principles
- Object graphs almost never are simple tree structures, instead they contain multiple references to the same object (nodes with multiple parents), cross-references (references between nodes that do not have the same parent), and even circular references
- Despite this, a coder does not want to archive the same object multiple times, or lose itself in an infinite loop when following circular references
- The first step to solve the problem is that
NSCoder
introduces the concept of a root object: This is simply the object that is used to initiate the archiving process of an object graph. - The second step is that the coder tracks every object it encodes. If the coder is asked to encode an object more than once, the coder encodes a reference to the first encoding instead of encoding the object again.
- When encoding objects, the coder not only writes the values of instances variables into the archive, it also records the class identity of the objects (or the type of Objective-C values) and their position in the hierarchy
Keyed archives
Key names:
- Values that an object encodes to a keyed archive are individually named with an arbitrary string - the key
- Archives are hierarchical with each object defining a separate name space for its encoded values
- Therefore, keys must be unique only within the scope of the current object being encoded
- Within a single object, however, the keys used by a subclass can conflict with keys used in its superclasses
- To prevent such conflicts, a class that can be subclassed should add some sort of prefix to the name string. Examples are an API prefix (e.g. Cocoa classes use "NS"), or a bundle identifier.
Other stuff
- If a key is missing, the coder doing the unarchiving returns a default value based on the return type of the decode method that was invoked. The default values are the equivalent of zero for each data type (e.g.
nil
for objects,NO
for booleans,NSZeroSize
for sizes, etc.). To detect the absence of a keyed value, theNSKeyedUnarchiver
instance methodcontainsValueForKey:
can be invoked. - A class might encode some type of version information
Encoding
The simplest way to start the encoding process, using a single root object, looks like this:
Foobar* aFoobarObject = [...]; // get this from somewhere NSString* archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"foobar.archive"]; BOOL success = [NSKeyedArchiver archiveRootObject:aFoobarObject toFile:archivePath];
A slightly more complicated way, where you can write multiple object graphs to the same archive, is this:
NSMutableData* data = [NSMutableData data]; NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; // Customize archiver here Foobar* aFoobarObject = [...]; // get this from somewhere [archiver encodeObject:aFoobarObject forKey:@"FBObject"]; [archiver finishEncoding]; NSString* archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"foobar.archive"]; BOOL result = [data writeToFile:archivePath atomically:YES]; [archiver release];
While the encoding process is running, the coder sends the message encodeWithCoder:
to every object that is about to be archived. This message is defined by the NSCoding
protocol. When an object receives the encoding message, the object sends messages back to the coder to tell the coder which objects or values (usually instance variables) to read or write next. Example:
- (void) encodeWithCoder:(NSCoder*)coder { [super encodeWithCoder:coder]; [coder encodeObject:myName forKey:@"FBName"]; [coder encodeFloat:myFactor forKey:@"FBFactor"]; [coder encodeConditionalObject:myAppendix forKey:@"FBAppendix"]; }
Notes:
- In this example, the superclass of Foobar also supports the
NSCoding
protocol. If the superclass of a class does not supportNSCoding
, the superclass’sencodeWithCoder:
method obviously should not be called. - The
myAppendix
object is encoded conditionally, assuming thatFoobar
objects do not "own" their appendix object and hold only a weak, non-retained, reference
Decoding
Corresponding to the examples in the previous section, this is the simplest way to start the decoding process:
NSString* archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"foobar.archive"]; Foobar* aFoobarObject = [NSKeyedUnarchiver unarchiveObjectWithFile:archivePath];
The slightly more complicated way, where you can decode multiple object graphs from the same archive, is this:
NSString* archivePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"foobar.archive"]; NSData* data = [NSData dataWithContentsOfFile:archivePath]; NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; // Customize unarchiver here Foobar* aFoobarObject = [unarchiver decodeObjectForKey:@"FBObject"]; [unarchiver finishDecoding]; [unarchiver release];
While the decoding process is running, the coder sends the message initWithCoder:
to every object that is about to be unarchived. This message is defined by the NSCoding
protocol. When an object receives the encoding message, the object sends messages back to the coder to tell the coder which objects or values (usually instance variables) to read or write next. Example:
- (id) initWithCoder:(NSCoder*)coder { self = [super initWithCoder:coder]; myName = [[coder decodeObjectForKey:@"FBName"] retain]; myAppendix = [coder decodeObjectForKey:@"FBAppendix"]; myFactor = [coder decodeFloatForKey:@"FBFactor"]; return self; }
Notes:
- As usual, we assign the return value of the superclass initializer to
self
, in case the superclass implementation decides to return an object other than itself - If the superclass of a class does not support
NSCoding
, you should invoke the superclass's designated initializer instead ofinitWithCoder:
- The
myAppendix
object is not retained after it is decoded becauseFoobar
objects do not own their appendix object. If the decoding process actually returns an object, it is the responsibility of the owner to retain it.
ApplicationKit
NSWindow
An application can have multiple NSWindow objects. Each of these objects represents a top-level window.
"Key" vs. "Main Window":
- at any given time, one NSWindow can be the "Key Window"; this window processes keyboard events; most of the time it is also responsible for receiving messages from menus and panels
- at any given time, one NSWindow can be the "Main Window"; the "Key Window" is often (but not always!) the same object as the "Main Window"; if they are not the same, the "Key Window" usually has some sort of relationship to the "Main Window"
- Examples for this concept are Font or Info windows
- these are "Key Windows"
- if a font is changed this usually has an effect in the application's "Main Window"
- if the selection changes in the "Main Window", this usually causes the information in the Info key window to be updated
- a subclass of NSWindow may implement canBecomeKeyWindow and/or canBecomeMainWindow and return false
An NSWindow can have a delegate. The delegate is programmatically set by invoking setDelegate, in InterfaceBuilder by connecting the outlet delegate. Useful delegate stuff:
- makeKeyAndOrderFront displays the window and makes it the "Key Window"
- setTitle sets the window title
Summary of "Window Programming Guide for Cocoa"
The document that this chapter refers to is: ADC Home > Reference Library > Guides > Cocoa > User Experience > Window Programming Guide for Cocoa
- The two principal functions of an NSWindow object are to provide an area in which NSView objects can be placed and to accept and distribute, to the appropriate views, events the user instigates through actions with the mouse and keyboard.
- An NSWindow object is defined by a frame rectangle that encloses the entire window, including its title bar, border, and other peripheral elements (such as the resize control), and by a content rectangle that encloses just its content area.
- When it’s created, a window automatically creates two views:
- An opaque frame view that fills the frame rectangle and draws the border, title bar, other peripheral elements, and background.
- A transparent content view that fills the content rectangle.
- The frame view and its peripheral elements are private objects that your application can’t access directly.
- The content view is the “highest” accessible view in the window; you can replace the default content view with a view of your own creation using the setContentView: method.
- You add other views to the window as subviews of the content view or as subviews of any of the content view’s subviews, and so on, through the NSView addSubview: method. This tree of views is called the window's view hierarchy
- Drawing
- When a window is told to display itself, it does so by sending display... messages to the top-level view in its view hierarchy.
- Because displaying is carried out in a determined order, the content view (which is drawn first) may be wholly or partially obscured by its subviews, and these subviews may be obscured by their subviews (and so on).
- Drawing is explained in more detail in the document's "How a window is displayed" chapter.
- Modal windows are explained in detail in the document's "How modal windows work".
- Panels
- Panels are windows with an auxiliary function.
- By default panels are not released when they’re closed, because they’re usually lightweight and often reused.
- The system removes panels from the screen when the application isn't active; this can be controlled by implementing the hidesOnDeactivate method in a custom class.
- Panels can become the key window, but not the main window.
- If a panel is the key window and has a close button, it closes itself when the user presses the Escape key.
- A panel can be precluded from becoming the key window by invoking the setBecomesKeyOnlyIfNeeded: method.
- Panels can float above standard windows and other panels. Invoke setFloatingPanel: to control this behaviour.
- NSWindowController can be used to manage an NSWindow, either standalone or as a role player in the Application Kit’s document-based architecture
- Display/close/hide windows
- Display a window by invoking one of its makeKeyAndOrderFront:, orderFront:, etc. methods. This puts the window in the application's window list.
- Close a window by invoking the close or performClose: method. setReleasedWhenClosed: controls whether the window should be released.
- orderOut: hides a window without closing it. setHidesOnDeactivate: is used to hide a window when its application becomes inactive.
- isVisible returns true if the window is on the screen
- Windows can be onscreen or offscreen
- Window layering
- Onscreen windows are layered back-to-front
- Each window has a unique position in the back-to-front order: its layer
- Multiple windows are grouped together within distinct levels
- When two windows belong to different levels, the one in the higher level is always in front of the other
- When two windows belong to the same level, either one can be in front
- Windows from different applications can be interleaved
- A window's depth in the layers is determined by when the window was last accessed.
- When an inactive window is clicked or selected from the application's "Window" menu, that window, and any open utility windows, are brought to the front within the same level assigned to each of them
- When an application's icon is clicked in the Dock, or the "Bring All to Front" item is selected in the app's "Window" menu, all windows of the application are brought to the front, again within the same leve
- Utility windows are always in the same layer, the top layer, but they are visible only when their application is active.
- To change a window's position, within the same level, use orderWindow:relativeTo:, makeKeyAndOrderFront:, orderFront:, and orderBack:
- orderOut: removes a window from the screen
- Cocoa assigns each window a level depending on the window's characteristics (e.g. is it a utility panel?)
- setLevel: is used to explicitly move a window to a specific level
- Using custom levels is discouraged, except possibly for NSScreenSaverWindowLevel +1
- NSWindow has a number of predefined window levels, e.g.
- NSNormalWindowLevel (the default level)
- NSFloatingWindowLevel
- NSScreenSaverWindowLevel
- NSStatusWindowLevel
- NSModalPanelWindowLevel
- NSPopUpMenuWindowLevel
- Key/Main window
- Onscreen windows can also carry a status: main or key.
- Offscreen windows are hidden or minimized on Dock, and do not carry either status
- Onscreen windows that are neither main nor key are inactive.
- Inactive windows appear greyed out, i.e. they have no color
- Active, i.e. the main and key windows, appear with colors
- If the main and the key window are not the same, they are distinguished by the look of their title bar: The key window's "sugar drops" control buttons are colored, while the main window's are not
- Usually a window is made key when the user clicks it.
- Whenever a standard window becomes the key window, it also becomes the main window
- Whenever a panel becomes the key window, the main-window status remains with the standard window.
NSView
An NSView can represent anything that is displayed within a window. Organizationally, NSView objects are secondary to NSWindow objects. The main view of a window is termed the Content View. Most (but not all) controls are direct or indirect subclasses of NSView (e.g. NSButton, NSSlider).
NSPanel and NSAlert
NSPanel and NSAlert are used to display dialogs to the user. NSPanel is a subclass of NSWindow, while NSAlert is a helper object that performs all the required tasks to display an application-modal dialog (an NSPanel object) or a document-modal sheet (an NSWindow object) to the user.
Summary of "Cocoa Drawing Guide"
The document that this chapter refers to:
http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaDrawingGuide/index.html
TODO: Review if the summary is complete and accurate. The current summary is from my first reading of the article a few years back.
- Below every NSWindow a hierarchy of NSView objects exists; the root of the hierarchy is the content view of the NSWindow
- An NSView is a "canvas", a background on which the entire view's content is drawn
- by default the origin of the canvas' coordinate system is the bottom-left corner, with the X axis pointing to the right and the Y axis pointing up
- it is possible to define the top-left corner as the origin, with the Y axis pointing down
- in the context of its superview, a canvas is displayed only partially; the part that is displayed is like a "window" that looks onto a small area of the entire canvas
- in the coordinate system of the canvas, the method bounds provides the boundaries of the canvas' visible part
- in the coordinate system of the superview, the method frame provides the location of the canvas' visible part inside the superview
- bounds and frame describe the same rectangle, but seen from two different points of view
- if the canvas' visible part is not completely inside the superview's visible rectangle, the canvas' visible part becomes clipped; this process of clipping is repeated across the entire view hierarchy, until it becomes possible to determine which part of the canvas is really visible
- the method visibleRect returns the really visible part of a canvas
- if two views on the same hierarchy level are overlapping, they are probably not displayed correctly
- if view A is supposed to be drawn over view B (thus hiding B), then view A must be defined as a subview of B
- when an NSWindow or NSView is created, a frame rect must be specified. The rectangle is given in screen coordinates (the origin being the bottom-left corner)
- the frame of the visible screen can be determined with [[NSScreen mainScreen] visibleFrame]
- in Interface Builder
- select the menu
- in the Info window check the attribute "Auto Enable Items"
- in a convenient location (usually the NSWindow delegate), implement a method with this signature (bool)validateMenuItem:(NSMenuItem)menuItem
- a message with that signature is sent in the following order
- first to the target of each menu item
- then to each object in the responder chain of the "Key Window"
- then to each object in the responder chain of the "Main Window"
Entering a password
In Interface Builder:
- create a new NSPanel (File->New->Attention Panel)
- drag a regular text field control into the panel
- in the Info window, change the text field's custom class to NSSecureTextField
- make the field the first responder (control-drag from the NSPanel instance to the text field instance, then connect the panel's outlet initialFirstResponder)
- create the instance of some application-specific controller class and connect any of its outlets (typically the text field)
Foundation
Number and Value programming
Source for information in this section: ADC Home > Reference Library > Guides > Cocoa > Data Management > Number and Value Programming Topics for Cocoa
- The <tt<NSNull class defines a singleton object you use to represent null values in situations where nil is prohibited as a value (typically in a collection object such as an array or a dictionary). For instance, NSDictionary's setObject:forKey: method requires a non-nil object of type id.
- The NSNumber class is a subclass of NSValue that offers a value as any C scalar (numeric) type. NSNumber can be used in situations where an id object is required and a value of a primitive type such as int or bool will not suffice.
NSUserDefaults
User defaults are stored on a per-user basis. User defaults are organized into different domains; for details see the ADC programming topic "User Defaults". An application that wants to store user defaults in the "Application domain" must have a so-called bundle identifier; this identifier is configured in Xcode; it should have a form similar to a Java package name (e.g. ch.herzbube.acexpander). The user defaults are then stored in
~/Library/Preferences/<bundle-identifier>.plist
Usually an application stores defaults in 2 domains:
- in the NSRegistration domain (volatile): these are "default" settings, i.e. settings that the user has not changed; these settings are usually loaded on application startup from a .plist file that is located in the application bundle
- in the Application domain (persistent): these are settings that the user has changed (e.g. in a Preferences dialog), or that have no corresponding entries in the NSRegistration domain (e.g. window positions)
- the Application domain is not really called "Application", the domain name actually is the bundle identifier
The domain NSGlobalDomain (persistent) contains settings that are valid for all applications. An example for this is the user's language. A PreferencePanel could use domain to store its values.
The domain NSArgumentDomain (volatile) contains settings that have their origin on the command line (i.e. when the application is launched on the command line)
Every language has its own Language domain (volatile) that contains settings specific to that language. The Language domain is not really called "Language", the domain name actually is the language identifier.
Domains are searched for values in a certain order. The order can be changed through [NSUserDefaults setSearchList]. As soon as a value has been found, the search stops. The default search order is this:
- NSArgumentDomain
- Application domain
- NSGlobal domain
- Language domain
- NSRegistration domain
Information property lists
The file
Content/Info.plist
inside a bundle is a so-called "Information Property List File". It contains configuration information about all supported platforms. It is possible to have several .plist files, each one for a different platform. The files are named like this:
Info-<platform>.plist
A platform-specific .plist file has priority over the generic .plist file.
Every .lproj directory may contain a file named
InfoPlist.strings
That file is used for localization and contains all translateable strings. If a key is not found, its value is taken from the Info.plist.
Keys can be accessed programmatically with
[NSBundle objectForInfoDictionaryKey]
A reference to all valid keys can be found in a sub-chapter of this document:
http://developer.apple.com/documentation/MacOSX/Conceptual/BPRuntimeConfig/index.html
System services
Documentation
http://developer.apple.com/documentation/Cocoa/Conceptual/SysServices/index.html
Providers
Applications that want to provide system services must be built with the .app extension and installed in the Applications folder (or one of its sub-folders), within one of the following "File-system domains":
- System domain: /System/Applications
- Network domain: /Network/Applications
- Local domain: /Applications
- User domain: ~/Applications
To create a standalone service (i.e. no application), it must be built with the .service extension and installed in the Library/Services folder, again within one of the File-system domains listed above.
The service provided by the application is defined in the application's Info.plist. See the ADC documentation for details.
The list of available services on the computer is built each time a user logs in. After installing your service in either an Applications or Library/Services directory, you need to log out and back in again before the service becomes available. You can force an update of the list of services without logging out by calling the following function:
void NSUpdateDynamicServices(void)
Running applications are not affected, but applications launched afterwards get the new list of services.
Clients
An application that wants to use services must
- have a "Services" menu
- register objects for the services it wants to use (for instance, a table whose rows correspond to files may wish to register for the "ShowInfo" service of the Finder)
Registration occurs through
[NSApplication registerServicesMenuTypes]
All NSResponder objects of an application that originate in the ApplicationKit register themselves automatically. Subclasses of NSResponder must manually register, e.g. in the initializer, or in the initialize method.
When the user opens the "Services" menu, menu items are enabled/disabled according to the following scheme:
- the message validRequestorForTypes is sent once for every possible service type (i.e. menu item) to all NSResponder objects, until one of them responds to the message
- the method implementation should be very fast as the method may be called a lot
- an object whose validRequestorForTypes method gets invoked should first check if it can do anything with the given service type, second if its internal state allows the menu item to become enabled (e.g. is a table row selected)
When the user selects an entry from the "Services" menu
- the first responder object in the responder chain that responds to validRequestorForTypes is used for subsequent operations
- the responder object is sent a message writeSelectionToPasteboardOfTypes; the method implementation must pass the information that the service provider needs for performing its service operation to the pasteboard specified in the method invocation
- if the service provider returns a value, the responder object is sent a message readSelectionFromPasteboard
Note: A service can be programmatically executed by invoking NSPerformService
Java
For historical reasons, this chapter contains some notes about Cocoa/Java.
Selectors
Instantiate an NSSelector in one of the following ways (useful e.g. for registering with a notification centre):
new NSSelector("foobarMethodName", new Class[] {}); new NSSelector("foobarMethodName", new Class[] {NSNotification.class});
If the method that the NSSelector represents has no arguments, the second constructor argument must not be null (even though the Apple docs for NSSelector say so), otherwise an exception is thrown.
Constructors required by the Java bridge
Sometimes a subclass of a Cocoa class must implement a specific constructor to prevent the Java bridge from dying at application runtime. I have encountered the following problems:
The following constructor is required if the Java bridge interrupts application startup and with a message that says that there is no constructor that matches initWithFrame:
public class FooBar extends NSView { public FooBar(NSRect rect) { super(rect); } }
The following constructor is required if the Java bridge interrupts the application startup with an obscure error message that looks something like this: "AppKitJava: uncaught exception OBJCJava_RUNTIME_EXCEPTION".
protected class FooBar extends NSBox { protected FooBar(NSCoder decoder, long token) { super(decoder, token); } }
Various
Debug output
The equivalent of print statements scattered throughout the code to provide debug output is NSLog:
// Print an NSString NSLog(loginURL); // String formatting for decimals NSLog(@"about to check %d servers", [serverList count]); // String formatting for strings NSLog(@"Connection failed! Error - %@ %@", [error localizedDescription], [[error userInfo] objectForKey:NSErrorFailingURLStringKey]);
Questions
No main nib
My Xcode project does not have a main nib file. How do I get Cocoa to alloc/init an instance of one of my project classes?
Usually I do this in the main nib:
- in InterfaceBuilder I define one of my project classes on the "Classes" tab
- then create an instance of the project class on the "Instances" tab
- then control-drag the instance icon on the "File's Owner" icon, also on the "Instances" tab
- this shows the "Connections" page in the Inspector window
- choose the "delegate" outlet and click the "connect" button
This sequence makes sure that an instance of my project class is created when the nib file is loaded by Cocoa's automagic. This causes the awakeFromNib() method to be invoked. The project class instance is further configured to be the delegate for NSApp, which is the shared instance of NSApplication that Cocoa automagically creates when the application launches. Being the delegate causes NSApplication to invoke several methods such as applicationDidFinishLaunching().
Now what happens when there is no main nib file? The only way I have found to define one of my project classes to be instantiated is to set the NSPrincipalClass property in the application's Info.plist file (can also be set on the "Properties" page of the dialog that opens when you double-click a target in Xcode). There are 2 problems with this, though:
- My project class needs to subclass NSApplication, which is something I don't really want to do
- The project class name becomes exposed to whoever looks at the application bundle with NSBundle::principalClass()
A possible solution that needs investigation is to implement a class method initialize: The Objective-C runtime system creates a class object for every class, then sends the initialize method to that class object. In theory, an implementation of the initialize method would therefore be able to alloc/init the desired object.