Publishers of technology books, eBooks, and videos for creative people

Home > Articles > Apple > Operating Systems

This chapter is from the book

Step 3: Implement the Add Tag Push Button

The Add Tag button works like the Add Entry button, with two exceptions. One exception is that the special character marking an entry's tag list is different from the character marking entry titles, to allow separate searches for titles and for tags. The other is that the tag list is inserted immediately following the title of whichever diary entry currently has keyboard focus in the split view; that is, the diary entry in which the text insertion cursor is currently located.

The tag list is actually a paragraph of text starting with the special character for tags followed by a space, the word Tags, a colon, another space, and tag words or phrases typed by the user. The Chef's Diary is not a database with records and fields. It is a simple RTF text object, and the tags can be edited like any other text. Special characters are used to identify the entry titles and the tags simply to make it easy to locate them programmatically. Entering the actual tags in the tag list is up to the user, who simply types them.

In Vermont Recipes, individual tags can be separated by any white space or punctuation marks, such as commas and spaces. This is done mostly for the sake of simplifying the code, but it works rather well. For example, if the tag list is ho hum, the user can use the search field to search for ho, hum, or ho hum. The problem with this is that the first two characters of hotel in a tag list will also be found. In a real-world application, you should use a more sophisticated approach. For now, just make a note of it.

A significant difference in the code for the new -addTag: action method is dictated by the need to find the title of the current diary entry so that the tag list can be inserted immediately following it, instead of at the end of the text view. A newline character is always inserted before the tag list because the tag list is always inserted at the end of the current title, before the title's trailing newline character. The tag list has no trailing newline character because the user should be allowed to start typing a new tag immediately following the Tags: label. If the entry already has a tag list, the method places the insertion pointer at its end, after the last tag, so that the user can immediately start typing additional tags.

Another difference in the -addTag: method is that it should look for the next diary entry, in order to define the end of the search range for an existing tag list in the current entry. This is necessary in part because the diary is a simple text file, not a database file, and the user might type additional text between a title and a tag list. Although the Add Tag button always inserts a new tag immediately following the title of the current entry, it should be user friendly and accommodate any user who has typed text between the current entry's title and an existing tag list. Another reason the search must stop at the next entry title marker is that tags are optional and the current entry may not have a tag list at all.

Looking ahead, you can see that the code to locate the title of the current and next entries might do double duty in the implementation of the navigation buttons, coming up shortly. It would therefore be a good idea to place this navigation code in separate methods. Call them -currentEntryTitleRangeForIndex: and -nextEntryTitleRangeForIndex:, and have them return both the location and the length of the title as an NSRange structure. Once you have written those two methods, you will be ready to write the method that returns the location and length of the current entry's tag list, if there is one, or the location where the tag list should be inserted and a length of 0 if there is no existing tag list in the current entry. You will call that method -currentEntryTagRangeForIndex:. Only after you have written these three supporting methods will you be ready to write the -addTag: action method.

As you work through the rest of this recipe, you may be struck by the fact that so many methods return an NSRange struct. The Cocoa text system relies heavily on ranges for efficiency and convenience. A text storage object can be very long, and you don't want to have to search the whole thing every time you need to find one substring or character in it.

Before getting started, consider again the MVC design pattern. The methods you are about to write start with information about the insertion point or the current selection in one of the diary window's text views, and they use that information to guide a search for characters in the diary document's text storage. They then add text to the text storage, and they end by changing the text view's selection and scrolling the new selection into view. These operations involve both the MVC model and the MVC view. You therefore implement some of them in the DiaryWindowController class and some of them in the DiaryDocument class. Searching for characters in the text storage object and adding tags to it is work for the MVC model, so you place these supporting methods in the DiaryDocument class, which, as you know, acts as a model controller.

Another thing to consider is code reuse. The methods you are about to write, and others like them that you will write later, involve some repetitive operations. You should look for every opportunity to gather repetitive code into separate methods for easy reuse.

