Created
March 6, 2014 01:13
-
-
Save keicoder/9380290 to your computer and use it in GitHub Desktop.
objective-c : Intermediate Text Kit (master-detail book app for ipad)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Intermediate Text Kit (master-detail book app for ipad) | |
//1. basic UI | |
//AppDelete.h | |
@interface AppDelegate : UIResponder <UIApplicationDelegate> | |
@property (strong, nonatomic) UIWindow *window; | |
@end | |
//AppDelegate.m | |
@implementation AppDelegate | |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions | |
{ | |
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; | |
UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; | |
splitViewController.delegate = (id)navigationController.topViewController | |
return YES; | |
} | |
@end | |
//ChaptersViewController.h (MasterViewController) | |
@class BookViewController; | |
@interface ChaptersViewController : UITableViewController | |
@property (strong, nonatomic) BookViewController *bookViewController; | |
@end | |
//ChaptersViewController.m | |
#import "BookViewController.h" | |
@implementation ChaptersViewController | |
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
self.bookViewController = (BookViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; | |
} | |
#pragma mark - Table View delegate method | |
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView | |
{ | |
return 1; | |
} | |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section | |
{ | |
return 0; | |
} | |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; | |
return cell; | |
} | |
@end | |
//BookViewController.h (DetailViewController) | |
@interface BookViewController : UIViewController <UISplitViewControllerDelegate> | |
@end | |
//BookViewController.m | |
@interface BookViewController () | |
@property (strong, nonatomic) UIPopoverController *masterPopoverController; | |
@end | |
@implementation BookViewController | |
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
self.view.backgroundColor = [UIColor colorWithWhite:0.87f alpha:1.0f]; | |
} | |
#pragma mark - Split view | |
- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController | |
{ | |
barButtonItem.title = @"Chapters"; | |
[self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES]; | |
self.masterPopoverController = popoverController; | |
} | |
- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem | |
{ | |
// Called when the view is shown again in the split view, invalidating the button and popover controller. | |
[self.navigationItem setLeftBarButtonItem:nil animated:YES]; | |
self.masterPopoverController = nil; | |
} | |
@end | |
//2. Rendering the text | |
//AppDelegate.h | |
@interface AppDelegate : UIResponder <UIApplicationDelegate> | |
//... | |
@property (nonatomic, copy) NSAttributedString *bookMarkup; //store the book markup file and formatting in an attributed string | |
@end | |
//AppDelegate.m | |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions | |
{ | |
//... | |
NSString *path = [[NSBundle mainBundle] pathForResource:@"alices_adventures" | |
ofType:@"md"]; | |
NSString *text = [NSString stringWithContentsOfFile:path | |
encoding:NSUTF8StringEncoding error:NULL]; | |
self.bookMarkup = [[NSAttributedString alloc] initWithString:text]; | |
return YES; | |
} | |
//Create a new BookView class, subclass of UIView, will be used to render the book text | |
//BookView.h | |
@interface BookView : UIView | |
@property (nonatomic, copy) NSAttributedString *bookMarkup; //stores the text to be rendered | |
- (void)buildFrames; //creates the Text Kit components required for rendering | |
@end | |
//BookView.m | |
@implementation BookView | |
{ | |
//layout manager: transforming the characters in text storage | |
//into rendered characters (i.e. glyphs) on screen. | |
NSLayoutManager *_layoutManager; | |
} | |
//creates a UITextView with a custom “Text Kit stack” | |
//consisting of an NSTextStorage, NSLayoutManager, and NSTextContainer | |
- (void)buildFrames { | |
// create the text storage | |
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.bookMarkup]; | |
// create the layout manager | |
_layoutManager = [[NSLayoutManager alloc] init]; | |
[textStorage addLayoutManager:_layoutManager]; | |
// create a container | |
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(self.bounds.size.width, FLT_MAX)]; | |
[_layoutManager addTextContainer:textContainer]; | |
// create a view | |
UITextView *textView = [[UITextView alloc] initWithFrame:self.bounds | |
textContainer:textContainer]; | |
textView.scrollEnabled = YES; | |
[self addSubview:textView]; | |
} | |
@end | |
//BookViewController.m | |
//import header | |
#import "BookView.h" | |
#import "AppDelegate.h" | |
//add a new instance variable | |
@implementation BookViewController | |
{ | |
BookView *_bookView; //keep track of an instance of BookView | |
} | |
//replace viewDidLoad with the following implementation | |
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
self.view.backgroundColor = [UIColor colorWithWhite:0.87f alpha:1.0f]; | |
//sets the edges for extended layout, so that the view doesn’t appear under the navigation bar | |
[self setEdgesForExtendedLayout:UIRectEdgeNone]; | |
AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate]; | |
_bookView = [[BookView alloc] initWithFrame:self.view.bounds]; | |
_bookView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | |
_bookView.bookMarkup = appDelegate.bookMarkup; //assigns the markup loaded by the app delegate to _bookView's bookMarkup property | |
[self.view addSubview:_bookView]; | |
} | |
//add the following code just below the viewDidLoad | |
- (void)viewDidLayoutSubviews | |
{ | |
[_bookView buildFrames]; | |
} | |
//3. Adding a multi-column layout | |
//change BookView's superclass from UIView to UIScrollView | |
//BookView.h | |
#import <UIKit/UIKit.h> | |
@interface BookView : UIScrollView | |
//... | |
@end | |
//BookView.m | |
- (void)buildFrames { | |
// create the text storage | |
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.bookMarkup]; | |
// create the layout manager | |
_layoutManager = [[NSLayoutManager alloc] init]; | |
[textStorage addLayoutManager:_layoutManager]; | |
// build the frames | |
NSRange range = NSMakeRange(0, 0); // NSUInteger loc, NSUInteger len | |
NSUInteger containerIndex = 0; | |
while(NSMaxRange(range) < _layoutManager.numberOfGlyphs) { | |
// 1. Create a frame for the view at index | |
CGRect textViewRect = [self frameForViewAtIndex:containerIndex]; //윈도우의 1/2 사이즈 뷰 rect 생성 | |
// 2. Create an instance of NSTextContainer | |
//UITextView adds an 8.0f margin above and below the container | |
CGSize containerSize = CGSizeMake(textViewRect.size.width, textViewRect.size.height - 16.0f); | |
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:containerSize]; | |
[_layoutManager addTextContainer:textContainer]; | |
// 3. Create the UITextView | |
UITextView *textView = [[UITextView alloc] initWithFrame:textViewRect | |
textContainer:textContainer]; | |
[self addSubview:textView]; | |
containerIndex++; | |
// 4. Determine the glyph range for the new text container | |
range = [_layoutManager glyphRangeForTextContainer:textContainer]; //returns the range of glyphs laid out in the given text container. used to determine whether further text containers are required. | |
} | |
// 5. Update the size of the scroll view | |
self.contentSize = CGSizeMake((self.bounds.size.width / 2) * (CGFloat)containerIndex, self.bounds.size.height); | |
self.pagingEnabled = YES; | |
} | |
//compute the frame size for each textView : frame to be a column of half the width of the screen, reduces the margins a bit | |
//then sets the position to be the proper amount from the left of the view based on which column it is | |
- (CGRect)frameForViewAtIndex:(NSUInteger)index { | |
CGRect textViewRect = CGRectMake(0, 0, self.bounds.size.width / 2, self.bounds.size.height); | |
textViewRect = CGRectInset(textViewRect, 10.0, 20.0); // CGRect rect, CGFloat dx, CGFloat dy | |
textViewRect = CGRectOffset(textViewRect, (self.bounds.size.width / 2) * (CGFloat)index, 0.0); | |
return textViewRect; | |
} | |
//4. Adding text styling | |
//apply some appropriate styling to the Markdown formatting | |
//Create a new class. Name the class MarkdownParser, make it a subclass of NSObject | |
//MarkdownParser.h | |
@interface MarkdownParser : NSObject | |
- (NSAttributedString *)parseMarkdownFile:(NSString *)path; | |
@end | |
//MarkdownParser.m | |
@implementation MarkdownParser | |
{ | |
//for storing the various text attributes that are applied to the text in order to style it. | |
NSDictionary *_bodyTextAttributes; | |
NSDictionary *_headingOneAttributes; | |
NSDictionary *_headingTwoAttributes; | |
NSDictionary *_headingThreeAttributes; | |
} | |
- (id) init | |
{ | |
if (self = [super init]) { | |
[self createTextAttributes]; } | |
return self; | |
} | |
- (void)createTextAttributes | |
{ | |
// 1. Create the font descriptors (UIFontDescriptor describe a font with a dictionary of attributes) | |
//Create two font descriptors for the Baskerville family: one normal, and one bold | |
UIFontDescriptor *baskerville = [UIFontDescriptor | |
fontDescriptorWithFontAttributes: @{UIFontDescriptorFamilyAttribute: @"Baskerville"}]; | |
UIFontDescriptor *baskervilleBold = [baskerville | |
fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold]; | |
// 2. determine the current text size preference (user’s text size preferences without using the default font) | |
UIFontDescriptor *bodyFont = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; | |
NSNumber *bodyFontSize = bodyFont.fontAttributes[UIFontDescriptorSizeAttribute]; | |
CGFloat bodyFontSizeValue = [bodyFontSize floatValue]; | |
// 3. create the attributes for the various styles (using Baskerville font and various multiplications of the user’s preferred body text size) | |
_bodyTextAttributes = [self attributesWithDescriptor:baskerville size:bodyFontSizeValue]; | |
_headingOneAttributes = [self attributesWithDescriptor:baskervilleBold size:bodyFontSizeValue * 2.0f]; | |
_headingTwoAttributes = [self attributesWithDescriptor:baskervilleBold size:bodyFontSizeValue * 1.8f]; | |
_headingThreeAttributes = [self attributesWithDescriptor:baskervilleBold size:bodyFontSizeValue * 1.4f]; | |
} | |
//creates a dictionary with a single font attribute | |
- (NSDictionary *)attributesWithDescriptor: (UIFontDescriptor*)descriptor size:(CGFloat)size | |
{ | |
UIFont *font = [UIFont fontWithDescriptor:descriptor size:size]; //returns a font matching the given font descriptor | |
return @{NSFontAttributeName: font}; | |
} | |
- (NSAttributedString *)parseMarkdownFile:(NSString *)path | |
{ | |
NSMutableAttributedString* parsedOutput = [[NSMutableAttributedString alloc] init]; | |
// 1. break the file into lines and iterate over each line | |
NSString *text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; | |
//componentsSeparatedByCharactersInSet: split the text into an array of individual lines. | |
NSArray *lines = [text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; | |
for(NSUInteger lineIndex=0; lineIndex < lines.count; lineIndex++) | |
{ | |
NSString *line = lines[lineIndex]; | |
if ([line isEqualToString:@""]) | |
continue; | |
// 2. match the various 'heading' styles | |
NSDictionary *textAttributes = _bodyTextAttributes; | |
if (line.length > 3) { | |
if ([[line substringToIndex:3] isEqualToString:@"###"]) { | |
textAttributes = _headingThreeAttributes; | |
line = [line substringFromIndex:3]; | |
} else if ([[line substringToIndex:2] isEqualToString:@"##"]) { | |
textAttributes = _headingTwoAttributes; | |
line = [line substringFromIndex:2]; | |
} else if ([[line substringToIndex:1] isEqualToString:@"#"]) { | |
textAttributes = _headingOneAttributes; | |
line = [line substringFromIndex:1]; | |
} | |
} | |
// 3. apply the attributes to line of text determined in step 2. | |
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:line | |
attributes:textAttributes]; | |
// 4. each complete line of text is appended to the output | |
[parsedOutput appendAttributedString:attributedText]; | |
[parsedOutput appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]]; | |
} | |
return parsedOutput; | |
} | |
//AppDelegate.m | |
//add the following import | |
#import "MarkdownParser.h" | |
@implementation AppDelegate | |
//in didFinishLaunchingWithOptions: method, replace with the following code | |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions | |
{ | |
//... | |
// NSString *path = [[NSBundle mainBundle] pathForResource:@"alices_adventures" | |
// ofType:@"md"]; | |
// NSString *text = [NSString stringWithContentsOfFile:path | |
// encoding:NSUTF8StringEncoding error:NULL]; | |
// self.bookMarkup = [[NSAttributedString alloc] initWithString:text]; | |
//replace above code | |
NSString* path = [[NSBundle mainBundle] pathForResource:@"alices_adventures" | |
ofType:@"md"]; | |
MarkdownParser* parser = [[MarkdownParser alloc] init]; | |
self.bookMarkup = [parser parseMarkdownFile:path]; | |
return YES; | |
} | |
@end | |
//5. Performance improvements | |
//preloads the left and right scroll views before the user actually scrolls the view, rather than preloads all views | |
//BookViewController.m | |
//log message to figure out how much app consumes memory and CPU | |
- (void)viewDidLoad | |
{ | |
//... | |
//log message to figure out how much app consumes memory and CPU | |
//see the time differences between viewDidLoad and viewDidAppear | |
//alse memory usage (almost 100mb) | |
NSLog(@"viewDidLoad"); | |
} | |
- (void)viewDidAppear:(BOOL)animated | |
{ | |
[super viewDidAppear:animated]; | |
NSLog(@"viewDidAppear"); | |
} | |
//remove the following lines of code from the buildFrames method | |
- (void)buildFrames | |
{ | |
//... | |
// 3. Create the UITextView | |
// UITextView *textView = [[UITextView alloc] initWithFrame:textViewRect | |
// textContainer:textContainer]; | |
// [self addSubview:textView]; | |
//... | |
} | |
//Add the following to the very end of the buildFrames method: | |
- (void)buildFrames | |
{ | |
//... | |
[self buildViewsForCurrentOffset]; | |
} | |
//add a few utility methods | |
#pragma mark - utility methods | |
//returns all the instances of UITextView that have been added as subviews of BookView | |
- (NSArray *)textSubViews | |
{ | |
NSMutableArray *views = [NSMutableArray new]; | |
for (UIView *subview in self.subviews) { | |
if ([subview class] == [UITextView class]) { | |
[views addObject:subview]; | |
} | |
} | |
return views; | |
} | |
//returns the owning UITextView for the NSTextContainer instance passed in, if one exists | |
- (UITextView *)textViewForContainer:(NSTextContainer *)textContainer | |
{ | |
for (UITextView *textView in [self textSubViews]) { | |
if (textView.textContainer == textContainer) { | |
return textView; | |
} | |
} | |
return nil; | |
} | |
//preloads the left and right scroll views before the user actually scrolls the view | |
- (BOOL)shouldRenderView:(CGRect)viewFrame | |
{ | |
if (viewFrame.origin.x + viewFrame.size.width < (self.contentOffset.x - self.bounds.size.width)) | |
return NO; | |
if (viewFrame.origin.x > (self.contentOffset.x + self.bounds.size.width * 2.0)) | |
return NO; | |
return YES; | |
} | |
//build the appropriate views for the current scroll offset | |
//creates the first four text views. two for the visible page, and two for the page to the right | |
- (void)buildViewsForCurrentOffset | |
{ | |
// 1. Iterate over all instances of NSTextContainer that have been added to the layout manager | |
for(NSUInteger index = 0; index < _layoutManager.textContainers.count; index++) { | |
// 2. Obtain the view that renders this container. textViewForContainer: will return nil if a view is not present | |
NSTextContainer *textContainer = _layoutManager.textContainers[index]; | |
UITextView *textView = [self textViewForContainer:textContainer]; | |
// 3. Determine the frame for this view | |
CGRect textViewRect = [self frameForViewAtIndex:index]; | |
if ([self shouldRenderView:textViewRect]) { | |
// 4. If it should be rendered, check whether it already exists. if not, create it. | |
if (!textView) { | |
NSLog(@"Adding view at index %u", index); | |
UITextView* textView = [[UITextView alloc] initWithFrame:textViewRect textContainer:textContainer]; | |
[self addSubview:textView]; | |
} | |
} else { | |
// 5. If it shouldn’t be rendered, check if it exists already. If it does, remove it. | |
if (textView) { | |
NSLog(@"Deleting view at index %u", index); | |
[textView removeFromSuperview]; | |
} | |
} | |
} | |
} | |
//invoke buildViewsForCurrentOffset method when the user scrolls | |
//BookView.h | |
//adopt the scroll view protocol by adding <UIScrollViewDelegate> | |
@interface BookView : UIScrollView <UIScrollViewDelegate> | |
//initWithFrame method, set the delegate property to reference self | |
- (id)initWithFrame:(CGRect)frame | |
{ | |
//... | |
self.delegate = self; //for invoking buildViewsForCurrentOffset method when the user scrolls | |
} | |
//implement the delegate method that’s invoked when scrolling finishes | |
#pragma mark - UIScrollView 델리게이트 메소드 | |
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView | |
{ | |
[self buildViewsForCurrentOffset]; | |
} | |
//6. Adding a table of contents | |
//Add a new class. Name the class Chapter, make it a subclass of NSObject | |
//Chapter.h | |
@interface Chapter : NSObject | |
@property (nonatomic, copy) NSString *title; | |
@property (nonatomic, assign) NSUInteger location; | |
@end | |
//AppDelegate.h | |
#import <UIKit/UIKit.h> | |
@interface AppDelegate : UIResponder <UIApplicationDelegate> | |
//... | |
@property (nonatomic, strong) NSArray *chapters; //exposes an array of chapters | |
@end | |
//AppDelegate.m | |
#import "Chapter.h" | |
//looks for the “CHAPTER” keyword | |
//and builds up an array of Chapter instances to mark their respective locations in the book based on the offset in the text. | |
- (NSMutableArray *)locateChapters:(NSString *)markdown | |
{ | |
NSMutableArray *chapters = [NSMutableArray new]; | |
[markdown enumerateSubstringsInRange:NSMakeRange(0, markdown.length) | |
options:NSStringEnumerationByLines usingBlock:^(NSString *substring, | |
NSRange substringRange, | |
NSRange enclosingRange, | |
BOOL *stop) { | |
if (substring.length > 7 && [[substring substringToIndex:7] isEqualToString:@"CHAPTER"]) { | |
Chapter *chapter = [Chapter new]; | |
chapter.title = substring; | |
chapter.location = substringRange.location; | |
[chapters addObject:chapter]; | |
} | |
}]; | |
return chapters; | |
} | |
//add the following line to application:didFinishLaunchingWithOptions: | |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions | |
{ | |
//... | |
self.chapters = [self locateChapters:self.bookMarkup.string]; | |
return YES; | |
} | |
//ChaptersViewController.m | |
#import "AppDelegate.h" | |
#import "Chapter.h" | |
#pragma mark - obtains the chapter array from the app delegate | |
//convenience method that obtains the chapter array from the app delegate | |
- (NSArray *)chapters | |
{ | |
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; | |
return appDelegate.chapters; | |
} | |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section | |
{ | |
return [self chapters].count;; | |
} | |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
//... | |
Chapter *chapter = [self chapters][indexPath.row]; | |
cell.textLabel.text = chapter.title; | |
return cell; | |
} | |
//7. Adding chapter navigation | |
//ChaptersViewController.m | |
//ChaptersViewController.m | |
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
Chapter *chapter = [self chapters][indexPath.row]; | |
[self.bookViewController navigateToCharacterLocation:chapter.location]; | |
} | |
//BookViewController.h | |
#import <UIKit/UIKit.h> | |
@interface BookViewController : UIViewController <UISplitViewControllerDelegate> | |
- (void)navigateToCharacterLocation:(NSUInteger)location; | |
@end | |
//BookViewController.m | |
- (void)navigateToCharacterLocation:(NSUInteger)location | |
{ | |
[self.masterPopoverController dismissPopoverAnimated:YES]; | |
[_bookView navigateToCharacterLocation:location]; //relinquishes the responsibility of navigating to the required location to the BookView instance | |
} | |
//BookView.h | |
#import <UIKit/UIKit.h> | |
@interface BookView : UIScrollView <UIScrollViewDelegate> | |
//... | |
- (void)navigateToCharacterLocation:(NSUInteger)location; | |
@end | |
//BookView.m | |
//iterates over each of the NSTextContainer instances associated with the layout manager and obtains the instance’s glyph range. | |
//then performs the critical step of converting the glyph range into a character range. | |
//For each instance of NSTextContainer, checks whether the required location is within the bounds of its characters range. | |
//If so, it applies the scroll view offset and invokes buildViewForCurrentOffset to build the required views. | |
- (void)navigateToCharacterLocation:(NSUInteger)location { | |
CGFloat offset = 0.0f; | |
for (NSTextContainer *container in _layoutManager.textContainers) { | |
NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:container]; | |
NSRange charRange = [_layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil]; | |
if (location >= charRange.location && location < NSMaxRange(charRange)) { | |
//applies the scroll view offset and invokes buildViewForCurrentOffset to build the required views | |
self.contentOffset = CGPointMake(offset, 0); | |
[self buildViewsForCurrentOffset]; | |
return; | |
} | |
offset += self.bounds.size.width / 2.0f; } | |
} | |
//ChaptersViewController.m | |
- (void)awakeFromNib | |
{ | |
self.clearsSelectionOnViewWillAppear = YES; | |
//When YES, the table view controller clears the table’s current selection when it receives a viewWillAppear: message. Setting this property to NO preserves the selection. | |
//... | |
} | |
//8. Adding images | |
//replacing each Markdown image tag with an instance of NSTextAttachment that contains the requisite image. | |
//ex)  | |
//Open MarkdownParser.m and add the following code to the parseMarkdownFile method | |
//MarkdownParser.m | |
- (NSAttributedString*)parseMarkdownFile:(NSString *)path { | |
NSMutableAttributedString* parsedOutput = [[NSMutableAttributedString alloc] init]; | |
//... | |
// locate images | |
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\!\\[.*\\]\\((.*)\\)" | |
options:0 | |
error:nil]; | |
//cast out the escaping backslashes, and consider just the core regular expression, it looks like | |
//\!\[.*\]\((.*)\) | |
//1. \! - match an exclamation mark | |
//2. \[.*\] - followed by some characters surrounded by square brackets, i.e. the alt-text | |
//3. \((.*)\) - followed by some characters surrounded by round brackets, i.e. the image location. | |
NSArray* matches = [regex matchesInString:[parsedOutput string] | |
options:0 | |
range:NSMakeRange(0, parsedOutput.length)]; | |
// iterate over matches in reverse | |
for (NSTextCheckingResult* result in [matches reverseObjectEnumerator]) { | |
NSRange matchRange = [result range]; | |
NSRange captureRange = [result rangeAtIndex:1]; | |
// create an NSTextAttachment instance for each image | |
NSTextAttachment* ta = [NSTextAttachment new]; | |
ta.image = [UIImage imageNamed:[parsedOutput.string substringWithRange:captureRange]]; | |
// image markdown is replaced with an attributed string based on the attachment | |
NSAttributedString* rep = [NSAttributedString attributedStringWithAttachment:ta]; | |
[parsedOutput replaceCharactersInRange:matchRange withAttributedString:rep]; | |
} | |
return parsedOutput; | |
} | |
//9. Adding dictionary lookups | |
//two steps: 1) finding and highlighting tapped words, 2) displaying the dictionary results. | |
//1) Finding and highlighting tapped words | |
//BookView.m | |
//add the following just below the self.delegate = self; statement in initWithFrame: | |
- (id)initWithFrame:(CGRect)frame | |
{ | |
//... | |
// add a tap recognizer | |
UITapGestureRecognizer* recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; | |
[self addGestureRecognizer:recognizer]; | |
//... | |
} | |
@implementation BookView | |
{ | |
//... | |
NSRange _wordCharacterRange; // NSUInteger location, NSUInteger length. stores the character range of the word that was tapped | |
} | |
-(void)handleTap:(UITapGestureRecognizer*)tapRecognizer { | |
NSTextStorage *textStorage = _layoutManager.textStorage; | |
// 1. locate the tapped instance of UITextView | |
CGPoint tappedLocation = [tapRecognizer locationInView:self]; | |
UITextView *tappedTextView = nil; | |
for (UITextView *textView in [self textSubViews]) { | |
if (CGRectContainsPoint(textView.frame, tappedLocation)) { | |
tappedTextView = textView; | |
break; } | |
} | |
if (!tappedTextView) | |
return; | |
// 2. convert the tap point into the coordinate system of the respective view | |
// and subtract the text container’s margin | |
CGPoint subViewLocation = [tapRecognizer locationInView:tappedTextView]; | |
subViewLocation.y -= 8.0; | |
// 3. Determine the index of the tapped glyph using NSLayoutManager | |
// and convert the glyph index into a character index. | |
// This allows you to look up the corresponding character(s) in the text storage. | |
NSUInteger glyphIndex = [_layoutManager glyphIndexForPoint:subViewLocation | |
inTextContainer:tappedTextView.textContainer]; | |
NSUInteger charIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex]; | |
// 4. Determine whether the tapped character is a letter | |
if (![[NSCharacterSet letterCharacterSet] characterIsMember:[textStorage.string | |
characterAtIndex:charIndex]]) | |
return; | |
// 5. Expand the character index into a word range | |
_wordCharacterRange = [self wordThatContainsCharacter:charIndex | |
string:textStorage.string]; | |
// 6. apply a text color attribute to the word range | |
[textStorage addAttribute:NSForegroundColorAttributeName | |
value:[UIColor redColor] | |
range:_wordCharacterRange]; | |
} | |
//calculates the word range by searching backward and forward from the selected index | |
//until it finds the non-letter characters on either side of the word | |
- (NSRange) wordThatContainsCharacter:(NSUInteger)charIndex string:(NSString*)string { | |
NSUInteger startLocation = charIndex; | |
while(startLocation>0 && [[NSCharacterSet letterCharacterSet] | |
characterIsMember:[string characterAtIndex:startLocation-1]]) { | |
startLocation--; | |
} | |
NSUInteger endLocation = charIndex; | |
while(endLocation < string.length && [[NSCharacterSet letterCharacterSet] | |
characterIsMember:[string characterAtIndex:endLocation+1]]) { | |
endLocation++; | |
} | |
return NSMakeRange(startLocation, endLocation-startLocation+1); | |
} | |
//2) Displaying dictionary results | |
//Create a new protocol | |
//Objective-C protocol template -> Name the protocol BookViewDelegate | |
//BookViewDelegate.h | |
#import <Foundation/Foundation.h> | |
@class BookView; | |
@protocol BookViewDelegate <NSObject> | |
//informs delegates of BookView that a word has been tapped | |
- (void)bookView:(BookView *)bookView didHighlightWord:(NSString *)word inRect:(CGRect)rect; | |
@end | |
//BookView.h | |
//import BookViewDelegate.h and add a property | |
#import "BookViewDelegate.h" | |
@interface BookView : UIScrollView <UIScrollViewDelegate> | |
//... | |
@property (nonatomic, weak) id<BookViewDelegate> bookViewDelegate; | |
@end | |
//BookView.m | |
//locate the handleTap: method and add the following to the bottom of the implementation | |
-(void)handleTap:(UITapGestureRecognizer*)tapRecognizer { | |
//... | |
// 1. Obtains the relevant line fragment for the tapped glyph. | |
CGRect rect = [_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex | |
effectiveRange:nil]; | |
// 2. Obtains the location of the first and last glyphs of the tapped word | |
NSRange wordGlyphRange = [_layoutManager glyphRangeForCharacterRange:_wordCharacterRange | |
actualCharacterRange:nil]; | |
CGPoint startLocation = [_layoutManager locationForGlyphAtIndex:wordGlyphRange.location]; | |
CGPoint endLocation = [_layoutManager locationForGlyphAtIndex:NSMaxRange(wordGlyphRange)]; | |
// 3. Calculates the rectangle of the selected word | |
//by using the height of the line fragment and the position of the start and end glyphs in the word | |
CGRect wordRect = CGRectMake(startLocation.x, rect.origin.y, endLocation.x - startLocation.x, rect.size.height); | |
// 4. Converts the resulting rectangle into the coordinate system | |
wordRect = CGRectOffset(wordRect, tappedTextView.frame.origin.x, tappedTextView.frame.origin.y); // CGRect rect, CGFloat dx, CGFloat dy | |
// 5. Adjusts the rectangle by the margin offset, and invokes the newly added delegate method. | |
wordRect = CGRectOffset(wordRect, 0.0, 8.0); | |
NSString* word = [textStorage.string substringWithRange:_wordCharacterRange]; | |
[self.bookViewDelegate bookView:self didHighlightWord:word inRect:wordRect]; | |
} | |
//BookViewController.m | |
#import "BookViewDelegate.h" | |
//adopt BookView delegate, together with the popover delegate: | |
@interface BookViewController () <BookViewDelegate, UIPopoverControllerDelegate> | |
//viewDidLoad method, set the view controller as the book view’s delegate: | |
- (void)viewDidLoad | |
{ | |
//... | |
_bookView.bookViewDelegate = self; //set the view controller as the book view’s delegate | |
} | |
//add instance variable for the popover | |
@implementation BookViewController | |
{ | |
//... | |
UIPopoverController* _popover; //instance variable for the popover | |
} | |
//book view delegate method which is invoked when a word is tapped. | |
#pragma mark - BookViewDelegate method | |
- (void)bookView:(BookView *)bookView didHighlightWord:(NSString *)word inRect:(CGRect)rect | |
{ | |
UIReferenceLibraryViewController *dictionaryVC = [[UIReferenceLibraryViewController alloc] | |
initWithTerm: word]; | |
_popover.contentViewController = dictionaryVC; | |
_popover = [[UIPopoverController alloc] initWithContentViewController:dictionaryVC]; | |
_popover.delegate = self; | |
//‘presented’ at the location of the tapped word | |
[_popover presentPopoverFromRect:rect | |
inView:_bookView | |
permittedArrowDirections:UIPopoverArrowDirectionAny | |
animated:YES]; | |
} | |
- (void)popoverControllerDidDismissPopover: (UIPopoverController *)popoverController | |
{ | |
[_bookView removeWordHighlight]; //informs the BookView instance that the word highlight should be removed. | |
} | |
//BookView.h | |
//add the following method to the interface declaration | |
- (void)removeWordHighlight; //when the popover is closed, word highlight should be removed. | |
//BookView.m | |
- (void)removeWordHighlight | |
{ | |
//when the popover is closed, word highlight should be removed. | |
[_layoutManager.textStorage removeAttribute:NSForegroundColorAttributeName | |
range:_wordCharacterRange]; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment