4 Разработка Mac-приложений на примерах. NSURL, NSXMLDocument, NSThread, NSPopUpButton, NSTableView

1. Создадим новый проект:
  • Product Name: Global Currency
  • Company Identifier: com.yourdomain
  • Class Prefix: Your Initials (for consistency we use BTS throughout this book)
Дополнительно включим Use Automatic Reference Counting

2. В MainMenu.xib создадим интерфейс:



3. AGAppDelegate.h


#import <Cocoa/Cocoa.h>

@interface AGAppDelegate : NSObject <NSApplicationDelegate>

/*
 Global Currency defines
 */
// The European Central Bank exchange rate XML File
#define D_ECB_RATES_URL @"http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml"
// The path to the last update time in the XML tree
#define D_XML_PATH_TIME @"/gesmes:Envelope/Cube/Cube"
// The path to the exchange rates in the XML tree
#define D_XML_PATH_RATES @"/gesmes:Envelope/Cube/Cube/Cube"
// The popup menu title
#define D_SELECT_CURRENCY @"Select Currency"
// The keys for the XML attributes
#define kTimeKey @"time"
#define kCurrencyKey @"currency"
#define kRateKey @"rate"

@property (assign) IBOutlet NSWindow *window;

/*
 Define the GUI elements
 */
@property (assign) IBOutlet NSTextField *mLastUpdateTimeTextField;
@property (assign) IBOutlet NSProgressIndicator
*mLastUpdateTimeProgressIndicator;
@property (assign) IBOutlet NSTextField *mValueToConvertTextField;
@property (assign) IBOutlet NSPopUpButton
*mCurrencyToConvertPopUp;
@property (assign) IBOutlet NSTableView
*mConvertedCurrencyTableView;

/*
 Define the Member elements
 */
@property (strong)  NSString *mLastUpdateTime;
@property (strong)  NSMutableDictionary *mCurrencyRates;

/*
 Methods we need to implement
 */
// Define the method to get the exchange
// rates and parse them.  It takes an object
// as an argument because it will be run on
// a background NSThread
- (void) getExchangeRatesFromECB: (id)a_object;

// Handle the Pop Up Menu selection
- (IBAction)selectToCurrency:(id)a_sender;

@end

Аннотация @property означает, что мы хотим чтобы компилятор создал setter и getter методы.

Note that the outlets are declared as assign rather than weak. Typically under Automatic Reference Counting (ARC) they would be weak (since they are owned by their super view) but we can also declare them assign if we want to be compatible with earlier OS versions that don't support ARC.

Notice the @property declaration for mLastUpdateTime and mCurrencyRates is strong rather than assign. This is required because our project uses Automatic Reference Counting (ARC). Because ARC is going to manage our program's object lifetime use, we need to tell ARC that we want to retain a copy of these objects. If we fail to provide this hint, then ARC will release the objects as soon as we create them (this means their retain count will be zero and they will be deallocated at an unknown time in the future by the memory manager), which would leave us with what is known as a dangling pointer that will cause our program to randomly crash based on how often memory is cleaned up.

4. AGAppDelegate.m

#import "AGAppDelegate.h"

@implementation AGAppDelegate

/*
 Define the GUI elements
 */
@synthesize mLastUpdateTimeTextField;
@synthesize mLastUpdateTimeProgressIndicator;
@synthesize mValueToConvertTextField;
@synthesize mCurrencyToConvertPopUp;
@synthesize mConvertedCurrencyTableView;

/*
 Define the Member elements
 */
@synthesize mLastUpdateTime;
@synthesize mCurrencyRates;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    // Insert code here to initialize your application
    // Create a mutable NSDictionary so that we can
    // save the exchange rate information
    mCurrencyRates = [[NSMutableDictionary alloc] init];

    // Modify the Progress Indicator
    // so that it is not visible when
    // it is not animating
    [mLastUpdateTimeProgressIndicator
     setDisplayedWhenStopped:NO];
    // Start the Progress Indicator spinning
    // It will spin until the XML file is
    // downloaded and parsed.  It should be
    // very brief
    [mLastUpdateTimeProgressIndicator
     startAnimation:self];
    
    // Start the background thread to download
    // the XML file
    [NSThread detachNewThreadSelector:@selector(getExchangeRatesFromECB:)
                             toTarget:self withObject:nil];
}

/*
 This method will download the XML file from
 the European Central Bank and parse it into
 member variables.
 Input:
 a_object - the protocol for NSThread requires
 this method to take an arbitrary
 object as an argument.  It will
 not be used.
 Output:
 As a side effect, the member variables mLastUpdateTime
 and mCurrencyRates will be populated.
 */
