How to Use Custom NSAttributedString Attributes

http://cocoafactory.com/blog/2012/10/29/how-to-use-custom-nsattributedstring-attributes/

Cocoa Factory

Industrial strength software for iOS and Mac

How to Use Custom NSAttributedString Attributes

OCT 29TH, 2012

How to draw a custom attribute in NSLayoutManager

The Cocoa Text System is incredibly flexible; but not nearly as well-documented as it should be given its power. The classes and methods themselves are completely documented as is the “big picture” - but there’s a lot of intermediate documentation that’s missing.

In this tutorial, we’ll build an app that draws a custom attribute in an NSTextView like this:

CustomAttributeTestApp

NSAttributedString is great for drawing standard attributes such as font, font size, foreground and background colors; but it gets more complicated when you need to some something that requires actual drawing. This tutorial will show you how to do simple drawing of a custom attribute.

Getting Started

Download the example project from Github. You need Xcode 4.5 for this project; so if you don’t have it - go update Xcode first.

NSAttributedString for decorated text

NSAttributedString and its mutable counterpart NSMutableAttributedString are used to draw decorated text. Using these classes, you can create strings with attributes that describe how the string should look when drawn. For example, you can add font and color attributes like this:

1
2
3
4
NSString *sampleString = @"This is a sample string";
NSAttributedString *attributedString;
attributedString = [[NSAttributedString alloc] initWithString:sampleString
                                                   attributes:@{NSFontSizeAttribute:@24}];

This creates a string whose font size attribute is 24.0 pt. And if we want to display the attributedString in an NSTextView:

1
2
NSTextView *textView;
[textView setAttributedString:attributedString];

With the mutable variant NSMutableAttributedString you can add and remove attributes dynamically:

1
2
3
NSMutableAttributedString *string;
string = [[NSMutableAttributedString alloc] initWithString:@"The quick brown fox"
              attributes:@{NSBackgroundColorAttributeName : [NSColor yellowColor]}];

which will render like this:

background-color

Of course, you can also combine attributes:

1
2
3
4
5
6
7
8
9
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
    [attributes setObject:[NSColor yellowColor] forKey:NSBackgroundColorAttributeName];

    NSFont *font = [[NSFontManager sharedFontManager] fontWithFamily:@"Arial" traits:0 weight:5 size:24];
    [attributes setObject:font forKey:NSFontAttributeName];
    NSMutableAttributedString *string;
    string = [[NSMutableAttributedString alloc] initWithString:@"The quick brown fox"
                                                    attributes:attributes];
    [[self textView] insertText:string];

Combining attributes

NSMutableAttributedString tracks changes to its string

If you want to change the underlying NSMutableAttributedString without disturbing its attributes, you can use its mutableString method to obtain an NSMutableString that you can manipulate behind its back, while the NSMutableAttributedString tracks the changes. In fact the object you get back from mutableString is not actually an instance of NSMutableString but an instance of NSMutableStringProxyForMutableAttributedStringinstead. This proxy object is responsible for the tracking behavior internally.

What about custom attributes, then?

Let’s get started building the custom attribute. The drawing is done in the context of a layout manager - a subclass of NSLayoutManager Since our intent is to use our custom attribute in the context of an NSTextView we should look at the architecture of that class first.NSTextView has a single text container in which is lays out text. The NSTextContainer is a rectangular region in which to layout text. Each NSTextView has a default text container, but it is possible to replace the text container using the replaceTextContainer method. The text container uses a layout manager to layout and draw the text. There is readonly access to the text container’s layout manager on NSTextView. In order to give NSTextView a new layout manager, we have to set it on a new NSTextContainer object.

So let’s start with a custom text view that we’ll call CCFTextView. You can find the source code in the “view” folder. This text view basically does on thing - replace its NSLayoutManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static CCFTextView *commonInit(CCFTextView *self) {
    //  set up our initial text size
    NSFont *font = [[NSFontManager sharedFontManager] fontWithFamily:@"Helvetica" traits:0 weight:5 size:24.0];
    NSDictionary *attributes = @{NSFontAttributeName : font};
    [self setTypingAttributes:attributes];

    //  replace our layout manager with custom layout manager
    CCFCustomLayoutManager *layoutManager = [[CCFCustomLayoutManager alloc] init];
    NSTextContainer *textContainer = [[NSTextContainer alloc] init];

    [self replaceTextContainer:textContainer];
    [textContainer replaceLayoutManager:layoutManager];
    return self;
}

