How do I add tab stops to an NSAttributedString and display in a UITextView
Asked Answered
C

4

7

I'm creating an iOS app, and I would like to display an attributed string with specific tab stops specified in a UITextView. I would also like to draw them directly into UIViews using Core Text in drawRect. I would like (if possible) for the same attributed string to be used in both scenarios.

So, in my app, I create an CFAttributedStringRef or an NSAttributedString and apply a CTParagraphStyle attribute to it. Then, I attempt to display the attributed string in a UITextView and I get a crash like the following:

-[__NSCFType headIndent]: unrecognized selector sent to instance 0x7545080

po 0x7545080
$0 = 122966144 CTParagraphStyle:
base writing direction = -1, alignment = 4, line break mode = 0, 
   default tab interval = 0
first line head indent = 0, head indent = 0, tail indent = 0
line height multiple = 0, maximum line height = 0, minimum line height = 0
line spacing adjustment = 0, paragraph spacing = 0, 
   paragraph spacing before = 0
tabs:
(
   "CTTextTab: location = 20, alignment = 0, options = (none)\n",
   "CTTextTab: location = 40, alignment = 0, options = (none)\n",
   "CTTextTab: location = 60, alignment = 0, options = (none)\n",
   "CTTextTab: location = 80, alignment = 0, options = (none)\n",
   "CTTextTab: location = 100, alignment = 0, options = (none)\n",
   "CTTextTab: location = 120, alignment = 0, options = (none)\n"
)

I understand what is going on, but I am wondering if I have any alternative way of doing what I'd like to. NSMutableParagraphStyle on iOS does not have tab stop support, yet I notice that NSMutableParagraphStyle on OS X does have this capability. This leads me to think that the iOS NSMutableParagraphStyle may one day support this.

In the meantime, is there way to add tab stops to a CFAttributedStringRef or an NSAttributedString and still have a UITextView display it?

The source in question is:

- (void)viewWillAppear:(BOOL)animated
{
   CFDictionaryRef attrs = (__bridge CFDictionaryRef) @{};
   CFAttributedStringRef a = CFAttributedStringCreate(
       kCFAllocatorDefault, CFSTR("a\tb\tc\td"), attrs);

   CFMutableAttributedStringRef attrStr;
   attrStr = CFAttributedStringCreateMutableCopy(
       kCFAllocatorDefault, CFAttributedStringGetLength(a), a);

   CFArrayRef tabStops = (__bridge CFArrayRef) @[
       (__bridge id) CTTextTabCreate(0, 20, NULL),
       (__bridge id) CTTextTabCreate(0, 40, NULL),
       (__bridge id) CTTextTabCreate(0, 60, NULL),
       (__bridge id) CTTextTabCreate(0, 80, NULL),
       (__bridge id) CTTextTabCreate(0, 100, NULL),
       (__bridge id) CTTextTabCreate(0, 120, NULL)];

   const CTParagraphStyleSetting paraSettings[] = {
       {kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops},
   };

   CTParagraphStyleRef paraStyle = CTParagraphStyleCreate(
       paraSettings, 
       sizeof(paraSettings) / sizeof(CTParagraphStyleSetting));

   CFRange range = CFRangeMake(0, CFAttributedStringGetLength(attrStr));
   CFAttributedStringSetAttribute(
       attrStr, range, kCTParagraphStyleAttributeName, paraStyle);

   CFRelease(paraStyle);
   CFIndex i, count = CFArrayGetCount(tabStops);
   for (i = 0; i < count; i++) {
       CFRelease(CFArrayGetValueAtIndex(tabStops, i));
   }

   [[self textView] setAttributedText:
       (__bridge NSAttributedString *)(attrStr)];
}
Centner answered 19/3, 2013 at 21:37 Comment(0)
E
11

In iOS 7 you can do it like this:

UIFont *font = [UIFont systemFontOfSize:18.0];
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
NSInteger cnt;
CGFloat tabInterval = 72.0;
paragraphStyle.defaultTabInterval = tabInterval;
NSMutableArray *tabs = [NSMutableArray array];
for (cnt = 1; cnt < 13; cnt++) {    // Add 12 tab stops, at desired intervals...
    [tabs addObject:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:tabInterval * cnt options:nil]];
}
paragraphStyle.tabStops = tabs;
NSDictionary *attributes = @{ NSFontAttributeName: font, NSParagraphStyleAttributeName: paragraphStyle};
Estuary answered 22/9, 2013 at 20:58 Comment(1)
paragraphStyle.defaultTabInterval doesn't do anything :/Erigena
G
4

This is the Swift version that worked for me:

    let tablInterval: CGFloat = 85.0
    let paragraphStyle = NSMutableParagraphStyle()
    let terms = NSTextTab.columnTerminatorsForLocale(NSLocale.currentLocale())
    let tabStop0 = NSTextTab(textAlignment: .Right, location: 0, options: [NSTabColumnTerminatorsAttributeName:terms])
    let tabStop1 = NSTextTab(textAlignment: .Right, location: tablInterval, options: [NSTabColumnTerminatorsAttributeName:terms])
    let tabStop2 = NSTextTab(textAlignment: .Right, location: tablInterval*2, options: [NSTabColumnTerminatorsAttributeName:terms])
    let tabStop3 = NSTextTab(textAlignment: .Right, location: tablInterval*3, options: [NSTabColumnTerminatorsAttributeName:terms])
    paragraphStyle.addTabStop(tabStop0)
    paragraphStyle.addTabStop(tabStop1)
    paragraphStyle.addTabStop(tabStop2)
    paragraphStyle.addTabStop(tabStop3)

    let attributedString = NSMutableAttributedString(string:text)
    attributedString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:rangeAll)
Garbe answered 10/12, 2015 at 21:21 Comment(2)
What do the options you included do?Defalcate
This answer may work in some cases, but did not work for me, due to it adding to, rather than replacing, the default array of text tabs that are already in each new paragragh style. See my imporoved/corrected Swift answer.Drastic
D
2

Improved/Corrected Answer for Swift:

let tablInterval: CGFloat = 20.0
var tabsArray = [NSTextTab]()
for i in 1...6 {
    tabsArray.append(NSTextTab(textAlignment: .Left, location: i*tabInterval, options: [:]))
}
let paraStyle = NSMutableParagraphStyle()
paraStyle.tabStops = tabsArray
textView.attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.ParagraphStyle: paraStyle])

Why this extra Swift Answer?

The earlier Swift answer is flawed because it adds tab stops without first removing the default tab stops (12 left-aligned tab stops at 28 pt spacing). Therefore it adds a new set of tab stops to the default set of tab stops instead of replacing them with the new set of tab stops. Using code based on that answer caused me a great deal of frustration trying to figure out why it did not work for me, until I went back to the documentation for NSMutableParagraphStyle.tabStops and read about the default tab stops:

The default value is an array of 12 left-aligned tabs at 28-point intervals.

This answer corrects this, and is made more succinct by removing the (unecessary?) 'options' as well as using a loop (which is only suitable in cases where all the tabs are at consistent spacing and all with the same alignment type).

Drastic answered 10/2, 2020 at 0:46 Comment(0)
I
-2

ios 6.1 added NSParagraphStyle but it seem the CTParagraphStyleRef and NSParagraphStyle are not toll free bridging.

As result if you have CTParagraphStyleRef in NSAttributeString it causes:

[__NSCFType headIndent]: unrecognized selector sent to instance xxxx

and other methods for NSParagraphStyle such as alignment.

Identical answered 18/6, 2013 at 0:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.