- (void) getExchangeRatesFromECB: (id)a_object
{
    // Create a URL object using a string
    NSURL *l_URL = [NSURL URLWithString: D_ECB_RATES_URL];
    
    // Create an XML Document object and
    // initialize it with the contents of the URL
    // This downloads the contents of the URL
    // from the internet
    NSXMLDocument * l_xmlDocument = [[NSXMLDocument alloc]
                                     initWithContentsOfURL:l_URL
                                     options:NSXMLDocumentTidyXML
                                     error: Nil];
    
    // If the XML document was
    // successfully retrieved we
    // can parse it
    if (l_xmlDocument)
    {
        // Declare an array object that we
        // can use to examine nodes
        NSArray *l_nodes;
        // Create an array of nodes at the path
        // where we can find the time attribute
        l_nodes = [l_xmlDocument
                   nodesForXPath:D_XML_PATH_TIME error:Nil];
        // Extract the time attribute from the node
        mLastUpdateTime =
        [[[l_nodes objectAtIndex:0]
          attributeForName: kTimeKey] stringValue];
        // Create an array of nodes at the path
        // where we can find the currency and rate
        // attributes
        l_nodes = [l_xmlDocument
                   nodesForXPath:D_XML_PATH_RATES error:Nil];
        // Declare some working variables
        NSString *l_currency;
        NSString *l_rate;
        NSXMLElement *l_element;
        // Loop over all the currency and rate nodes
        // and look at each element
        for (l_element in l_nodes)
        {
            // Extract the currency attribute into a NSString
            l_currency = [[l_element attributeForName:
                           kCurrencyKey] stringValue];
            // Extract the rate attribute into a NSString
            l_rate = [[l_element attributeForName: kRateKey]
                      stringValue];
            // Add the rate to the mutable NSDictionary using
            // the currency as the dictionary key
            [mCurrencyRates setObject:l_rate forKey:l_currency];
        }
    }
    
    // Because this method will execute on a
    // background thread we need to invoke
    // another method, on the main thread, to
    // update the GUI
    [self
     performSelectorOnMainThread:@selector(populateGUIOnMainThread:)
     withObject:nil waitUntilDone:NO];
}

- (void) populateGUIOnMainThread: (id) a_object
{
    // If the last update time is nil its because
    // the XML file could not be retrieved or
    // parsed so there is nothing to
    // display
    if (mLastUpdateTime)
    {
        // Display the time in the GUI
        [mLastUpdateTimeTextField setStringValue:mLastUpdateTime];
        // Remove everything from the Pop Up Menu
        [mCurrencyToConvertPopUp removeAllItems];
        // Add an item "Select Currency" to the Pop Up menu
        [mCurrencyToConvertPopUp addItemWithTitle:D_SELECT_CURRENCY];
        // The currency rates returned in the XML
        // file are not sorted.  Create a new NSArray
        // sorted in alphabetical order
        NSArray *l_sortedKeys = [[mCurrencyRates allKeys]
                                 sortedArrayUsingSelector:@selector(compare:)];
        // Add the currencies to the Pop Up Menu
        [mCurrencyToConvertPopUp addItemsWithTitles: l_sortedKeys];
    }
    // Stop animating the progress indicator.
    // Animation was started just before the
    // background thread to download the XML file
    // was invoked
    [mLastUpdateTimeProgressIndicator stopAnimation:self];
}

// This method will be invoked whenever a currency
// code is selected from the Pop Up Menu
- (IBAction)selectToCurrency:(id)a_sender
{
    //NSBeep();
    // Send a message to the table view telling it that
    // it needs to reload its data
    [mConvertedCurrencyTableView reloadData];
}

@end

NSDictionary делает себе личную копию ключа, поэтому объект используемый в качестве ключа должен поддерживать копирование (NSString). Если нужно обойти это ограничение, то надо использовать NSMapTable. Чтобы получить список всех ключей в NSArray надо послать сообщение allKeys объекту NSDictionary (или NSMutableDictionary).

Главный поток управляет обработкой событий (mouse, keyboard, и т. д.) и GUI.
Если главный поток занят какой-нибудь длинной задачей это останавливает обработку событий, и Mac OS покажет spinning multicolored cursor и приложение будет помечено как не отвечаемое. Фоновый поток убивается когда закрывается приложение.

5. ПКМ перетягиваем наш App Delegate object в MainMenu.xib на компоненты интерфейса.

6. ПКМ перетягиваем pop-up menu на App Delegate и выбираем selectToCurrency:.

7. Добавим новый класс BTS_GCTableViewDelegate с Subclass равным NSObject

