Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save keicoder/9380290 to your computer and use it in GitHub Desktop.
Save keicoder/9380290 to your computer and use it in GitHub Desktop.
objective-c : Intermediate Text Kit (master-detail book app for ipad)
//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) ![Alt text](/path/to/image.png)
//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