The commonInit function is called from either initWithCoder: or initWithFrame: so that no matter how the CCFTextView gets initialized, we replace its text container’s layout manager with our own subclass. Let’s look at the NSLayoutManager subclass - CCFCustomLayoutManager in the “helpers+managers” directory. In the header file “CCFCustomLayouManager.h” we define a few constants.CCFSpecialHighlighterAttributeName is the name of our custom attribute and CCFHighlightColorKey and CCFLineColorKey are keys to the dictionary value of our attribute.

In the implementation of our layout manager, we override a single method drawGlyphsForGlyphRange:atPoint:. Here we’ll digress about glyphs vs. characters.

Glyphs versus characters

The character can is the smallest unit of a written language that has meaning. In Roman and other alphabets, it maps to a particular sound in the spoken counterpart of the written language. However in the case of other languages, like Chinese, it can represent an entire word.

glyph on the other hand is a graphically-concrete form of a character.

Glyphs-vs-characters

The distinction is important, because while we’re manipulating characters in our code, the text system is working behind the scenes laying out glyphs, not characters. In this case, we need to do both. That’s why out NSLayoutManager subclass overrides drawGlyphsForGlyphRange:atPoint. So let’s look a little more closely at what we do in this method, which we’ll build up from pseudo-code

1
2
3
4
5
6
7
8
9
10
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
    /*
     iterate over our glyph ranges
     map the glyph range back to the character range that it represents
     check if our attribute is set on the character range
     if attribute is set
         get the rect of where we should draw the glyph
         do our drawing
    */
}

First, since we need to refer to the character sequence when we do the mapping, we need a source for that mapping. Fortunately, NSLayoutManager keeps a reference to its NSTextStorage object. This object is a subclass of NSMutableAttributedString. We will get this reference and copy glyphsToShow to a local variable so that we can iterate over its span.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
  NSTextStorage *textStorage = self.textStorage;
    NSRange glyphRange = glyphsToShow;
    while (glyphRange.length > 0) {
      /*
     map the glyph range back to the character range that it represents
     check if our attribute is set on the character range
     if attribute is set
         get the rect of where we should draw the glyph
         do our drawing
     */
  }
}

Now, we take care of the glyph-to-character mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
  NSTextStorage *textStorage = self.textStorage;
    NSRange glyphRange = glyphsToShow;
    while (glyphRange.length > 0) {
      NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
        NSRange attributeCharRange, attributeGlyphRange;
        id attribute = [textStorage attribute:CCFSpecialHighlightAttributeName
                                      atIndex:charRange.location longestEffectiveRange:&attributeCharRange
                                      inRange:charRange];
        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
      /*
     check if our attribute is set on the character range
     if attribute is set
         get the rect of where we should draw the glyph
         do our drawing
     */
  }
}

Then to check if the attribute is set on this charRange, we just test for non-nil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
  NSTextStorage *textStorage = self.textStorage;
    NSRange glyphRange = glyphsToShow;
    while (glyphRange.length > 0) {
      NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
        NSRange attributeCharRange, attributeGlyphRange;
        id attribute = [textStorage attribute:CCFSpecialHighlightAttributeName
                                      atIndex:charRange.location longestEffectiveRange:&attributeCharRange
                                      inRange:charRange];
        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
      
      if( attribute != nil ) {
          /*
             get the rect of where we should draw the glyph
             do our drawing
         */
      }
      }
}

