Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make a part of a UILabel visually be a block quote?

How do I make a specific portion of a UILabel look like a blockquote, or have there be a vertical line on the left side of the text? Would TextKit come in here? If so, how?

Mail.app does this (see the colored portions and the line on the side of them):

enter image description here

How would I replicate this effect without using multiple UILabels (which as I'm creating it dynamically would be rather gross)?

like image 814
Doug Smith Avatar asked Dec 14 '14 07:12

Doug Smith


4 Answers

XIB file

Create a view (XIB) with this general layout like the picture above. There is a UILabel, a UITextView and a UIView (the blue rectangle is a UIView with the background color set). Let's call it ThreadView.xib. Hook up the label, textview and view as properties to the view.

We can then have a method to generate one of these views for us to use and a method to add more ThreadViews as subviews based on how many comments/replies a post has.

+ (instancetype)threadViewWithLabelText:(NSString *)labelText
                           textViewText:(NSString *)textViewText
                                  color:(UIColor *)color
{
    ThreadView *threadView = [[[NSBundle mainBundle] loadNibNamed:@"ThreadView"
                                                            owner:self
                                                          options:nil] firstObject];
    if (threadView) {
        threadView.label.text = labelText;
        threadView.textView.text = textViewText;
        threadView.colorView.backgroundColor = color;
    }
    return threadView;
}

- (void)addCommentView:(ThreadView *)threadView
      toViewController:(UIViewController *)viewController
{
    threadView.frame = CGRectMake(self.frame.origin.x + 25,
                                  self.textView.frame.origin.y + self.textView.frame.size.height,
                                  self.frame.size.width - (self.frame.origin.x + 10),
                                  self.frame.size.height - (self.textView.frame.origin.y + self.textView.frame.size.height));
    [viewController.view addSubview:threadView];
}

Now, in the main view controller, we can create and add these views with just these two method calls:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Load the first post
    ThreadView *originalPost = [ThreadView  threadViewWithLabelText:@"10 Some Words 2014 More Words"
                                                       textViewText:loremIpsum
                                                              color:[UIColor blueColor]];
    originalPost.frame = CGRectMake(self.view.frame.origin.x + 8,
                                    self.view.frame.origin.y + 15,
                                    self.view.frame.size.width - 8,
                                    self.view.frame.size.height - 15);
    [self.view addSubview:originalPost];

    // Load a comment post
    ThreadView *commentPost = [ThreadView threadViewWithLabelText:@"12 December 2014 Maybe A Username"
                                                     textViewText:loremIpsum
                                                            color:[UIColor greenColor]];
    [originalPost addCommentView:commentPost
                toViewController:self];
}

This will give us a result like in the picture below. This code could use some refactoring/restructuring, but this should get you started. You can also mix up use of autolayout and/or setting the frames of the views.

Final result

like image 124
keithbhunter Avatar answered Nov 16 '22 15:11

keithbhunter


Try this?

NSString *html =[NSString stringWithFormat:
@"<html>"
"  <head>"
"    <style type='text/css'>"
"ul"
"{"
"    list-style-type: none;"
"}"
"    </style>"
"  </head>"
"  <body>"
"%@ - PARENT"
"<ul>"
"<li>"
"%@ - CHILD 1"
"</li>"
"<li>"
"%@ - CHILD 2 "
"</li>"
"</ul>"
"</body>"
"</html>"
,@"Parent Title", @"Child Description 1", @"Child Description 2"];


NSError *err = nil;
_label.attributedText =
[[NSAttributedString alloc]
 initWithData: [html dataUsingEncoding:NSUTF8StringEncoding]
 options: @{ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType }
 documentAttributes: nil
 error: &err];
if(err)
    NSLog(@"Unable to parse label text: %@", err);

Result

And the Result is like this .

like image 23
Prajeet Shrestha Avatar answered Nov 16 '22 15:11

Prajeet Shrestha


This can be easily done with Text Kit. I do stuff like this in my app. The difference is I use boxes (nested if needed) to mark each text block. Here is what you should do:

  1. Parse html string (or whatever you use to mark text), mark each text block quote with a custom attribute, like MyTextBlockAttribute, save ranges of each text block (i.e. block quote) and add it as a attribute to the related range of the attributed string(construct this attributed string from your content) and the list attached to the content. Lets call this list MyTextBlockList.

  2. draw text with Text Kit yourself. draw background first (white color, light gray color.. etc, whatever), draw text or vertical lines next. Since you can get each text block's range by loop through the list, you can get bounding rect of these blocks with method [NSLayoutManager range: inTextContainer:textContainer].