BTS_GCTableViewDelegate.h

#import <Foundation/Foundation.h>
#import "AGAppDelegate.h"
#import <Foundation/Foundation.h>

// Create defines that will be used
// to identify the column in the
// table view
#define kBTSGCCurrency @"currency"
#define kBTSGCValue @"value"

@interface BTS_GCTableViewDelegate : NSObject



@end

BTS_GCTableViewDelegate.m

#import "BTS_GCTableViewDelegate.h"

@implementation BTS_GCTableViewDelegate

{
    // Create a reference to the AppDelegate so
    // that we only need to look it up one time
    AGAppDelegate *mAppDelegate;
}

/*
 This method is invoked automatically
 when the object instance is revived
 from the .xib file
 This is where we do any initialization
 needed by the .xib object
 */
-(void) awakeFromNib
{
    // Get a reference to our AppDelegate
    // object and save it for later use
    mAppDelegate = [NSApp delegate];
}

/*
 This method is invoked automatically
 when the table view GUI element needs
 to know how many rows it has to
 display
 All dataSource objects must implement
 this method.
 */
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
    // Ask the AppDelegate to lookup the mCurrencyRates
    // member variable and then ask it for the number
    // of objects it contains.  That is the number of
    // rows in the table view
    return mAppDelegate.mCurrencyRates.count;
}

- (id)tableView:(NSTableView *)a_tableView objectValueForTableColumn:(NSTableColumn *)a_tableColumn row:(NSInteger)a_rowIndex
{
    // Retrive the identifier for the row.  These are
    // set in the .xib file
    NSString *l_identifier = [a_tableColumn identifier];
    // If the desired column is the currency, then
    // return the value for the currency code
    if ([kBTSGCCurrency isEqual: l_identifier])
    {
        return [mAppDelegate.mCurrencyRates.allKeys
                objectAtIndex:a_rowIndex];
    }
    // If the desired column is the value, then
    // return the value for the value for that currency code
    if ([kBTSGCValue isEqual: l_identifier])
    {
        // Get the value to convert from
        // the AppDelegate
        double l_valueToConvert =
        [mAppDelegate.mValueToConvertTextField doubleValue];
        // Get the currency code of the value
        // to convert from the AppDelegate
        NSString * l_selectedCurrency =
        [mAppDelegate.mCurrencyToConvertPopUp
         titleOfSelectedItem];
        // Get the exchange rate to Euros for
        // the value to convert from the AppDelegate
        double l_rateFrom =
        [[mAppDelegate.mCurrencyRates
          objectForKey:l_selectedCurrency] doubleValue];
        // Get the currency codes from the AppDelegate
        // and look up the currency code for the
        // requested table row
        NSString *l_toCurrency =
        [mAppDelegate.mCurrencyRates.allKeys
         objectAtIndex:a_rowIndex];
        // Get the exchange rate to Euros for
        // the row from the AppDelegate
        double l_rateTo =
        [[mAppDelegate.mCurrencyRates objectForKey:l_toCurrency]
         doubleValue];
        // Calculate the converted value.
        // First by converting the from currency to
        // Euros, then by converting the result
        // from Euors to the desired currency
        double l_euroValue = l_valueToConvert / l_rateFrom;
        double l_finalValue = l_euroValue * l_rateTo;
        // Return the result as an NSString
        return [NSString stringWithFormat:@"%f",l_finalValue];
    }
    
    return nil;
}

@end


  • nil (all lower-case) is a null pointer to an Objective-C object.
  • Nil (capitalized) is a null pointer to an Objective-C class.
  • NULL (all caps) is a null pointer to anything else.

8. В MainMenu.xib перетягиваем компонент Object на панель объектов и в Custom Class меняем для него Class на BTS_GCTableViewDelegate

9. Три раза щелкаем на таблицы чтобы выделить NSTableView (первый раз выделяется NSScrollView).  И ПКМ перетягиваем его на объект BTS_GCTableViewDelegate. В появившемся окне выбираем dataSource. А затем еще раз, только выбираем delegate.

Объект delegate реализует методы протокола NSTableViewDelegate, которые вызываются когда table view нужно выполнить такие дейтсвия как информирование delegate о том что выделение было изменено или спросить у delegate должен ли столбец быть выделен.

Объект dataSource реализует методы протокола NSTableViewDataSource, которые вызываются автоматически, когда table view необходимо отобразить данные.

10. Три раза щелкаем на таблицы чтобы выделить NSTableView (первый раз выделяется NSScrollView).  И в Identity Inspector, для первого столбца ставим Identifier значение currency, а для второго столбца value