TweetFollow Us on Twitter

Using NSParagraphStyle

Volume Number: 19 (2003)
Issue Number: 8
Column Tag: Cocoa

Using NSParagraphStyle

How to programmatically set tabs and more in an NSTextView

by Clark Jackson

The need to become acquainted with NSParagraphStyle occurred when I attempted to automate some reporting. At times "enterprise" computing stands in contrast to typical "consumer" computing because the latter is user-centric while the former is report-centric. For example, consumer computing facilitates lots of user control whereas enterprise computing just wants to-generate-reports-unattended-at-midnight-thank-you. Since much of the text system in Cocoa "just works" for the user, the details of how to let the computer take control in making a report are often left a mystery. Let's shed a little light on some of that mystery.

What we need is to know how to use just one piece of Cocoa's extensive and capable text system that will allow a program to format text inside an NSTextView. The key is using the NSParagraphStyle class.

Some preliminaries

Explaining Interface Builder and Project Builder is beyond the scope of this article. Suffice it to say, the project uses the Model-View-Controller paradigm employing MyModel, MyWindowController, and NSTextView classes. Here's what the view looks like:


Figure 1. The interface window containing an NSTextView.

The only thing noteworthy about this window is that it contains an NSTextView because that is where our formatted text will appear.

To see the results of our formatting it will be useful to use NSTextView's ruler. To see the ruler it is necessary to add the Format menu to our menu bar. Therefore, before leaving IB, we drag the Format menu onto our project's main menu bar and give Show Ruler a key equivalent:


Figure 2. For convenience, in Interface Builder we drag a Format menu onto the menu bar and give Show Ruler a key equivalent.

The view

For our purposes, the important thing to know about NSTextView is that the text displayed in an NSTextView is its textStorage and that textStorage holds attributed strings. The textStorage is an instance of NSTextStorage which inherits from NSAttributedString itself. Attributed strings are Cocoa Foundation objects that contain both ordinary NSString's and formatting objects that apply to those strings, for example, NSFont and NSParagraphStyle. You can bypass dealing with NSTextView's textStorage directly, that is, you can [myNSTextView setString:@"aString"], but @"aString" will adopt either the default paragraph style or a previous style already residing in the textStorage.

Therefore, in order for text to appear formatted in an NSTextView we have to: 1) create an NSAttributedString instance initialized with the string we want to format, and 2) give that NSAttributedString instance the objects that it will use to format the string-in this case an NSParagraphStyle object. This attributed string goes into our NSTextView's textStorage where it appears formatted to the user.

The Controller

The interface buttons are connected to the controller's actions in a typical fashion. Basically, each button initiates a controller action to display a different formatted paragraph in the NSTextView. The controller does this task by sending a string (NSString) to the model object. The model object will return attributed strings (NSAttributedString's attributed with NSParagraphStyle's) to the controller. Finally, the controller will pass the attributed string to the text storage of the NSTextView where it will appear formatted to the user.

Here is one of the three controller actions:

applyStyle1

This method sets a pattern for the other two belonging to the controller. An unformatted string is 
put in the NSTextView for comparison purposes only. The program pauses so the user can see it. Next, 
the string is sent to the model object which returns it as an attributed string. Finally, the 
attributed string goes into the NSTextView revealing the new format. Refer to the source to see the 
other two action methods, applyStyle2 and applyStyle3. 

- (IBAction)applyStyle1:(id)sender
{
   MyModel *theModel;
   NSAttributedString *anAttString;
   NSString *aString;
   // a string containing multiple paragraphs
   aString = @"On a merry-go-round in the night, ";
   aString = [aString stringByAppendingString:
      @"Coriolis was shaken with fright.\n"];
   aString = [aString stringByAppendingString:
      @"Despite how he walked, "];
   aString = [aString stringByAppendingString:
      @"'Twas like he was stalked,\n"];
   aString = [aString stringByAppendingString:
      @"By some fiend always pushing him right."];
// limerick by David Morin, Eric Zaslow, E'beth Haley, John Golden, and Nathan Salwen,
//   http://www.aps.org/apsnews/11855.html
   // a convenience method to restore the default paragraph style to the NSTextView
   [self resetTextView]; 
   // install string w/o changing paragraph style for comparison
   [myNSTextView setString:aString]; 
   [myNSTextView display]; // force redraw
   sleep(1); // pause
   
   // now convert aString to an NSAttributedString by applying an NSParagraphStyle
   //   using the model object
   theModel = [[MyModel alloc]init];
   anAttString = [theModel attributeString1:aString];
   [theModel release];
   
   // now that we have the attributed string, put it into the NSTextView.
   // In order to get multiple attributed strings into a textStorage use
//      [myNSTextView insertText:attributedString] instead
   [[myNSTextView textStorage]
      setAttributedString:anAttString];
   [myNSTextView setNeedsDisplay:YES];
   return;
}

The Model