Finally, the drawing is the easiest part. We just need to bracket our drawing code with calls to save then restore the NSGraphicsContext before drawing. To get the rectangle in which our glyph is drawn, we ask for the boundingRectForGlyphRange:inTextContainer:. Lastly, we have our completed implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
    NSTextStorage *textStorage = self.textStorage;
    NSRange glyphRange = glyphsToShow;
    while (glyphRange.length > 0) {
        NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
        NSRange attributeCharRange, attributeGlyphRange;
        id attribute = [textStorage attribute:CCFSpecialHighlightAttributeName
                                      atIndex:charRange.location longestEffectiveRange:&attributeCharRange
                                      inRange:charRange];
        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
        if( attribute != nil ) {
            [NSGraphicsContext saveGraphicsState];

            NSColor *bgColor = [attribute objectForKey:CCFHighlightColorKey];
            NSColor *lineColor = [attribute objectForKey:CCFLineColorKey];

            NSTextContainer *textContainer = self.textContainers[0];
            NSRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange inTextContainer:textContainer];

            [bgColor setFill];
            NSRectFill(boundingRect);

            NSRect bottom = NSMakeRect(NSMinX(boundingRect), NSMaxY(boundingRect)-1.0, NSWidth(boundingRect), 1.0f);
            [lineColor setFill];
            NSRectFill(bottom);

            NSRect topRect = NSMakeRect(NSMinX(boundingRect), NSMinY(boundingRect), NSWidth(boundingRect), 1.0);
            NSRectFill(topRect);

            [super drawGlyphsForGlyphRange:attributeGlyphRange atPoint:origin];
            [NSGraphicsContext restoreGraphicsState];
        }
        else {
            [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
        }
        glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
        glyphRange.location = NSMaxRange(attributeGlyphRange);
    }
}

Setting attributes

Let’s turn our attention to CCFMainWindowController where our attributes are being managed. When the user presses the highlight button, we want to tell the text view to apply our attribute to the selection - which is what we do in setCustomAttribute::

1
2
3
4
5
6
7
8
9
10
11
12
13
- (IBAction)setCustomAttribute:(id)sender {
    //  add our custom attribute to the selected range
    NSRange selectedRange = [[self textView] selectedRange];
    NSTextStorage *textStorage = self.textView.textStorage;
    [textStorage addAttribute:CCFSpecialHighlightAttributeName value:[self attributeColors] range:selectedRange];
}

#pragma mark - Private

//  return dictionary of highlight and line colors for our custom attribute's value
- (NSDictionary *)attributeColors {
    return @{  CCFHighlightColorKey : self.highlightColorWell.color, CCFLineColorKey : self.lineColorWell.color };
}

The rest of the code in CCFMainWindowController is for setup and for observing for changes in the highlight and line colors. Using Key-value observing, we are able to detect when the colors change and re-do our markup accordingly.

Athough here’s much more to the text system in Cocoa you should have a good starting point for custom attributes.

Question? Comments? Tweet Alan @NSBum.

 Oct 29th, 2012  cocoa,macostext,

« Target conditionalsCocoa Factory Style Guide »

About Cocoa Factory

We design great software for iOS and Mac platforms. And you can hire us for your next project. Contact us at info@cocoa-factory.com

Our open source components

Not only do we make great software, we support the developer community by open-sourcing some of what we do. We plan to more of this, so stay tuned.

Recent Posts

Latest Tweets

  • Status updating...

Stack Overflow

profile for NSBum at Stack Overflow, Q&A for professional and enthusiast programmers

Copyright © 2013 - Alan Duncan - Powered by Octopress

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Key Features Move beyond default UI templates, create and customize amazing UIs with Android Custom View Enable smooth data flow and create futuristic UIs by creating flexible custom views Scale your apps with responsive and data intensive views Book Description To build great user interfaces for your Android apps that go beyond the standard UI elements, you need to use custom Android views. With these, you can give your app a distinctive look and ensure that it functions properly across multiple devices. This book will help you construct a great UI for your apps by teaching you how to create custom Android views. You will start by creating your first Android custom view and go through the design considerations. You will then see how the right choices will enable your custom view to perform seamlessly across multiple platforms and Android versions. You will create custom styleable attributes that work with Android XML layouts, learn to process touch events, define custom attributes, and add properties and events to them. By the end of this book, you will be able to create apps with custom views that are responsive and adaptable to make your app distinctive and an instant hit with its users. What you will learn Extend the standard UI widget framework by creating Custom views Add complex rendering, animations, and interactions to your views Optimize performance and decrease battery usage Implement custom views to share between multiple projects, or share it publicly Create 3D custom views using OpenGL ES Table of Contents Chapter 1. Getting Started Chapter 2. Implementing Your First Custom View Chapter 3. Handling Events Chapter 4. Advanced 2D Rendering Chapter 5. Introducing 3D Custom Views Chapter 6. Animations Chapter 7. Performance Considerations Chapter 8. Sharing Our Custom View Chapter 9. Implementing Your Own Epg Chapter 10. Building A Charts Component Chapter 11. Creating A 3D Spinning Wheel Menu

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值