Here is the code I used in my app:

// subclass of NSTextContainer
#import "MyTextContainer.h"
#import "MyBlockAttribute.h"
@interface MyTextContainer ()
@property (nonatomic) BOOL isBlock;
@end

@implementation MyTextContainer
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect
                                  atIndex:(NSUInteger)characterIndex
                         writingDirection:(NSWritingDirection)baseWritingDirection
                            remainingRect:(CGRect *)remainingRect {

    CGRect output = [super lineFragmentRectForProposedRect:proposedRect
                                                   atIndex:characterIndex
                                          writingDirection:baseWritingDirection
                                             remainingRect:remainingRect];

    NSUInteger length = self.layoutManager.textStorage.length;

    MyTextBlockAttribute *blockAttribute;
    if (characterIndex < length) {
        blockAttribute = [self.layoutManager.textStorage attribute:MyTextBlockAttributeName atIndex:characterIndex effectiveRange:NULL]; // MyTextBlockAttributeName is a global NSString constant
    }

    if (blockAttribute) { // text block detected, enter "block" layout mode!
        output = CGRectInset(output, blockAttribute.padding, 0.0f); // set the padding when  constructing the attributed string from raw html string, use padding to control nesting, inner boxes have bigger padding, again, this is done in parsing pass
        if (!self.isBlock) {
            self.isBlock = YES;
            output = CGRectOffset(output, 0.0f, blockAttribute.padding);
        }
    } else if (self.isBlock) {
        self.isBlock = NO; // just finished a block, return back to the "normal" layout mode
    }

    // no text block detected, not just finished a block either, do nothing, just return super implementation's output

    return output;
}


@end


// drawing code, with drawRect: or other drawing technique, like drawing into bitmap context, doesn't matter
- (void)drawBlockList:(NSArray *)blockList content:(MyContent *)content {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 0.5f);
    [[UIColor colorWithWhite:0.98f alpha:1.0f] setFill];
    CGContextSaveGState(context);
    MyTextContainer *textContainer = content.textContainer;

    // since I draw boxes, I have to draw inner text block first, so use reverse enumerator
    for (MyTextBlockAttribute *blockAttribute in [blockList reverseObjectEnumerator]) {
        if (blockAttribute.noBackground) { // sometimes I don't draw boxes in some conditions
            continue;
        }

        CGRect frame = CGRectIntegral([content.layoutManager boundingRectForGlyphRange:blockAttribute.range inTextContainer:textContainer]);
        frame.size.width = textContainer.size.width - 2 * (blockAttribute.padding - MyDefaultMargin); // yeah... there is some margin around the boxes, like html's box model, just some simple math to calculate the accurate rectangles of text blocks
        frame.origin.x = blockAttribute.padding - MyDefaultMargin;
        frame = CGRectInset(frame, 0, -MyDefaultMargin);
        if (blockAttribute.backgroundColor) { // some text blocks may have specific background color
            CGContextSaveGState(context);
            [blockAttribute.backgroundColor setFill];
            CGContextFillRect(context, frame);
            CGContextRestoreGState(context);
        } else {
            CGContextFillRect(context, frame);
        }

        CGContextStrokeRect(context, frame); // draw borders of text blocks in the last
    }
    CGContextRestoreGState(context);
}


- (UIImage *)drawContent:(MyContent *)content {
    UIImage *output;
    UIGraphicsBeginImageContextWithOptions(content.bounds.size, YES, 0.0f); // bounds is calculated in other places
    [[UIColor whiteColor] setFill];
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:content.bounds];
    [path fill];

    [self drawBlockList:content.blockList content:content]; // draw background first!
    [content.layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, content.textStorage.length) atPoint:CGPointZero]; // every content object has a set of Text Kit core objects, textStorage, textContainer, layoutManager

    output = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return output;
}

In your case, you don't draw boxes, you draw left borders instead. The technique is the same, hope this can help you!

like image 3
wcd Avatar answered Nov 16 '22 15:11

wcd


If you are targeting iOS lower than 7, You can do somethingsimilar by using Core Text, but since Core Text is kind of old C opaque types implementation, I suggest you to use DTCoreText.
If you are using >=iOS7 you can use NSAttributed string and NSXMLDocument. Even if attributed string are available from 3.x they only added them into UIKIT objects into ios6 and changed radically the UIKit behavior in managing them into iOS7.
NSXMLDocument it's helpful because you can render your string representing them as HTML.

like image 2
Andrea Avatar answered Nov 16 '22 13:11

Andrea