MyModel has a method for each of the controller's actions. It is here that we make use of NSParagraphStyle. Each method takes an ordinary NSString and returns it as an NSAttributedString so it can end up formatted in the NSTextView. When running the program, click inside the NSTextView and make the ruler visible. Some of the paragraph style attributes that we set show up on the NSTextView's ruler:


Figure 3. Many of NSParagraphStyle attributes appear in the NSTextView ruler.

Following are the methods used to create NSParagraphStyle's and apply them to strings thus creating NSAttributedString's:

attributeString1

This method creates an NSMutableAttributedString, using an NSString and an NSMutableParagraphStyle.

-(NSMutableAttributedString *) attributeString1:
      (NSString *) aString
{
   NSMutableParagraphStyle *aMutableParagraphStyle;
   NSMutableAttributedString   *attString;
   /*
   The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
   NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
   to copy, we use the default one.
   
   The default values supplied by the default NSParagraphStyle are:
      Alignment   NSNaturalTextAlignment
      Tab stops   12 left-aligned tabs, spaced by 28.0 points
      Line break mode   NSLineBreakByWordWrapping
      All others   0.0
   */
   aMutableParagraphStyle =
      [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
   
   // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
   // The numeric values below are in points (72 points per inch)
   [aMutableParagraphStyle
      setAlignment:NSLeftTextAlignment];
   [aMutableParagraphStyle setLineSpacing:5.5];
   [aMutableParagraphStyle setParagraphSpacing:25.5];
   [aMutableParagraphStyle setHeadIndent:25.0];
   [aMutableParagraphStyle setTailIndent:-45.0];
   // setTailIndent: if negative, offset from right margin (right margin mark does
   //      NOT appear); if positive, offset from left margin (margin mark DOES appear)
   [aMutableParagraphStyle setFirstLineHeadIndent:65.0];
   [aMutableParagraphStyle
      setLineBreakMode:NSLineBreakByWordWrapping];
   /*
    possible allignments
      NSLeftTextAlignment
      NSRightTextAlignment
      NSCenterTextAlignment
      NSJustifiedTextAlignment
      NSNaturalTextAlignment
    possible line wraps
      NSLineBreakByWordWrapping
      NSLineBreakByCharWrapping
      NSLineBreakByClipping
    */
   
   // Instantiate the NSMutableAttributedString with the argument string
   attString = [[NSMutableAttributedString alloc]
      initWithString:aString];
   // Apply your paragraph style attribute over the entire string
   [attString addAttribute:NSParagraphStyleAttributeName
      value:aMutableParagraphStyle
      range:NSMakeRange(0,[aString length])];
   [aMutableParagraphStyle release]; // since it was copy'd
   [attString autorelease]; // since it was alloc'd
   return attString;
}

If your NSTextView already has attributed strings in its textStorage, you can get the NSParagraphStyle by:

aMutableParagraphStyle = [[myTextView typingAttributes]
   objectForKey:@"NSParagraphStyle"];

The NSParagraphStyle returned above comes from the attributes of the text from where the cursor is found inside the NSTextView. NSParagraphStyle's can span multiple paragraphs but there cannot be more than one per paragraph.

attributeString2

This method creates an array of NSTextTab's and applies them to the NSMutableParagraphStyle before 
it is used to create the NSMutableAttributedString.

-(NSMutableAttributedString *)
   attributeString2:(NSString *) aString
{
   float firstColumnInch = 1.75,
      otherColumnInch = 0.6, pntPerInch = 72.0;
   int i;
   NSTextTab *aTab;
   NSMutableArray *myArrayOfTabs;
   NSMutableParagraphStyle *aMutableParagraphStyle;
   NSMutableAttributedString   *attString;
   myArrayOfTabs = [NSMutableArray arrayWithCapacity:14];
   aTab = [[NSTextTab alloc]
      initWithType:NSLeftTabStopType
      location:firstColumnInch*pntPerInch];
   [myArrayOfTabs addObject:aTab];
   [aTab release]; // aTab was alloc'd and the array owns it now so release it
   for(i=1;i<14;i++)
   {
      aTab = [[NSTextTab alloc]
         initWithType:NSRightTabStopType
         location:(firstColumnInch*pntPerInch)
         + ((float)i * otherColumnInch * pntPerInch)];
      [myArrayOfTabs addObject:aTab];
[aTab release]; // aTab was alloc'd and the array owns it now so release it
   }
   /*
   possible tab stop types
      NSLeftTabStopType
      NSRightTabStopType
      NSCenterTabStopType
      NSDecimalTabStopType
    */
    
   aMutableParagraphStyle =
      [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
   [aMutableParagraphStyle setTabStops:myArrayOfTabs];
   attString = [[NSMutableAttributedString alloc]
      initWithString:aString];
   [attString addAttribute:NSParagraphStyleAttributeName
      value:aMutableParagraphStyle
      range:NSMakeRange(0,[aString length])];
   [aMutableParagraphStyle release];
   [attString autorelease];
   return attString;
}

Tabbed text appears as below without using NSParagraphStyle:


Figure 4. Default behavior of an NSTextView containing tabbed text. The tab spacing is arbitrary and the lines wrap even though there is space enough to hold the whole line.

Tabbed text appears as below after creating an NSMutableParagraphStyle and assigning it to an NSMutableAttributedString using the attributeString2 method:


Figure 5. By using an NSMutableParagraphStyle the tab stops have been precisely set and the lines do not wrap unexpectedly.

attributeString3

This method supplies a way to parse a string containing multiple paragraphs and apply different 
paragraph styles to each.

-(NSMutableArray *) attributeString3:(NSString *)aString 
{
   NSRange myRange,rangeOfLine,rangeOfLineContent;
   unsigned startIndex,lineEndIndex,contentsEndIndex;
   NSMutableParagraphStyle
      *aMutableParagraphStyle1,*aMutableParagraphStyle2;
   NSMutableAttributedString   *attString;
   int lineCtr;
   NSMutableArray * arrayOfNSMutableAttributedString;
   NSString *pulledOutParagraph;
   arrayOfNSMutableAttributedString
      = [NSMutableArray arrayWithCapacity:5];
   /*
    lines can be terminated in the following ways for use with
      getLineStart:end:contentsEnd:forRange:
    *    U+000D (\r or CR), Mac
    *    U+2028 (Unicode line separator)
    *    U+000A (\n or LF), Unix
    *    U+2029 (Unicode paragraph separator)
    *    \r\n, in that order (also known as CRLF), Windows
    */
   /*
   Using getLineStart:end:contentsEnd:forRange: 
   @"Big\r\nMac    --example string
   in this case
      1st contentsEndIndex    is set to 3,
         the first character past the line content,
         line content being defined as the characters not including
         line termination character(s)
      1st lineEndIndex    is set to 5
         the first character past the end of line character(s)
   */
   // make 2 different paragraph styles
   aMutableParagraphStyle1 =
      [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
   aMutableParagraphStyle2 =
      [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
   [aMutableParagraphStyle1
      setAlignment:NSLeftTextAlignment];
   [aMutableParagraphStyle2
      setAlignment:NSRightTextAlignment];
   
   // initialize
   myRange = NSMakeRange(0,0); // (location,length)
   lineEndIndex = 0;
   lineCtr = 0;
   /*
    The while() block below walks down the string pulling out lines (paragraphs)
   including their end of line (paragraph) characters, converts them to attributed strings,
   and adds them to an array.
   The range in the getLineStart:end:contentsEnd:forRange: method has to be picky
   about where it's location is but doesn't have to be picky about length (i.e. 0 will do)
   */
   while(lineEndIndex < [aString length])
   {
      [aString getLineStart: &startIndex
         end: &lineEndIndex
         contentsEnd: &contentsEndIndex
         forRange: myRange];
      // rangeOfLineContent excludes line termination character(s)
      rangeOfLineContent =
         NSMakeRange(startIndex,contentsEndIndex-startIndex); 
      // rangeOfLine includes line termination character(s)
      rangeOfLine =
         NSMakeRange(startIndex,lineEndIndex-startIndex); 
      // apply paragraph style to pulledOutParagraph
      pulledOutParagraph =
         [aString substringWithRange:rangeOfLine];
      attString = [[NSMutableAttributedString alloc]
         initWithString:pulledOutParagraph];
      if(lineCtr % 2 == 0) // alternate paragraph styles on odd/even lines
      {
         // If range.length below is zero then the paragraph style is not applied.
         //   For strings with length > 0, range.length should be > 0 and <= length of the
         //   string so NSMakeRange(0,1) would work as well below
         //   provided you don't have an empty string.
         [attString addAttribute:NSParagraphStyleAttributeName
            value: aMutableParagraphStyle1
            range: NSMakeRange(0,[pulledOutParagraph length])];
      }
      else
      {
         [attString addAttribute:NSParagraphStyleAttributeName
            value:aMutableParagraphStyle2
            range:NSMakeRange(0,[pulledOutParagraph length])];
      }
      [arrayOfNSMutableAttributedString addObject:attString];
      [attString release]; // the array now owns it
      lineCtr++;
      // recalculate the range before looping
      myRange = NSMakeRange(lineEndIndex,0); 
   }
   [aMutableParagraphStyle1 release];
   [aMutableParagraphStyle2 release];
   return arrayOfNSMutableAttributedString;
}

Conclusion

Cocoa's text system is extensive involving many classes not even mentioned here. But if you just need to get some formatted, tabbed text into an NSTextView for that special report, the methods explained here should do the trick.


Clark Jackson test drove the first Mac and recognized that it was about to change the world. Lately he has been busy wearing many hats automating energy accounting in the rapidly changing electrical power industry at Tacoma Power. He can be contacted at cjackson@cityoftacoma.org.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.