Start with the DiaryDocument methods. I will reproduce one or two of these methods here. The rest are similar. Rather than reproduce all of them in the book, I refer you to the downloadable project file for Recipe 4 at www.peachpit.com/cocoarecipes, where they are laid out in full.

  1. Just as you wrote the -entryMarker, -entryTitleForDate:, and -entryTitleInsertForDate: methods in Step 2 for the entry title, you need to write -tagMarker, -tagTitle, and -tagTitleInsert methods for the tag title. They are much simpler, because they don't have to construct any date or time strings. You will find them in the downloadable project file for Recipe 4.

  2. Now you're ready to write the first of several methods that obtain the range of entry titles and tag titles in the Chef's Diary. You start with the current entry, which is defined as the entry in which the insertion point is currently located. In this method and all of the other methods like it that you have yet to write, the current insertion point is passed into the method as its index in the text storage object.

    In the DiaryDocument.h header file, declare the -currentEntryTitleRangeForIndex: method at the end of the file, just before the @end directive:

    - (NSRange)currentEntryTitleRangeForIndex:(NSUInteger)index;

    In the DiaryDocument.m implementation file, define it like this:

    - (NSRange)currentEntryTitleRangeForIndex:(NSUInteger)index {
         if ([[self diaryDocTextStorage] length] == 0)
               return NSMakeRange(NSNotFound, 0);
         index = [self adjustedIndexIfMarkerAtIndex:index];
         NSUInteger markerIndex = [[[self diaryDocTextStorage] string]
                 rangeOfString:[self entryMarker]
                 options:NSBackwardsSearch
                 range:NSMakeRange(0, index)].location;
         return [self rangeOfLineFromMarkerIndex:markerIndex];
    }

    The first statement takes care of an edge case. If the text storage object is currently empty, it doesn't contain an entry title, so you create and return a range with length 0 whose location is NSNotFound. This conforms to the Cocoa text system convention that a search that comes up empty returns a range whose location member is NSNotFound. This also simplifies the rest of the method's code by eliminating any need to test for an empty text storage object.

    The second statement deals with another edge case. From the user's perspective, if the insertion point is currently located at the entry marker character—that is, immediately before the entry marker and on the same line—it is in the entry marked by that character. In other words, this is the current entry. You force this convention on the text system by defining the search range so that its location member is just after the entry marker. As a result, the backward search finds it immediately. This will also work for forward searches because it prevents the method from finding the marker at the insertion point and allows it to find the following marker for the next entry. You do this in a utility method, -adjustedIndexIfMarkerAtIndex:, which you will write shortly.

    Finally, the method pursues an efficient strategy to locate the current entry's entry marker. You will soon follow a similar strategy in methods designed to search for the next and previous titles. In summary, you start by searching backward from the insertion point (the index argument), looking for the first entry marker you find. If you don't find one, you know the current selection was in an untitled preface near the beginning of the diary, and you exit signaling that a title was not found. If you do find an entry marker, you next search forward from that point, looking for the first newline character you find, or for the end of the file if you don't find a newline character. The newline character or the end of the file defines the length of the entry's title. When returning a range value from the -currentEntryTitleRangeForIndex: method, the location member represents the location of the title marker, and the length member counts the marker and all the characters in the title up to, but not including, the newline character. You must exclude the newline character so that you can use the range even if it ends at the end of the file instead of at a newline character.

    You carry out this strategy in two steps. First, you call an important Cocoa method, -rangeOfString:options:range:, to find the preceding entry marker. Then you call a utility method that you will write shortly, -rangeOfLineFromMarkerIndex: to find the first newline character following the entry marker, or the end of the text storage object's contents if there is no following newline character. The utility method converts this to a range encompassing the entry's title, and you return that range from the -currentEntryTitleRangeForIndex: method. This utility method deals specially with the case where no preceding entry marker is found, returning, as you by now expect, a range whose location member is NSNotFound. The application interprets this to mean that there is no current entry; instead, the insertion point is consider to lie in a preface to the diary preceding its first entry.

    You perform the search for the preceding entry marker using NSString's -rangeOfString:options:range: method. This workhorse text system method is highly optimized and very fast. What you search for is DIARY_TITLE_MARKER, using the -entryMarker method you just wrote. You defined the macro in Step 2 as a string containing the Unicode character with code point 0x270E, a dingbat character representing a pencil.

    If an entry marker is found, the -currentEntryTitleRangeForIndex: method next searches forward from the entry marker, looking for the new-line character that defines the end of the entry's title. It does this using the -rangeOfLineFromMarkerIndex: utility method, which you will write in a moment.

  3. Now write the two utility methods used by -currentEntryTitleRange:ForIndex:. The first is -adjustedIndexIfMarkerAtIndex:.

    Enter this declaration of the method just before the -currentEntryTitleRangeForIndex: method in the DiaryDocument.h header file:

    - (NSUInteger)adjustedIndexIfMarkerAtIndex:(NSUInteger)index;

    Enter the method's definition in the DiaryDocument.m implementation file:

    - (NSUInteger)adjustedIndexIfMarkerAtIndex:(NSUInteger)index {
         return ((index < [[self diaryDocTextStorage] length]) &&
                ([[[self diaryDocTextStorage] string] characterAtIndex:index]
                == [[self entryMarker] characterAtIndex:0]))
                ? index + 1 : index;
    }

    The method first tests whether index is less than the text storage's total length. This test returns NO if the insertion point is at the end of the diary, thus preventing the method from incrementing index to an illegal value. There is no point in searching for a marker character at the end of the file anyway, because it could not mark an entry title.

    The method then tests whether the character at index is an entry marker, and, if it is, increments index. This implements the application's convention, described earlier, that if the insertion point is at the entry marker and on the same line, it is considered to be in the current entry.

  4. Now write the second utility method just after the first.

    In the DiaryDocument.h header file, declare the method as follows:

    - (NSRange)rangeOfLineFromMarkerIndex:(NSUInteger)markerIndex;

    In the DiaryDocument.m implementation file, define it like this:

    - (NSRange)rangeOfLineFromMarkerIndex:(NSUInteger)markerIndex {
         if (markerIndex == NSNotFound) return NSMakeRange(NSNotFound, 0);
         NSRange searchRange = NSMakeRange(markerIndex,
                 [[self diaryDocTextStorage] length] - markerIndex);
         NSUInteger newlineIndex = [[[self diaryDocTextStorage] string]
                 rangeOfString:@"\n" options:0
                 range:searchRange].location;
         if (newlineIndex == NSNotFound) return searchRange;
         return NSMakeRange(markerIndex, newlineIndex - markerIndex);
    }
    

    The method returns a range with a location of NSNotFound and a length of 0 if the incoming index argument is NSNotFound. This is the case if the search in -currentEntryTitleRangeForIndex: found no preceding entry marker.

    If there is a preceding entry marker, the method searches forward for a trailing newline character marking the end of the entry's title. Cocoa does not declare a constant for forward searches on the model of NSBackwardsSearch for backward searches. Just use 0.

    The method has to take into account the possibility that the title is the last line in the diary; that is, that there is no following newline character. It therefore defines the search range to encompass the text storage from the entry marker to the end of the file. If a newline character is not found, it returns the search range, since without a trailing newline character the title must encompass the entire remainder of the text storage. Finally, if a newline character is found, the method returns the current title's range, including its location and its length excluding any trailing newline character.

  5. You can now write the -nextEntryTitleRangeForIndex: method, which, like -currentEntryTitleRangeForIndex:, is needed before you can implement the -currentEntryTagRangeForIndex: and -enterTag: methods. You will find it in the downloadable project file for Recipe 4.

    The -nextEntryTitleRangeForIndex: method is identical to -currentEntryTitleRangeForIndex: except that it searches the portion of the file following the insertion point and the search is forward toward the end of the file. There is no NSForwardsSearch constant like the NSBackwardsSearchconstant, so you use 0.

  6. Now write the -currentEntryTagRangeForIndex:method. It is a little more complicated, because it has to search for the beginning and the end of the current entry before it can determine whether the current entry already has a tag title. To do this, it calls the -currentEntryTitleRangeForIndex: and -nextEntryTitleRangeForIndex:methods you just wrote. You will find it in the downloadable project file for Recipe 4.

    The -currentEntryTagRangeForIndex: method is similar to -currentEntryTitleRangeForIndex: and -nextEntryTitleRangeForIndex:, but the strategy is a little different because the tag title is positioned differently in the text view. The application specification calls for an optional tag title to be inserted on the line immediately following an entry's title. You therefore start by searching backward from the insertion point for an entry marker. There should be no tag title in an untitled preface, and the returned range's location should therefore be NSNotFound if no entry marker is found searching backward. You perform the backward entry marker search using the -currentEntryTitleRangeForIndex: method you just wrote. If an entry marker is found, you next search forward for a following entry marker to define the endpoint of the tag marker search range. You use the -nextEntryTitleRangeForIndex: method you just wrote to perform the forward entry marker search. You then search the range between the two entry markers—or between the first entry marker and the end of the file—for a tag marker and its trailing newline character using code that is nearly identical to the searches for an entry marker and its trailing newline character in the methods you just wrote.

    Another difference in the -currentEntryTagRangeForIndex: method is that it must return the location where a tag list should be inserted if there is no existing tag list in the current entry. In that case, you return a range with a location at the end of the current title string and a length of 0. In the -addTag: action method you are about to write, you will test for a length of 0 in order to choose between inserting a new tag list or simply moving the insertion point to the end of an existing tag list so the user can begin typing tags.

  7. The -addTag: action method calls one more utility method that you have yet to write,-insertionPointIndex:. You have read several times that methods you have just written, like -currentEntryTitleForindex:, take as their index argument the current insertion point in the text view having keyboard focus. Write the method that gets the insertion point now. It belongs in the DiaryWindowController class because it relates to the MVC view.

    At the end of the DiaryWindowController.h header file, insert this declaration:

    - (NSUInteger)insertionPointIndex;

    Define it in the DiaryWindowController.m implementation file:

    - (NSUInteger)insertionPointIndex {
         return [[[[self keyDiaryView] selectedRanges] objectAtIndex:0]
                rangeValue].location;
    }

    In the Cocoa text system, the insertion point is a selection range with a length of 0. When the user clicks the Add Tag button, however, it is entirely possible that a word or phrase may be selected, or even several words or phrases now that the text system supports multiple selection. You must therefore implement a convention defining what the application will consider to be the insertion point and how it will behave when inserting a new tag title. The location of new tag titles is defined in the application specification, so it cannot replace the user's current selection. The insertion point is needed only to know where to begin searching for title markers and tag markers.

    In this method, you treat the location of the first selection in the text view's array of selection ranges as the insertion point. According to the NSTextView Class Reference, the Cocoa text system guarantees that -selectedRanges always returns an array having at least one element, so you do not have to test for these conditions.

  8. At last, you are ready to write the -addTag: action method. This affects the MVC view, so it belongs in the DiaryWindowController class. You will find it in the downloadable project file for Recipe 4.

    The -addTag: action method begins by defining three local variables, keyView , storage , and tagString , following the pattern of -addEntry:. The tagString variable uses the DIARY_TAG_MARKER macro, which you defined at the beginning of this step as the Unicode WHITE FLAG character, with code point 0x2690.

    The method next calls the -currentEntryTagRangeForIndex: method you just finished writing, passing to it the current insertion point. If its location member has the value NSNotFound , the method returns without doing anything because this means the insertion point is currently in an untitled preface. The application specification dictates that an untitled preface cannot hold a valid tag list. You don't really want an action method to do nothing when its button is clicked or its menu item is chosen, because this will confuse the user. You will shortly arrange for the Add Tag button to be disabled in this circumstance. Nevertheless, you should leave this line in the method as a backstop.

    The next line tests the length member of the range returned by the -currentEntryTagRangeForIndex: method. If it is 0, there is no existing tag marker in the current entry and you should insert a new, empty tag title at location. You do this using the same techniques you used in the -addEntry: action method in Step 2, complete with undo support and generation of appropriate notifications. The Edit menu will hold Undo Add Tag and Redo Add Tag menu items. If the length is not 0, then there is already a tag title in the current entry and you don't have to add one.

    Finally, whether you're adding a new tag title or one already exists in the current entry, the method scrolls the text view having keyboard focus to the new or existing tag title and places the insertion pointer appropriately so that the user can begin typing a new tag.

  9. As with every new action method, you must remember to connect it up in Interface Builder. Open the DiaryWindow nib file and Control-drag from the Add Tag button in the diary window to the First Responder proxy in the nib file's window. In the HUD, choose the addTag: action.

  10. You should build and run the application now to test the Add Entry and Add Tag buttons in combination. Open the Chef's Diary window and click the Add Entry button. Immediately after that, click the Add Tag button. In the line following the new entry's title, you see a white flag character and the word Tags: , and the insertion point is located just after that so you can type a tag. Type dessert , appetizer or something similar. Then press the Return key and write up a real or imagined culinary experience, using as many paragraphs as you like. Then click Add Entry and Add Tag again and type some more. Now experiment. Click in the middle of one of the entries and click Add Entry. A new entry appears at the end of the diary, and if the end was scrolled out of view, it scrolls into view. Click again in the middle of any entry and click Add Tag. If that entry already has a tag title, the insertion point moves to its end to let you type another tag.

    Now click at the beginning of the diary, before the first entry's title marker. Type some text, creating an untitled preface to the diary, followed by Return, and click in the new text. Notice that the Add Tag button remains enabled, but when you click it, nothing happens. Now delete all the text from the Chef's Diary window. The Add Tag button is still enabled. Click Add Tag again, and still nothing happens. You will arrange to disable the Add Tag button in either of these circumstances in Step 4.

Peachpit Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from Peachpit and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about Peachpit products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites; develop new products and services; conduct educational research; and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email ask@peachpit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by Adobe Press. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.peachpit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020