Skip to content

Instantly share code, notes, and snippets.

Created December 30, 2012 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/4413906 to your computer and use it in GitHub Desktop.
Save anonymous/4413906 to your computer and use it in GitHub Desktop.
//
// DemoTextViewController.m
// CoreTextExtensions
//
// Created by Oliver Drobnik on 1/9/11.
// Copyright 2011 Drobnik.com. All rights reserved.
//
#import "DemoTextViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <MediaPlayer/MediaPlayer.h>
#import "DTVersion.h"
@interface DemoTextViewController ()
- (void)_segmentedControlChanged:(id)sender;
- (void)linkPushed:(DTLinkButton *)button;
- (void)linkLongPressed:(UILongPressGestureRecognizer *)gesture;
- (void)debugButton:(UIBarButtonItem *)sender;
@property (nonatomic, strong) NSMutableSet *mediaPlayers;
@end
@implementation DemoTextViewController
{
NSString *_fileName;
UISegmentedControl *_segmentedControl;
DTAttributedTextView *_textView;
UITextView *_rangeView;
UITextView *_charsView;
UITextView *_htmlView;
UITextView *_iOS6View;
NSURL *baseURL;
// private
NSURL *lastActionLink;
NSMutableSet *mediaPlayers;
}
#pragma mark NSObject
- (id)init
{
self = [super init];
if (self)
{
NSMutableArray *items = [[NSMutableArray alloc] initWithObjects:@"View", @"Ranges", @"Chars", @"HTML", nil];
if (![DTVersion osVersionIsLessThen:@"6.0"])
{
[items addObject:@"iOS 6"];
}
_segmentedControl = [[UISegmentedControl alloc] initWithItems:items];
_segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
_segmentedControl.selectedSegmentIndex = 0;
[_segmentedControl addTarget:self action:@selector(_segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
self.navigationItem.titleView = _segmentedControl;
UIBarButtonItem *debug = [[UIBarButtonItem alloc] initWithTitle:@"Debug Frames" style:UIBarButtonItemStyleBordered target:self action:@selector(debugButton:)];
NSArray *toolbarItems = [NSArray arrayWithObject:debug];
[self setToolbarItems:toolbarItems];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
#pragma mark UIViewController
- (void)loadView {
[super loadView];
CGRect frame = CGRectMake(0.0, 0.0, self.view.frame.size.width, self.view.frame.size.height);
// Create chars view
_charsView = [[UITextView alloc] initWithFrame:frame];
_charsView.editable = NO;
_charsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_charsView];
// Create range view
_rangeView = [[UITextView alloc] initWithFrame:frame];
_rangeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_rangeView.editable = NO;
[self.view addSubview:_rangeView];
// Create html view
_htmlView = [[UITextView alloc] initWithFrame:frame];
_htmlView.editable = NO;
_htmlView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_htmlView];
// Create text view
[DTAttributedTextContentView setLayerClass:[CATiledLayer class]];
_textView = [[DTAttributedTextView alloc] initWithFrame:frame];
_textView.textDelegate = self;
_textView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_textView.contentView.shouldDrawImages = YES;
[self.view addSubview:_textView];
// create a text view to for testing iOS 6 compatibility
// Create html view
_iOS6View = [[UITextView alloc] initWithFrame:frame];
_iOS6View.editable = NO;
_iOS6View.contentInset = UIEdgeInsetsMake(10, 0, 10, 0);
_iOS6View.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_iOS6View];
}
- (NSAttributedString *)_attributedStringForSnippetUsingiOS6Attributes:(BOOL)useiOS6Attributes
{
// Load HTML data
NSString *readmePath = [[NSBundle mainBundle] pathForResource:_fileName ofType:nil];
NSString *html = [NSString stringWithContentsOfFile:readmePath encoding:NSUTF8StringEncoding error:NULL];
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
// Create attributed string from HTML
CGSize maxImageSize = CGSizeMake(self.view.bounds.size.width - 20.0, self.view.bounds.size.height - 20.0);
// example for setting a willFlushCallback, that gets called before elements are written to the generated attributed string
void (^callBackBlock)(DTHTMLElement *element) = ^(DTHTMLElement *element) {
// if an element is larger than twice the font size put it in it's own block
if (element.displayStyle == DTHTMLElementDisplayStyleInline && element.textAttachment.displaySize.height > 2.0 * element.fontDescriptor.pointSize)
{
element.displayStyle = DTHTMLElementDisplayStyleBlock;
}
};
NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:1.0], NSTextSizeMultiplierDocumentOption, [NSValue valueWithCGSize:maxImageSize], DTMaxImageSize,
@"Times New Roman", DTDefaultFontFamily, @"purple", DTDefaultLinkColor, callBackBlock, DTWillFlushBlockCallBack, nil];
if (useiOS6Attributes)
{
[options setObject:[NSNumber numberWithBool:YES] forKey:DTUseiOS6Attributes];
}
NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:data options:options documentAttributes:NULL];
return string;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Display string
_textView.contentView.edgeInsets = UIEdgeInsetsMake(10, 10, 10, 10);
_textView.contentView.shouldDrawLinks = NO; // we draw them in DTLinkButton
_textView.attributedString = [self _attributedStringForSnippetUsingiOS6Attributes:NO];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect bounds = self.view.bounds;
_textView.frame = bounds;
[self _segmentedControlChanged:nil];
[_textView setContentInset:UIEdgeInsetsMake(0, 0, 44, 0)];
[_textView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 44, 0)];
[self.navigationController setToolbarHidden:NO animated:YES];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// now the bar is up so we can autoresize again
_textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
- (void)viewWillDisappear:(BOOL)animated;
{
[self.navigationController setToolbarHidden:YES animated:YES];
// stop all playing media
for (MPMoviePlayerController *player in self.mediaPlayers)
{
[player stop];
}
[super viewWillDisappear:animated];
}
#pragma mark Private Methods
- (void)updateDetailViewForIndex:(NSUInteger)index
{
switch (index)
{
case 1:
{
NSMutableString *dumpOutput = [[NSMutableString alloc] init];
NSDictionary *attributes = nil;
NSRange effectiveRange = NSMakeRange(0, 0);
if ([_textView.attributedString length])
{
while ((attributes = [_textView.attributedString attributesAtIndex:effectiveRange.location effectiveRange:&effectiveRange]))
{
[dumpOutput appendFormat:@"Range: (%d, %d), %@\n\n", effectiveRange.location, effectiveRange.length, attributes];
effectiveRange.location += effectiveRange.length;
if (effectiveRange.location >= [_textView.attributedString length])
{
break;
}
}
}
_rangeView.text = dumpOutput;
break;
}
case 2:
{
// Create characters view
NSMutableString *dumpOutput = [[NSMutableString alloc] init];
NSData *dump = [[_textView.attributedString string] dataUsingEncoding:NSUTF8StringEncoding];
for (NSInteger i = 0; i < [dump length]; i++)
{
char *bytes = (char *)[dump bytes];
char b = bytes[i];
[dumpOutput appendFormat:@"%i: %x %c\n", i, b, b];
}
_charsView.text = dumpOutput;
break;
}
case 3:
{
_htmlView.text = [_textView.attributedString htmlString];
break;
}
case 4:
{
if (![_iOS6View.attributedText length])
{
_iOS6View.attributedText = [self _attributedStringForSnippetUsingiOS6Attributes:YES];
}
}
}
}
- (void)_segmentedControlChanged:(id)sender {
UIScrollView *selectedView = _textView;
switch (_segmentedControl.selectedSegmentIndex) {
case 1:
selectedView = _rangeView;
break;
case 2:
selectedView = _charsView;
break;
case 3:
{
selectedView = _htmlView;
break;
}
case 4:
{
selectedView = _iOS6View;
break;
}
}
// refresh only this tab
[self updateDetailViewForIndex:_segmentedControl.selectedSegmentIndex];
[self.view bringSubviewToFront:selectedView];
[selectedView flashScrollIndicators];
}
#pragma mark Custom Views on Text
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttributedString:(NSAttributedString *)string frame:(CGRect)frame
{
NSDictionary *attributes = [string attributesAtIndex:0 effectiveRange:NULL];
NSURL *URL = [attributes objectForKey:DTLinkAttribute];
NSString *identifier = [attributes objectForKey:DTGUIDAttribute];
DTLinkButton *button = [[DTLinkButton alloc] initWithFrame:frame];
button.URL = URL;
button.minimumHitSize = CGSizeMake(25, 25); // adjusts it's bounds so that button is always large enough
button.GUID = identifier;
// we draw the contents ourselves
button.attributedString = string;
// make a version with different text color
NSMutableAttributedString *highlightedString = [string mutableCopy];
NSRange range = NSMakeRange(0, highlightedString.length);
NSDictionary *highlightedAttributes = [NSDictionary dictionaryWithObject:(__bridge id)[UIColor redColor].CGColor forKey:(id)kCTForegroundColorAttributeName];
[highlightedString addAttributes:highlightedAttributes range:range];
button.highlightedAttributedString = highlightedString;
// use normal push action for opening URL
[button addTarget:self action:@selector(linkPushed:) forControlEvents:UIControlEventTouchUpInside];
// demonstrate combination with long press
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(linkLongPressed:)];
[button addGestureRecognizer:longPress];
return button;
}
- (UIView *)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView viewForAttachment:(DTTextAttachment *)attachment frame:(CGRect)frame
{
if (attachment.contentType == DTTextAttachmentTypeVideoURL)
{
NSURL *url = (id)attachment.contentURL;
// we could customize the view that shows before playback starts
UIView *grayView = [[UIView alloc] initWithFrame:frame];
grayView.backgroundColor = [DTColor blackColor];
// find a player for this URL if we already got one
MPMoviePlayerController *player = nil;
for (player in self.mediaPlayers)
{
if ([player.contentURL isEqual:url])
{
break;
}
}
if (!player)
{
player = [[MPMoviePlayerController alloc] initWithContentURL:url];
[self.mediaPlayers addObject:player];
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_4_2
NSString *airplayAttr = [attachment.attributes objectForKey:@"x-webkit-airplay"];
if ([airplayAttr isEqualToString:@"allow"])
{
if ([player respondsToSelector:@selector(setAllowsAirPlay:)])
{
player.allowsAirPlay = YES;
}
}
#endif
NSString *controlsAttr = [attachment.attributes objectForKey:@"controls"];
if (controlsAttr)
{
player.controlStyle = MPMovieControlStyleEmbedded;
}
else
{
player.controlStyle = MPMovieControlStyleNone;
}
NSString *loopAttr = [attachment.attributes objectForKey:@"loop"];
if (loopAttr)
{
player.repeatMode = MPMovieRepeatModeOne;
}
else
{
player.repeatMode = MPMovieRepeatModeNone;
}
NSString *autoplayAttr = [attachment.attributes objectForKey:@"autoplay"];
if (autoplayAttr)
{
player.shouldAutoplay = YES;
}
else
{
player.shouldAutoplay = NO;
}
[player prepareToPlay];
player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
player.view.frame = grayView.bounds;
[grayView addSubview:player.view];
return grayView;
}
/*else if (attachment.contentType == DTTextAttachmentTypeImage)
{
// if the attachment has a hyperlinkURL then this is currently ignored
DTLazyImageView *imageView = [[DTLazyImageView alloc] initWithFrame:frame];
imageView.delegate = self;
if (attachment.contents)
{
imageView.image = attachment.contents;
}
// url for deferred loading
imageView.url = attachment.contentURL;
// if there is a hyperlink then add a link button on top of this image
if (attachment.hyperLinkURL)
{
// NOTE: this is a hack, you probably want to use your own image view and touch handling
// also, this treats an image with a hyperlink by itself because we don't have the GUID of the link parts
imageView.userInteractionEnabled = YES;
DTLinkButton *button = (DTLinkButton *)[self attributedTextContentView:attributedTextContentView viewForLink:attachment.hyperLinkURL identifier:attachment.hyperLinkGUID frame:imageView.bounds];
[imageView addSubview:button];
}
return imageView;
}*/
else if (attachment.contentType == DTTextAttachmentTypeIframe)
{
frame.origin.x += 50;
DTWebVideoView *videoView = [[DTWebVideoView alloc] initWithFrame:frame];
videoView.attachment = attachment;
return videoView;
}
else if (attachment.contentType == DTTextAttachmentTypeObject)
{
// somecolorparameter has a HTML color
UIColor *someColor = [UIColor colorWithHTMLName:[attachment.attributes objectForKey:@"somecolorparameter"]];
UIView *someView = [[UIView alloc] initWithFrame:frame];
someView.backgroundColor = someColor;
someView.layer.borderWidth = 1;
someView.layer.borderColor = [UIColor blackColor].CGColor;
return someView;
}
return nil;
}
- (BOOL)attributedTextContentView:(DTAttributedTextContentView *)attributedTextContentView shouldDrawBackgroundForTextBlock:(DTTextBlock *)textBlock frame:(CGRect)frame context:(CGContextRef)context forLayoutFrame:(DTCoreTextLayoutFrame *)layoutFrame
{
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:10];
CGColorRef color = [textBlock.backgroundColor CGColor];
if (color)
{
CGContextSetFillColorWithColor(context, color);
CGContextAddPath(context, [roundedRect CGPath]);
CGContextFillPath(context);
CGContextAddPath(context, [roundedRect CGPath]);
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1);
CGContextStrokePath(context);
return NO;
}
return YES; // draw standard background
}
#pragma mark Actions
- (void)linkPushed:(DTLinkButton *)button
{
NSURL *URL = button.URL;
if ([[UIApplication sharedApplication] canOpenURL:[URL absoluteURL]])
{
[[UIApplication sharedApplication] openURL:[URL absoluteURL]];
}
else
{
if (![URL host] && ![URL path])
{
// possibly a local anchor link
NSString *fragment = [URL fragment];
if (fragment)
{
[_textView scrollToAnchorNamed:fragment animated:NO];
}
}
}
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex != actionSheet.cancelButtonIndex)
{
[[UIApplication sharedApplication] openURL:[self.lastActionLink absoluteURL]];
}
}
- (void)linkLongPressed:(UILongPressGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan)
{
DTLinkButton *button = (id)[gesture view];
button.highlighted = NO;
self.lastActionLink = button.URL;
if ([[UIApplication sharedApplication] canOpenURL:[button.URL absoluteURL]])
{
UIActionSheet *action = [[UIActionSheet alloc] initWithTitle:[[button.URL absoluteURL] description] delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Open in Safari", nil];
[action showFromRect:button.frame inView:button.superview animated:YES];
}
}
}
- (void)debugButton:(UIBarButtonItem *)sender
{
_textView.contentView.drawDebugFrames = !_textView.contentView.drawDebugFrames;
[DTCoreTextLayoutFrame setShouldDrawDebugFrames:_textView.contentView.drawDebugFrames];
[self.view setNeedsDisplay];
}
#pragma mark DTLazyImageViewDelegate
- (void)lazyImageView:(DTLazyImageView *)lazyImageView didChangeImageSize:(CGSize)size {
NSURL *url = lazyImageView.url;
CGSize imageSize = size;
NSPredicate *pred = [NSPredicate predicateWithFormat:@"contentURL == %@", url];
// update all attachments that matchin this URL (possibly multiple images with same size)
for (DTTextAttachment *oneAttachment in [_textView.contentView.layoutFrame textAttachmentsWithPredicate:pred])
{
oneAttachment.originalSize = imageSize;
if (!CGSizeEqualToSize(imageSize, oneAttachment.displaySize))
{
oneAttachment.displaySize = imageSize;
}
}
// redo layout
// here we're layouting the entire string, might be more efficient to only relayout the paragraphs that contain these attachments
[_textView.contentView relayoutText];
}
#pragma mark Properties
- (NSMutableSet *)mediaPlayers
{
if (!mediaPlayers)
{
mediaPlayers = [[NSMutableSet alloc] init];
}
return mediaPlayers;
}
@synthesize fileName = _fileName;
@synthesize lastActionLink;
@synthesize mediaPlayers;
@synthesize baseURL;
@end
<body style="text-shadow: 0px 0px 3px red;">
<h1>Image Handling</h1>
<p>Some basic <b>image</b> handling has been implemented</p>
<h2>Inline</h2>
<p>So far <img src="icon_smile.gif"> images work inline, sitting on the line's baseline. There is a workaround in place that if an image is more than twice as high as the surrounding text it will be treated as it's own block.</p>
<h2>Block</h2>
<p>There is a known issue with images as blocks, outside of p tags.</p>
<img class="Bla" style="width:150px; height:150px" src="Oliver.jpg">
<p>An Image outside of P is treated as a paragraph</p>
<p><img style="float:right;" width="100" height="100" src="Oliver.jpg">The previous image has float style. As a <span style="background-color:yellow;">Workaround</span> a newline is added after it until we can support floating in the layouting. This is done if the inline image is more than 5 times as large as the current font pixel size. This should allow small inline images, like smileys to stay in line, while most float images would probably be larger than this.</p>
<h2>Supported Attributes</h2>
<ul>
<li><b>width</b>, <b>height</b> in pixels</li>
<li><b>src</b> with a local file path relative to the app bundle</li>
</ul>
<h2>Supported Image Formats</h2>
<p>According to <a href="http://developer.apple.com/library/ios/#documentation/2DDrawing/Conceptual/DrawingPrintingiOS/Images/Images.html">Apple</a> the following image formats are supported for use with UIImage:</p>
<ul>
<li>png</li>
<li>tif, tiff</li>
<li>jpeg, jpg</li>
<li>gif</li>
<li>bmp, bmpf</li>
<li>ico</li>
<li>cur</li>
<li>xbm</li>
</ul>
<h2>Vertical Alignment</h2>
<p>Limited support for the CSS vertical-align tag exists</p>
<p style="font-size:20px">Baseline: <img style="vertical-align:baseline;" height=40 width=40 src="icon_smile.gif"></p>
<p style="font-size:20px">text-top: <img style="vertical-align:text-top;" height=40 width=40 src="icon_smile.gif"></p>
<p style="font-size:20px">text-bottom: <img style="vertical-align:text-bottom;" height=40 width=40 src="icon_smile.gif"></p>
<p style="font-size:20px">middle: <img style="vertical-align:middle;" height=40 width=40 src="icon_smile.gif"></p>
<h2>Data Source</h2>
<p>Base64 encoded data SRC is supported, for example this red dot: <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" /></p>
<p>Another example:</p><img src="data:image/gif;base64,R0lGODlhDwAPAKECAAAAzMzM/////
wAAACwAAAAADwAPAAACIISPeQHsrZ5ModrLlN48CXF8m2iQ3YmmKqVlRtW4ML
wWACH+H09wdGltaXplZCBieSBVbGVhZCBTbWFydFNhdmVyIQAAOw=="
alt="Base64 encoded image" width="100" height="100"/>
<h2>Remote Images</h2>
<p>Images can also be loaded from remote URLs, even without specifying a size in the HTML. The code demonstrates how to update the DTTextAttachment's display size after download and triggering a re-layout.</p>
<img src="https://si0.twimg.com/profile_images/1401231905/Cocoanetics_Square_reasonably_small.jpg">
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment