KB_5K0077: ISEview – An iPhone/iPad app for Xiotech ISE Cortex Monitoring. Description: This knowledgebase article summarizes the ISEview application and in the process will hopefully give you pointers on how to modify the application for your own purposes. Full sources are available with the Xiotech Cortex SDK. What is ISEview? ISEview is a portable monitoring tool for Xiotech Emprise 5000 (ISE) storage arrays. It has been designed specifically for the iPhone and iPad as two separate applications with a common look and feel. The tool is designed to provide a quick status view of up to 16 different ISEs. The ISEs can be local or remote…the only requirement is that they are accessible over whichever LAN connection the iPhone or iPad has access to (either direct via WiFi, over the internet via 3G, or via VPN.) ISEview allows monitoring of both status (environmental, states of FRU components, and component info) as well as performance at both the overall ISE level and per controller. It also features an API Browser that allows potential developers to view part of the Restful XML responses obtained from Cortex on an ISE as well as a simple analysis screen that summarizes and ISE in an easy to read fashion. THE PROGRAM: ISEview was designed in somewhat patchwork fashion over a 3 week period as the two developers were also learning the SDK and Objective C at the same time. The following books provided good insight during this process, with excellent examples that helped us along the way of gathering XML data and building an in-memory XML tree for browsing purposes: The iPhone Developer’s Cookbook: Building Applications with the iPhone SDK: Erica Sadun Sams Teach Yourself iPhone Application Development in 24 hours: John Ray and Sean Johnson Also, iPhone requirements are 3.1.3, iPad requires compiling with SDK 3.2. The ISEview app is best thought of as having three ‘viewing’ levels. These are referred to as the Top level, the MidView level, and the API Browser level. The following is an image of the Top Level. The top level simply shows the various ISEs that the App has been configured to watch. The IP addresses and ISE name are displayed here. This level is also where you use the normal ‘edit’/”“/”+”/’done’ logic to remove and add new ISEs to the list as shown below: The logic is designed to show you immediately upon entering an IP address what the Serial number and alternate IP address is. This method also prevents you from entering a bad IP address and then having to delete it later when you determine that it isn’t correct. The MidView level is where the action is. Performance information is updated in this screen roughly once every 2 seconds. The ISE and MRC performance for IOPS and MB/S are shown via both a ‘log10’ line display as well as raw data numbers. The log10 approach was taken to give a better feeling for relative change no matter if you are doing high IO or low IO. This avoids the need for doing axis scaling. Here also you will see the status of all the components update dynamically (updates are upon entry and then every 120 seconds after that…the assumption being that component states just don’t change that often). Below is the MidView. Also, the MidView sports active buttons. Any of the MRC, Battery, Power Supply, or Datapac buttons can be pressed to get more info. Below Datapac2 was pressed to see why it was yellow. In this case the handle was left open and needs to be screwed down. Pressing the About button will return the following: And pressing the Analyze button will generate something like the following: The analyze screen is intended to provide a high level view into the system (i.e. rather than just jamming some qdepth and latency numbers it combines a lot of performance information from various sources and tries to reduce that to concepts like ‘idle’, ‘busy’, ‘too busy’, and ‘balanced’. Some wiggle room there for improvement and enhancements. This screen is also kept ‘text only’ so that it is easy to copy and past the results into an email if you so choose. NOTE: any screen can be copied ‘hot’ and then emailed on an iPhone or iPad by simply pressing the ‘square’ button at the bottom of the device (shown at the bottom of the picture above) and the top ‘power on/off’ button at the same time and then releasing them. The picture goes in your normal pictures folders from where you can then email it. One of those undocumented iPhone features that carried into the iPad. Finally, the API Browser View was something of an afterthought, even though it was our first ‘view’ that we played with. Since all the data was gathered into memory by this logic, we decided to keep the logic in the program code and allow users to browse through the restful interface ‘/all’ option. This option contains much of the infrastructure, status, and performance data that you will need for programming, although often it is easier on the system if you just ask for what you need (i.e. as the MidView does to get performance information at a fast rate.) Pressing the API Browser button will cause the button to ‘depress’ and after a few seconds you will be taken to a table view screen that shows what level of the Restful hierarchy you are at (in this case you are in controller/1 under the controllers section) along with a backpointer to take you up a level in the hierarchy. There are also lines that end with a > to indicate that pressing on this line will take you down the hierarchy. Very simple actually. So, the displays are all neat, but the following section will delve into the guts of the iPhone/iPad application code itself and that is even neater (now that we have it working). But, before jumping into that, here is a snippet of documentation from the STAT5000 document that might help you to better get a feel for how we ‘grab’ the correct information via Restful. If you have an ISE available right now do the following: 1) From a web browser, enter either one of your ISEs IP addresses into the web browser address bar followed by a /query (i.e. 10.10.20.30/query). 2) You will get something that looks like the following: <?xml version="1.0" encoding="ISO-8859-1" ?> - <array self="http://10.20.54.32/storage/arrays/1BC10089"> <status value="0" string="Operational" /> <globalid>1BC10089</globalid> <id>20000014C367348C</id> <name>ISE</name> <serialnumber>1BC10089</serialnumber> <model>ISE1400</model> <vendor>XIOTECH</vendor> ...and so on… 3) Cut and paste the http string into the browser and press return. You will be requested for the Adminstrator login and password. NOTE: the application has the administrator default password hard coded into it. 4) Take other http:// lines that you see in the resulting page and try looking at those Now things will make more sense when you look at what this code does (i.e. it is a big parsing, analysis, and display engine for some of this data.) Also, ISEview is designed without any complex schemas or data structures. You will see a pretty flat structure when you look at the code and that was done entirely to keep things readable and ‘safe’. No pointers, strings are all essentially copy safe, no complex conditional code that you have to reread a number of times, etc. Also, this program is safe because it is ONLY monitoring an ISE…there is no configuration code active in this example at all (i.e. no volume deletes). During the entire development and subsequent testing of ISEview not a single ‘impact’ was ever seen on an ISE due to running this program. Oh yes, the iPad. Everything above applies to the iPad application. We just scale things up when needed and take advantage of additional lines in other screens to see more of the data with less scrolling. Future versions of the iPad will have more detail and the iPad has a faster processor so we will take advantage of that as well. Here is a screen capture of the iPads midview: As you can see…plenty of real-estate left. Now for the code. Sources for ISEview are currently about 3400 lines of code. The code that is provided with the SDK might vary a bit from this document’s contents but hopefully the gist will carry forwards. The following section will go through the code from top to bottom and after that section will be one final section that summarizes our first impressions of working with the iPhone SDK (at least the positive ones). The Code: This section is presented in overview fashion, first summarizing the files themselves that compose the code, then describing the general program flow for the entire application, and then discussing the various areas of the code that had a higher learning curve (i.e. were harder to design). The files: The ISEview code is composed of these source files: Classes TreeNode.h TreeNode.m XMLParser.h XMLParser.m db_build.h db_build.m MyAppDelegate.h MyAppDelegate.m UIimage+Resizing.h UIimage+Resizing.m MyListController.h MyListController.m ISEkclass.h ISEkclass.m iseCfgFromXmlQuery.h iseCfgFromXmlQuery.m iseCfgFromXmlQuery.xibMidView.h MidView.m MidView.xib TreeBrowser.h TreeBrowser.m analyzeView.h analyzeView.m analyzeView.xib aboutView.h aboutView.m aboutView.xib Other Sources main.m ISEview_Prefix.pch - Defines the TreeNode object which is extensively by the XML parser Methods used for creating, searching, adding and deleting a TreeNode object Defines the XMLParser stack array Methods that parse XML and store the results in TreeNode and ISE structures Defines the ISE structure used for storing information retrieved for an ISE Contains the sharedInstance definition for the ISE structure Defines the window and view controllers used by ISEview Creates the initial window, table view controller, and navigation controller Routines available for manipulating and resizing images Routines available for manipulating and resizing images Defines the routines for displaying and managing the top view Contains the Methods used for displaying and managing the top view Defines the object used to represent an ISE that is known to ISEview Methods used to create and init and ISEkclass object Defines the UI objects ad routines used to discover and ISE Methods used for discovering an ISE on the network The ISE discovery interface builder file Responsible for displaying information about an ISE. It also instantiates an NSTimer and NSoperationQueue to enable retrieval of data from the ISE Methods to support managing the display of information for an ISE Interface builder file for the MidView Defines the TreeBroswerController (UITableViewController) for displaying the API view of XML data Methods for dealing with the API viewer table view Defines objects associate with the AnalyzeView Methods used for displaying the analyzeView Interface builder file for analyze view Defines objects used for the aboutView Methods used in the aboutView UI Interface builder file for the aboutView nothing to see here so move along. It is important though Prefix header for table view And these files compose the rest of the application bundle: Resources gray2_btn.png blue2_btn.png red2_button.png green2_btn.png yellow2_btn.png 7_BLUE.png 6_ORANGE.png 5_GREEN.png 4_RED.png 3_YELLOW.png 2_GRAY.png green.jpg ISEs.plist info.plist Icon.png Default.png - For uninitialized buttons background not used For button background displaying a failure state For button background displaying an optimal state For button background displaying a warning state Blue X not currently used Orange X not currently used Green X used in top view Red X not currently used Yellow X not currently used Gray X not currently used Used for screen background in MidView persistant data describing ISEs properties list file required by apple application icon splashscreen Resources-iPad iseCfgFromXmlQuery.xib MidView.xib analyzeView.xib aboutView.xib - interface builder ipad file interface builder ipad file interface builder ipad file interface builder ipad file Frameworks UIKit.framework Foundation.framework CoreGraphics.framework - Apple SDK Apple SDK Apple SDK Products ISEview.app iphone application ISEview-iPad.app - ipad application The Code Flow: The following is a high level flow description for the ISEview code. 1) Init/Start a. ISEview starts by creating a navigation controller and setting up the top table view in MyAppDelegate.m. b. From there initialization continues in MyListController.m where data is read from the properties list file ISEs.plist and is used to populate ISEview’s ISE table. ISEs.plist contains static data defining the ISE that ISEview is configured to monitor. i. It contains an array of dictionary items which describe an ISE in enough detail such that it can be accessed over the network using the CorteX Restful interface. This includes the IP address of both MRCs, the ISE name, and the ISE serial number. ii. Additional query URI are defined in the ISEs.plist file for convenience as well. 2) Now the top level ISE table should be displayed. At this point the runloop monitors the application for the users next action which may consist of discovering and adding another ISE, deleting an ISE, or changing the order of the ISE in the table. a. If the add ISE button (“+”) is pressed a modal view controller is created and displayed. This dialogue allows the user to enter an ISEs hostname or IP address and discover a new (or an already existing ISE). Upon successful discovery the ISE will be added to the ISEs.plist file and display in the top table view. b. If the Edit button is pressed, the normal iPhone logic for removing an entry is used (i.e. each entry will show an ‘-‘ and then when selected a DELETE button will appear that the user can then select. This logic is in [KARL]. This is also where the user can change the order. c. If just the entry itself is pressed the user is immediately moved to the Midview. 3) Midview - An ISE has been selected from the top view table which brings up midView. midView performs a number functions which allow the display of the specific RESTful data for the given ISE. a. The first item it creates is an operation queue. The operation queue allows RESTful queries to be performed in the background, but synchronously to avoid the possibility of multiple XML parsing operations being performed simultaneously. The parser used in ISEview is a singleton and there can be only one XML object parsed at any given time. b. Next, it starts an NSTimer which is used to control the periodic retrieval and display of data for the ISE. Performance data is retrieved every two seconds and the status data is fetched once a minute. Performance data drives the real-time MRC and ISE IOPS and MB/S bars on the screen and is also used when analyzing the ISE. Status data is used to display the colored button states and the content when the status buttons are pressed. The first time thru both the performance and the status data is fetched immediately. c. As previously stated the performance data for an ISE is fetched every two seconds. This is accomplished by placing the retrieval job on the NSoperation queue. This job once executed will i. fetch the XML from the ISE, ii. parse it into usable data, and iii. call a method to actually display the data. d. The status data retrieved every 60 seconds is handled in the same way as the performance data. e. In parallel with the data retrieval, the runloop monitors user actions. Pressing one of the MRC, PS, BAT, or Datapac buttons will bring up an alert view with the relevant data associated with that object. The About, Analyze, and API Viewer buttons are made available for selection and each have their own view (see file list). When selected special care was taken to pause the operation queue and prevent the display of data while that view is not visible. Since alert views sit on top of the midView we do continue to allow performance data to update in the image below the popup. f. If the user selects to go back to the top view from the midView the operation queue will be destroyed and the NStimer removed. 4) AboutView - About view simply displays the version number and information about the ISEview program 5) AnalyzeView - The analyze view performs a high level analysis of the data retrieved for a given ISE and displays it in an easy to read fashion. 6) APIbrowser - The API browser performs an 'all' query for a given ISE, gathers and parses the results (building a tree structure in the process), and then allows the user to browse up and down an automatically constructed table view of this data. a. Invoking this operation can take a few seconds so special care was done in handling actions after this button is pressed. i. First, a query for 'all' is placed into the operation queue where it will be handled in the order it was received. If there are other jobs still pending in the queue they will be handled first. ii. Then the API browser button is disabled so that it is impossible to press it more than once. b. Once the API browser job is run from the queue, it will fetch the data from the ISE, parse it, and finally call a method to display it in a new table view. c. Pressing back to go back into the top view after pressing API browser but before the API browser view comes up will cause the operation to cancel. The neat stuff: In more detail, the above code flow can be broken down into the following main sections…each of which took a bit of research and trial and error to get right: Gathering URL data (how we get all that Restful XML data) XML Parsing (how we decided to parse the XML data…yes there are multiple ways) XML tree node logic (how we keep info around to view hierarchically) Data Structure Construction (how we keep info around to analyze) Timer logic (how to do things in the background) Alert View (how to show data quickly upon a button press) Add/remove/sort ISE list logic o Dynamic query while adding an ISE (validate on the fly) Custom Button Creation/Usage (i.e. making things look nice) Dynamic query while adding an ISE (validate on the fly) The following sections will discuss these in more detail. Gathering URL data This is what iPhones and iPads are made for. Unfortunately there are several methods that can be used and of course some are better than others. The logic we ended up using was the NSURLRequest method as shown in the following code snippet: [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; NSError *error; NSURLResponse *response; NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:12]; NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if(error) { [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; [pool drain]; return nil; } NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; The advantage to this logic is that we could both use it in a background mode, but also specify timeouts to better handle error conditions. This logic is all in file XMLParser.m. XML Parsing Of course, if gathering URL data is high on an iPhones list of things to do, then close behind is XML parsing. Luckily there are only 2 ‘common’ ways of doing this. A SAX (Simple Api for Xml) parser or a DOM (Document Object Module) parser. We decided on a hybrid approach, using SAX parser (NSXMLParser), mostly because it is included as the default with the iPhone SDK, but building our own tree structure and using that in some cases (i.e. to browse the API structures). SAX parsers are indirect callback parsers (i.e. the parser walks through the entire XML file and ‘calls’ routines (that you write) whenever it starts an element, has data that is part of an element, or sees the end of an element…i.e hitting <element>data</element> would generate three separate calls. DOM parsers just stick all the XML in a buffer/tree and let you grope through it picking out what you want after the fact. The logic for XML Parsing is in the module named XMLParser.m and much of the logic resides in the three callback methods (didStartElement, foundCharacters, and didEndElement) which in turn create the node tree and stuff our data structures. Most of our logic then simply uses the data that is built up from these data structures, although we did include an example of a DOM search in our code as well. This is in the logic that is used to look at the query that comes back in response when you are entering a new ISE’s IP address. There we know we just want the ip addresses and the ISE name and SN so we ‘grope’ for those in the returned data buffer. This is covered in more detail later on in this document. XML TreeNode Logic Got all that XML data and want to let people ‘view’ it? The treenode logic comes next. - (TreeNode *) init { if (self = [super init]) { self.key = nil; self.leafvalue = nil; self.parent = nil; self.children = nil; } return self; } + (TreeNode *) treeNode { return [[[self alloc] init] autorelease]; } The logic used is similar to other treenode implementations, although it is based heavily on code examples from Erica Sadun’s book “The iPhone Developers’ Cookbook”. Data Structure Construction (how we keep info around to analyze) Examples are included in the code for both normal C data structure usage as well as using shared instances to update an objects NSString and NSObject structures. We chose the C data structure usage simply because it was MUCH less typing and mapped easily into our existing data structure logic. Example if(ise[cur_ise].parse_level!=LVL_DISKS && ise[cur_ise].parse_level!=LVL_POOL) { if([elementName isEqualToString:@"medium"]) { elementName=@"medium: Datapac"; ise[cur_ise].parse_level=LVL_DP; NSString *wh_cn=[attributeDict objectForKey:@"self"]; NSRange sl = [wh_cn rangeOfString:@"/" options: (NSBackwardsSearch)]; if(sl.location!=NSNotFound) { NSString *wh_sub=[wh_cn substringFromIndex:sl.location+1]; elementName = [elementName stringByAppendingString:@" #"]; elementName = [elementName stringByAppendingString:wh_sub]; SAFECOPY(ise[0].curr_attrib,[elementName UTF8String]); on_dp=( [wh_sub isEqualToString:@"2"] ); } } } Timer logic (how to do things in the background) Setup the queue and start the timer NSOperationQueue *_updqueue = [NSOperationQueue new]; self.updqueue = _updqueue; [_updqueue release]; [updqueue setMaxConcurrentOperationCount:1]; self.updtimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target:self selector:@selector(updateMidView:) userInfo:nil repeats: YES]; . . . Then submit jobs to the queue based on modulo timerCount intervals // fast timer performance for the perf data if ((timerCount % 2) == 0) { // fetch performance data NSString *perfurl = [whichIse perfurl]; NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadXMLPerfDataFromQueue:) object:perfurl]; [updqueue addOperation:operation]; [operation release]; } Alert View (how to show data quickly upon a button press) else if ([button.currentTitle isEqual:@"DP1"] || [button.currentTitle isEqual:@"DP2"]) { i=( [button.currentTitle isEqual:@"DP2"] ); alestr = [[NSString alloc] initWithFormat: @"%@\n%@: %@\n%@: %@\n%@: %@\n%@: %@\n%@: %@ GB\n%@: %@\n%@: %d degF\n", [NSString stringWithUTF8String:ise[cur_ise].dp[i].sts], @"detail", [NSString stringWithUTF8String:ise[cur_ise].dp[i].detail], @"serial#", [NSString stringWithUTF8String:ise[cur_ise].dp[i].sn], @"model", [NSString stringWithUTF8String:ise[cur_ise].dp[i].model], @"part#", [NSString stringWithUTF8String:ise[cur_ise].dp[i].partnum], @"capacity", [NSString stringWithUTF8String:ise[cur_ise].dp[i].capacity], @"health", [NSString stringWithUTF8String:ise[cur_ise].dp[i].health], @"temperature", (((atoi(ise[cur_ise].dp[i].temp))*9)/5)+32 ]; altitle = [[NSString alloc] initWithFormat:@"DataPac #%d",i+1]; } UIAlertView *av = [[[UIAlertView alloc] initWithTitle:altitle message:alestr delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease]; [av show]; [alestr release]; [altitle release]; Custom Button Creation/Usage (i.e. making things look nice) Create the button using interface builder and lay a custom png file down as the background such that it looks pretty. If you want, you can change the background png as follows. NSArray *buttonVals = [NSArray arrayWithObjects: @"green2_btn.png", @"yellow2_btn.png", @"red2_btn.png", @"gray2_btn.png", nil]; int statcolor = 0; NSMutableString *color = nil; //mrc1 statcolor = [self chkStatDetails:ise[cur_ise].cn[0].detail]; color = [buttonVals objectAtIndex:statcolor]; [mrc1stat setBackgroundImage:[UIImage imageNamed:color] forState:UIControlStateNormal]; Display/Add/remove/sort ISE list logic The logic for displaying/adding/removing/re-ordering the ISE in the Top table view is all implemented in the standard table view controller logic in MyListController.m. Upon initialization the ISEs.plist file which contains persistant information regarding ISE is read and used to display the Top view data. 1) Display (in MyListController.m) NSString *labelInfo = [NSString stringWithFormat: @"%@ - %@", [isekclass displayname], [isekclass serialnumber]]; [[cell textLabel] setText:labelInfo]; NSString *detailText = [NSString stringWithFormat: @"%@ / %@", [isekclass mrc1ip], [isekclass mrc2ip]]; 2) Add (See below for adding ISE but the code in MyListController.m is as follows) // Creates a new nav controller with an instance of MyDetailController as // its root view controller, and runs it as a modal view controller. By // default, that causes the detail view to be animated as sliding up from // the bottom of the screen. And because the detail controller is the root // view controller, there's no back button. // - (void)add { iseCfgFromXmlQuery *controller = [[iseCfgFromXmlQuery alloc] init]; controller.view; UINavigationController *newNavController = [[UINavigationController alloc] initWithRootViewController:controller]; [[self navigationController] presentModalViewController:newNavController animated:YES]; [controller release]; } 3) Remove // Override inherited method to enable/disable Edit button // - (void)setEditing:(BOOL)editing animated:(BOOL)animated { [self save]; [super setEditing:editing animated:animated]; UIBarButtonItem *editButton = [[self navigationItem] rightBarButtonItem]; [editButton setEnabled:!editing]; } 4) Sort (allowing re-order of table cells) The re-order is dependant on the successful save to complete at program termination in order to maintain the new order persistantly // Invoked when the user drags one of the table view's cells. Mirror the // change in the user interface by updating the array of displayed objects. // - (void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)targetIndexPath { NSUInteger sourceIndex = [sourceIndexPath row]; NSUInteger targetIndex = [targetIndexPath row]; if (sourceIndex != targetIndex) { [[self displayedObjects] exchangeObjectAtIndex:sourceIndex withObjectAtIndex:targetIndex]; } } Dynamic query while adding an ISE (validate on the fly) These routines manage the discovery and management of ISE systems which are known to ISEview. The data is kept around or maintains persistence via the use of the ISEs.plist properties file. The logic for discovering an ISE is one of the more simple functions thanks to the parsing capabilities of the XMLparser.m. The following logic is employed: 1. Build the query URL and call the XMLparser to sort it out (in iseCfgFromXmlQuery.m) // fetch data was pressed -(IBAction) setOutput:(id)sender { // build the query url NSString *queryurl = [[NSString alloc] initWithFormat:@"http://%@/query", userInput.text]; // Call the xml parser to fetch and parse the query url TreeNode *root = [[XMLParser sharedInstance] parseXMLFromURL:[NSURL URLWithString:queryurl]]; 2. Use the DOM style tree in memory to find the SN and MRC 1 and 2 IP addresses // find the serialnumber TreeNode *sn = [root objectForKey: @"serialnumber"]; if(sn.leafvalue!=nil) { sn.leafvalue=[sn.leafvalue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } // find the name TreeNode *isename = [root objectForKey: @"name"]; if(isename.leafvalue!=nil) { isename.leafvalue=[isename.leafvalue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; } // find controller1 ip address TreeNode *cn1 = [root objectForKey:@"controller1"]; if(cn1.leafvalue!=nil) { cn1.leafvalue=[cn1.leafvalue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } TreeNode *ip1 = [cn1 objectForKey:@"ipaddress"]; if(ip1.leafvalue!=nil) { ip1.leafvalue=[ip1.leafvalue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } // find controller2 ip address TreeNode *cn2 = [root objectForKey:@"controller2"]; if(cn2.leafvalue!=nil) { cn2.leafvalue=[cn2.leafvalue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } TreeNode *ip2 = [cn2 objectForKey:@"ipaddress"]; if(ip2.leafvalue!=nil) { ip2.leafvalue=[ip2.leafvalue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } 3. Update the display using the newly discovered ISE information // update the displayed values mrc1ipOutlet.text = ip1.leafvalue; mrc2ipOutlet.text = ip2.leafvalue; snOutlet.text = sn.leafvalue; 4. And save the data to the end of the ISEs.plist file Get the path to the ISEs.plist file NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *path = [documentsDirectory stringByAppendingPathComponent:@"ISEs.plist"]; Read it into an array NSMutableArray *iseDicts = [NSMutableArray arrayWithContentsOfFile:path]; Create the new entry NSMutableDictionary *newdictionary = [NSDictionary dictionaryWithObjectsAndKeys: (NSString *)isename.leafvalue, @"displayname", (NSString *)sn.leafvalue, @"serialnumber", (NSString *)ip1.leafvalue, @"mrc1ip", (NSString *)ip2.leafvalue, @"mrc2ip", (NSString *)uid, @"userid", (NSString *)pw, @"password", (NSString *)queryurl, @"queryurl", (NSString *)perfurl, @"perfurl", (NSString *)statusurl, @"statusurl", (NSString *)driveurl, @"driveurl", nil]; Put them together and write them out // add ise to the array of ises if ([iseDicts count] < 16) { [iseDicts addObject:newdictionary]; NSString *plist = [iseDicts description]; NSError *error = nil; [plist writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error) { NSLog(@"error writing to plist file"); } }