Skip to content

Instantly share code, notes, and snippets.

@kreeger
Created October 25, 2012 16:32
Show Gist options
  • Save kreeger/3953873 to your computer and use it in GitHub Desktop.
Save kreeger/3953873 to your computer and use it in GitHub Desktop.
Displaying Markdown on Cocoa Touch.

In writing my first iOS app, I found myself reaching out in a few areas of self-discovery regarding the Cocoa Touch framework and what it's capable of, especially in how it compares to Ruby and it's standard library (which is what I've been used to for the past few years of my life). I've grown to love Ruby mixins and monkey-patching, and was delighted to learn that Objective-C has something quite similar: categories. ClassName+CategoryName.{h,m} is all you need, and you can define new class/static and instance methods on an Objective-C class.

One of my other strong preferences is for the Markdown format, and thus I snagged one of the more popular C-based implementations of Markdown (named Sundown) and wrote a category around UIWebView for effortlessly displaying a parsed file. This was all made possible in the first place by an awesome post on Stack Overflow on using Sundown with Objective-C.

//
//  UIWebView+Markdown.h
//

#import <UIKit/UIKit.h>

@interface UIWebView (Markdown)

+ (id)webViewWithContentsOfMarkdownFile:(NSString *)markdownFilePath;
+ (id)webViewWithContentsOfMarkdownFile:(NSString *)markdownFilePath withCSSPath:(NSString *)cssFilePath;

@end

//
//  UIWebView+Markdown.m
//

#import "UIWebView+Markdown.h"
#include "markdown.h"
#include "html.h"
#include "buffer.h"

@implementation UIWebView (Markdown)

+ (id)webViewWithContentsOfMarkdownFile:(NSString *)markdownFilePath {
    return [self webViewWithContentsOfMarkdownFile:markdownFilePath
                                       withCSSPath:[[NSBundle mainBundle] pathForResource:@"webview" ofType:@"css"]];
}

+ (id)webViewWithContentsOfMarkdownFile:(NSString *)markdownFilePath withCSSPath:(NSString *)cssFilePath {
    UIWebView *instance = [[[self class] alloc] init];
    if (!instance) return nil;
    
    // Grab the Markdown text, convert it to a C char.
    NSString *rawMarkdown = [NSString stringWithContentsOfFile:markdownFilePath
                                                      encoding:NSUTF8StringEncoding error:nil];
    const char *prose = [rawMarkdown UTF8String];
    struct buf *ib, *ob;
    
    // Get length of string, create a new buffer, copy the data over to the new buffer.
    int length = [rawMarkdown lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
    ib = bufnew(length);
    bufgrow(ib, length);
    memcpy(ib->data, prose, length);
    ib->size = length;
    
    ob = bufnew(64);
    
    struct sd_callbacks callbacks;
    struct html_renderopt options;
    struct sd_markdown *markdown;
    
    // Render the markdown.
    sdhtml_renderer(&callbacks, &options, 0);
    markdown = sd_markdown_new(0, 16, &callbacks, &options);
    sd_markdown_render(ob, ib->data, ib->size, markdown);
    sd_markdown_free(markdown);
    
    // Convert it back into an NSString, attach the stylesheet to it, load it into our UIWebView.
    NSString *shinyNewHTML = [NSString stringWithUTF8String:(const char *)ob->data];
    NSString *style = [NSString stringWithContentsOfFile:cssFilePath encoding:NSUTF8StringEncoding error:nil];
    [instance loadHTMLString:[NSString stringWithFormat:@"<style>%@</style>%@", style, shinyNewHTML]
                     baseURL:[[NSURL alloc] initWithString:@""]];
    
    // Release our C buffers.
    bufrelease(ib);
    bufrelease(ob);
    
    // Set the background color to clear, return this instance.
    instance.backgroundColor = [UIColor clearColor];
    instance.opaque = NO;
    return instance;
}

@end

So now that I've got this awesomely-modified version of UIWebView, I can mix it in with a UIViewController, too.

//
// BKMarkdownViewController.h
//

#import <UIKit/UIKit.h>

@interface BKMarkdownViewController : UIViewController <UIWebViewDelegate>

@property (strong, nonatomic) UIWebView *webView;
@property (strong, nonatomic) NSString *markdownPath;
@property (strong, nonatomic) NSString *cssPath;

+ (id)controllerWithPathToMarkdownFile:(NSString *)markdownPath pathToCSSFile:(NSString *)cssPath;
- (id)initWithPathToMarkdownFile:(NSString *)filepath pathToCSSFile:(NSString *)cssPath;

@end

//
// BKMarkdownViewController.m
//

#import "BKMarkdownViewController.h"
#import "UIWebView+Markdown.h"

@implementation BKMarkdownViewController

+ (id)controllerWithPathToMarkdownFile:(NSString *)markdownPath pathToCSSFile:(NSString *)cssPath {
    return [[self alloc] initWithPathToMarkdownFile:markdownPath pathToCSSFile:cssPath];
}

- (id)initWithPathToMarkdownFile:(NSString *)markdownPath pathToCSSFile:(NSString *)cssPath {
    if ((self = [super init])) {
        _markdownPath = markdownPath;
        _cssPath = cssPath;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.webView = [UIWebView webViewWithContentsOfMarkdownFile:self.markdownPath withCSSPath:self.cssPath];
    self.webView.backgroundColor = [UIColor whiteColor];
    self.webView.delegate = self;
}

- (void)viewWillAppear:(BOOL)animated {
    self.webView.frame = self.view.bounds;
    [self.view addSubview:self.webView];
}

- (void)viewDidUnload {
    [super viewDidUnload];
    self.webView.delegate = nil;
    self.webView = nil;
    self.markdownPath = nil;
    self.cssPath = nil;
}

@end

And just like that, I can drop some code into a different view controller that pushes one of these babies right on the nav stack.

- (void)someButtonWasJustHitOrSomething {
    NSString *md = [[NSBundle mainBundle] pathForResource:@"this-is-awesome" ofType:@"md"];
    NSString *css = [[NSBundle mainBundle] pathForResource:@"some-styles-and-stuff" ofType:@"css"];
    BKMarkdownViewController *mdVC = [BKMarkdownViewController controllerWithPathToMarkdownFile:md pathToCSSFile:css];
    mdVC.title = @"This is Awesome.";
    [self.navigationController pushViewController:mdVC animated:YES];
}

Seriously, you guys, Markdown. It makes the world go 'round.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment