part of the groupelephant.com family
beyond corporate purpose

All posts by Jelle

Appearance in iOS 5

27 Mar 2012

Since iOS 5 as a developer you can finally change the tint/design of some elements without much trouble. Changing the background of your UINavigationBar just became so much easier.

@selector(appearance)

The classes that support the UIAppearance protocol have access to an appearance selector. This returns the appearance proxy for the receiver.

It’s on this proxy that you can call selectors like setTintColor:, setBackgroundImage:forBarMetrics:, etc…

Here is a list of classes that are supported:

  • UIActivityIndicatorView
  • UIBarButtonItem
  • UIBarItem
  • UINavigationBar
  • UIPopoverController
  • UIProgressView
  • UISearchBar
  • UISegmentedControl
  • UISlider
  • UISwitch
  • UITabBar
  • UITabBarItem
  • UIToolbar
  • UIView
  • UIViewController

Change the navigation bar background

Here is a quick example how you can easily set the background image of a navigation bar for the whole application.

Add the following line of code to your application:didFinishLaunchingWithOptions: in your AppDelegate:

[[UINavigationBar appearance] 
		setBackgroundImage:[UIImage imageNamed:@"navbar.png"]
		forBarMetrics:UIBarMetricsDefault];

This will set the navbar.png image as a background on your navigation bar for all the navigation bars in your application.

Appearance depending on containment

But what if you want your popover to have the default blue UINavigationBar, but you just changed the appearance for the application. Than you can add this line of code:

[[UINavigationBar appearanceWhenContainedIn:
		[UIPopoverController class], nil] 
				setBackgroundImage:nil 
				forBarMetrics:UIBarMetricsDefault];

When the UINavigationBar is used inside a UIPopoverController, then the background image for the navigation bar will be nil, and therefore the default background will be shown.

You can use UIPopoverController, your custom UIViewController classes, etc… as the containment classes.

What selectors are available on a class

How to find out which selectors can be accessed by the appearance proxy? Just go to XCode and go to the header file of a class, for example UINavigationBar.

In this header file you find the methods supported by the proxy by searching for the UI_APPEARANCE_SELECTOR constant. This is located behind the method definition.

You notice that the UINavigationBar appearance proxy supports the following selectors:

  • tintColor
  • setTintColor:
  • setBackgroundImage:forBarMetrics:
  • backgroundImageForBarMetrics:
  • titleTextAttributes
  • setTitleTextAttributes:
  • setTitleVerticalPositionAdjustment:forBarMetrics:
  • titleVerticalPositionAdjustmentForBarMetrics:

Reference

Creating a Quick Look plugin

27 Jan 2012

I’ve been searching for some decent documentation on how to create a Quick Look plugin. It took me several months before I finally found a way to develop the plugin. With many thanks to a soon to be released book by The Big Nerd Ranch.

For those wondering on what Quick Look does, here is Apple’s explanation.

In this blog post I’ll show you how to…

  • create a simple Quick Look plugin

  • support a custom file type

  • debug your plugin with Xcode

Setup your Xcode project

Now let’s start developing!

  • Create a new Quick Look project Quick Look Project

  • Change the extension for both GenerateThumbnailForURL.c & GeneratePreviewForURL.c to .m. (This allows you to use Objective-C code and frameworks in these files)

Support an extension

In this example I want to support the .10to1 extension so that I can preview this file as a text file.

Below is an example on what you have to add to the Info.plist to support the .10to1 extension.

<key>UTImportedTypeDeclarations</key>
<array>
  <dict>
    <key>UTTypeIdentifier</key>
    <string>be.10to1.quicklook</string>
    <key>UTTypeDescription</key>
    <string>10to1 document</string>
    <key>UTTypeConformsTo</key>
    <array>
      <string>public.data</string>
    </array>
    <key>UTTypeTagSpecification</key>
    <dict>
      <key>public.filename-extension</key>
      <array>
        <string>10to1</string>
      </array>
    </dict>
  </dict>
</array>	
<key>CFBundleDocumentTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>QLGenerator</string>
    <key>LSItemContentTypes</key>
    <array>
      <string>be.10to1.quicklook</string>
    </array>
  </dict>
</array>

Be aware, the default project template for a Quick Look plugin already has the following line of code included in the plist.

<key>CFBundleDocumentTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>QLGenerator</string>
    <key>LSItemContentTypes</key>
    <array>
      <string>SUPPORTED_UTI_TYPE</string>
    </array>
  </dict>
</array>

If you want to create a plugin for an existing UTI type, you just have to change SUPPORTED_UTI_TYPE string with for example public.png for a PNG image.

But if your plugin supports a non existing UTI type you have to define it in the plist under the UTImportedTypeDeclarations key. If you want to find out to what UTI type your file conforms to, just use the mdsl executable and you’ll find the UTI type(s) in key kMDItemContentTypeTree.

mdls FILENAME

More information can be found here.

Render the preview

This is the view shown after the space button is pressed. This will display the content of the .10to1 file.

  • Add the Cococa framework

  • Import <Cocoa/Cocoa.h> in the GeneratePreviewForURL.m file

  • Add some generation code inside the GeneratePreviewForURL function as done in this file

Render the thumbnail

This is the icon with a generated content that is shown in your Finder window.

  • Add the WebKit framework

  • Import <Cocoa/Cocoa.h> & <WebKit/WebKit> in the GenerateThumbnailForURL.m file

  • Add some generation code inside the GenerateThumbnailForURL function as done in this file

Debugging

But how do we debug a Quick Look plugin? You can’t just run it as an executable. Therefore we use qlmanage provided by Apple.

We first have to copy the plugin to ~/Library/QuickLook after a successfull build, otherwise your plugin will not run correctly with Xcode.

  • Add a New Copy Files build phase to your target

  • Select Absolute Path and enter ~/Library/QuickLook as the Subpath

  • Add the generated file to the files list

    Copy Files

Run the plugin using qlmanage.

  • Because you can’t select qlmanage in the Finder you have to copy the binary to your project root
cp /usr/bin/qlmanage PROJECT_ROOT/.
  • Edit your scheme and select the Run/Info tab

  • Select the qlmanage executable in your project root

    qlmanage

  • In the Run/Arguments tab you can add the folling Arguments Passed On Launch to render the preview:

-p dummy.10to1

Next up, run the application and the dummy.10to1 file will be used as the selected file.

To test the thumbnail generation you could to the same, add the same argument but with -t in the Arguments tab:

-t dummy.10to1

But this didn’t to the trick (I have no idea why…), so you can manually run this command in your Terminal in order to test the preview generation:

qlmanage -t dummy.10to1

The result

After all this we have created our Quick Look plugin.

If you want do distribute the plugin, just Archive & Save it.

Open a .10to1 file and enjoy the plugin! :)

The plugin

Some utilities

  • Reload the Quick Look generator list
qlmanage -r
  • List the UTI types for the given file, very handy when defining your supported UTI format
mdls FILE

Reference

Core Data Versioning

28 Nov 2011

Every time I have to do a small (read: very simple) migration in an iOS-project I have to dive into Google to find out how it works. I practically never get to it anymore, but I finally decided to write a post about it on this blog.

Migrating what and when?

I only use migrations and version models when an application is already used in production. Core Data will not automatically update its database when you change something in it. The app will just crash… And I don’t think that’s what the user wants. The app won’t always crash: sometimes the changes just aren’t executed and there will occur a problem when you try to call a method related to the changes.

A new version

Go to Editor > Model Version in order to create a new version of your Core Data Model. Choose a name and select the Model you want to use as the starting point, this will be the database you currently use in the production app.

A new version

Now that we’ve created a new version we can just apply some changes to it. It’s just a copy of our old xcdatamodel file.

The last thing we have to do is set this version of the Core Data Model as our default version. In your project navigator select the .xcdatamodeld container in which the 2 versions reside. Open the Utilities pane on the left and select the File Inspector. Here you can change your Current version of your model.

Set the default version

Simple mapping

The mapping model is the place where you tell Core Data how to handle model changes between 2 versions. You don’t always need a mapping model, when you add or remove a column/model for example. But when you want to do more complex migrations like renaming a column or filling the added columns with new values, then this is the way to go.

Add a new “Mapping Model” file to your project, and follow the steps to complete the creation.

Create a mapping model

In this model you can see all the Entity Mappings with the changes between the 2 versions.

Here is an example on how you rename a field (name to fullName) and connect the data between the 2 Core Data Models:

The mapping

After the migration the field will be renamed, and the data will be preserved.

Custom mapping

When we want to do a bit more advanced mapping, we’ll have to subclass NSEntityMigrationPolicy in order to get the result we want.

In the next example we will autofill the newly added field (companyName) with a default value ‘10to1’.

Create a new file called CustomPhotoMigration that subclasses from NSEntityMigrationPolicy and add the following code to the implementation class. You can choose the name of the file, doesn’t really matter.

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)instance
                                      entityMapping:(NSEntityMapping *)mapping
                                            manager:(NSMigrationManager *)manager
                                              error:(NSError **)error {
    NSArray *_properties = [mapping attributeMappings];
    for (NSPropertyMapping *_property in _properties) {
        if ([[_property name] isEqualToString:@"companyName"]) {
            NSExpression *_expression = [NSExpression expressionForConstantValue:@"10to1"];
            [_property setValueExpression:_expression];
        }
    }
    
    return [super createDestinationInstancesForSourceInstance:instance 
                                                entityMapping:mapping 
                                                      manager:manager 
                                                        error:error];
}

Once the file is created you’ll have to tell the mapping model to use our custom policy. Go to your .xcmappingmodel file and click on the entity mapping you would like to customize. In the Mapping Model Inspector pane on the right, just fill in the class name of your custom policy. In our case this will be CustomPhotoMigration.

The custom mapping policy

This should auto fill the companyName field on every existing record with “10to1” the next time you run the app.

For the record: you don’t have to run a script or something like that to perform the migration. Just run your application and it will be executed for you.

Localizing Javascript in your rails app

22 Mar 2011

On a recent project I had to translate some text inside my Javascript code. But I didn’t want to use duplicate localization files so I looked for a framework so I could reuse the translations from my rails app. So no duplicate translation files, just the locales/*.yml files used by rails.

###Installation

Add the i18n-js gem to your gemfile.

gem "i18n-js"

Now add the following before_filter to your ApplicationController. This generates the Javascript translation file every time you render a page in development.

class ApplicationController < ActionController::Base
  before_filter :export_i18n_messages

  def export_i18n_messages
    SimplesIdeias::I18n.export! if Rails.env.development?
  end
end

###Use it!

Before you can really start using the localizations, you have to set the current locale. Do this in your application.html.erb file. (But it doesn’t really matter where you set it, just do it before the call to translate)

<%= javascript_include_tag "i18n" %>
<%= javascript_include_tag "translations" %>
<%= javascript_tag do %>
  I18n.defaultLocale = "<%= I18n.default_locale %>";
  I18n.locale = "<%= I18n.locale %>";
<% end %>

And now just translate the same way you do in your rails app.

I18n.t("user.name") // returns 'Name'

Here is an example of my en.yml file.

en:
  user:
    name: "Name"
    children: 
      zero: "no children"
      one: "%{count} child"
      other: "%{count} children"

Other shizzle

Using number in your locales with pluralization.

I18n.t("user.children", { count: 0 }); // 'no children'
I18n.t("user.children", { count: 1 }); // '1 child'
I18n.t("user.children", { count: 3 }); // '3 children'

Use a default value when no translation is present.

I18n.t("user.age", { defaultValue: "Age" }); // 'Age'

Use localizations for number formatting.

I18n.l("number", 1990.99); // '1,990.99'

For more info check Github.

10to1 Lighthousekeeper Theme

26 Feb 2011

I recently started using Lighthousekeeper to manage my Lighthouseapp projects. I found the app much easier to use than the current web interface.

But because I didn’t like the main layout and they allow you to create custom themes, I created one myself (with a little help from @maxvoltar).

10to1 Lighthousekeeper Theme

You can find the installation instructions on Github.

Creating your personal theme

The source files for the 10to1 theme can be found here. It’s very easy to create when you know HTML and CSS.

Now let’s get you on the way.

Create a folder (with the .lhktheme extension) in your ~/Library/Application\ Support/Lighthouse_Keeper/Themes directory.

Inside this folder you should to create a typical index.html file. You can also place some CSS files and images next to it.

Lighthousekeeper Theme

And now just design the template as you would with a website. There are also some Lighthousekeeper variables available, just checkout the Lighthousekeeper Manual.

Start!

Switching to Vim

07 Jan 2011

As a fulltime Textmate user, I was really happy with my editor. Even with 2 colleagues that try to convince me that Emacs is the way to go, But I still didn’t feel the need to switch. But then one day something popped… The day after I switched to Vim, and until now I’m really glad I did.

So why would I do this?

Well, I can’t lie to you. I wanted something more special than Textmate, everybody uses Textmate these days and I have the impression that more developers in the Rails community are switching to an editor like Emacs or Vim. And because Piet is a die hard Emacs user I wanted to compete with him by using Vim.

Github

I have my whole Vim configuration on my Github account, just in case my machine desides to explode. You can find it here. This can come in very handy when working on multiple machines, just commit, push and pull and the configuration is identical on both machines! Isn’t that just freakin’ awesome?

These are some of the features/plugins I added to my configuration:

  • Ack (find in project) support
  • Emacs-style file finder called Lusty Explorer
  • A nice Railscast colorscheme
  • Somewhat better full screen support
  • A simple strip trailing spaces function

Here is an example screenshot of the colorscheme I use in MacVim.

The Railscast colorscheme

I’m adding new stuff every day while learning more about Vim.

Installing my config

First things first, backup your current configuration so you don’t have to blame me for ruining your life.

Next up:

  • Clone this repo to .vim/
  • Create a simlink for the .vimrc: ln -nfs ~/.vim/.vimrc ~/.vimrc
  • Create a symlink for the .gvimrc: ln -nfs ~/.vim/.gvimrc ~/.gvimrc

And that’s all you have to do.

Enjoy!

Reference

Custom delegates, the Apple way

03 Jan 2011

When you get into Mac or iOS development, you’ll soon be confronted with delegates. And after a certain period of time when you gain more experience, you’ll start creating your own custom delegate calls.

How Apple uses delegates

Let’s say we want to log the number of characters entered in a UITextView. We’ll have to assign a delegate to the UITextView instance and then implement the textViewDidChange method.

UITextView *textView = [[UITextView alloc] init];
textView.frame = CGRectMake(0, 0, 100, 30);
textView.delegate = self;
[self.view addSubview:textView];

This code snippet creates the UITextView somewhere in the viewDidLoad method from out main UIViewController. Next up: implement the textViewDidChange method.

- (void)textViewDidChange:(UITextView *)textView {
  NSLog(@"number of chars: %i", [textView.text length]);
}

This will log the number of characters entered every time you change something in the UITextView.

But there is a warning

The code above will work perfectly but there will be a warning.

Delegate warning

The warning thrown is this one: class ‘ExampleViewController’ does not implement the ‘UITextViewDelegate’ protocol. It means we have to follow the UITextViewDelegate protocol and set it in the header file like this:

The solution to remove the warning is fairly easy, just add the UITextViewDelegate protocol to the header file.

@interface ExampleViewController :
                   UIViewController <UITextViewDelegate>

To make a class conform to a protocol, you just have to add it in between the < and >.

Create your custom delegate call

Now what if you want a custom view class to call a method in your controller because you think the code functionality belongs to the controller. You’ll first create a property where you can assign the delegate:

@property (nonatomic, retain) NSObject *customDelegate;

Your delegate instance is now available in your class, so when you call the setColor: method on your instance, than you want to call the delegate’s changeColor: method if available in the customDelegate: instance.

You can find more info on the difference between id and NSObject on this blog. There reason I’m using NSObject is because it doesn’t generate warnings when using the respondsToSelector:.

In the code below we’ll first check if the method is available, and if so we call it and pass the the current color so the controller can do the rest.

- (void)setColor:(UIColor *)color {
  SEL method = @selector(changeColor:);
  if ([customDelegate respondsToSelector:method]) {
    [customDelegate performSelector:method
                    withObject:color];
  }
}

For those that don’t really know what selectors are, they can read it here

But I want a warning!

It can come in handy to throw a warning to notify the developer that he can or has to implement certain methods in order for the delegate to work properly.

You should first create a protocol that defines the @required and @optional methods that the delegate class can or has to implement. And you can do this in the header file (or in another file).

@protocol ExampleViewDelegate
@required 
- (void)changeColor:(UIColor *)color;
@optional
- (void)clearColor;
@end

The methods below the @required directive are all obligatory. The ones below @optional are not. Now this won’t throw any warning it’s just a preparation for what is coming.

Now you have to do a small modification to the @property where the customDelegate is defined. We have to tell the customDelegate that it should implement the ExampleViewDelegate protocol.

@property (nonatomic, retain)
                  id<ExampleViewDelegate> customDelegate;

When we now assign the delegate, a warning is thrown when the protocol is not implemented (cfr. How Apple uses delegates). So we just have to implement the protocol to stop the warning from appearing.

@interface ExampleViewController
                 : UIViewController <ExampleViewDelegate>

And when we don’t implement the changeColor: method, then another warning will arise, because this method is set to required in the protocol.

Now let’s go and delegate!

Reference

You can find a small project with the UITextView and the custom delegate examples on Github

Stop worrying about Core Data

10 Dec 2010

Core Data is one of the frameworks I find a bit scary to use when I’m developing iOS applications. I always felt like I was developing something that wasn’t really under my control. That was until Piet pointed out this framework to me: Mogenerator.

Draw your models

The first thing you should do is create your models in the XCode Modelling Tool. More information on this topic can be found here.

Here is an example of my Core Data model:

Core Data model

Let’s generate

When you created a model with some attributes, you can use mogenerator to generate your classes (or regenerate them, when you did some editing).

I pass two arguments to mogenerator to generate the files.

  • -m the location of your data model
  • -O the location of your model classes

Here is an example:

mogenerator -m TestProject.xcdatamodel -O Classes

This command generates all the classes for your models created with the modelling tool. Be aware: You still have to add them to your XCode project in orde to use them.

What is generated?

For each model four class files are generated. The 2 class files starting with a dash should never be modified. When you want to add custom methods/logix, you can do this in the other 2 generated classes. They extend from the dashed-classes.

Generated Files

Every attribute can now be accessed as a property on the object. This makes it very easy to assign new values.

Don’t forget that when you change your model in the modelling tool, that you’ll have to run the mogenerator command again in orde to regenerate the dashed files.

Some code

The code snippet below will create a Person object and assigns some values to the attributes. When everything is assigned the changes are saved.

// NSManagedObjectContext from somewhere in your system
NSManagedObjectContext *moc = ...

Person *p = [Person insertInManagedObjectContext:moc];

p.firstName = @"Zot";
p.lastName = @"Franske";
p.age = [NSNumber numberWithInt:25];

[moc save:nil];

Check these out!

Here are some links that helped me during development:

Creating a PDF map application

21 Oct 2010

For a recent project I had to create a small PDF ‘Google Maps like’ viewer for iOS. The viewer needs to handle zooming and dragging to navigate the PDF page. And for performance issues it’s best to load the PDF using tiles.

In some code snippets below, I’ll show you how it’s done. Make sure to check out Github for an example XCode project with comments for almost every line of code.

Setup

This is the setup I used in this tutorial:

  • I have a UIViewController with an attached XIB file.
  • I created a map UIView that is a subview belonging to the controller’s view (which is a UIScrollView).
  • I built a CATiledLayer that is added as a sublayer to the map view.
  • That CATiledLayer is responsible for the tiling of the PDF map.

Create the PDF

Here is how you create the PDF. First grab a NSURL instance with the PDF’s location, and then you create the CGPDFDocumentRef that’s needed to construct the CGPDFPageRef which represents a page.

NSString *filePath = [[NSBundle mainBundle] 
                          pathForResource:@"filename"
                          ofType:@"pdf"];
NSURL *pdfURL = [NSURL fileURLWithPath:filePath];
CGPDFDocumentRef *myDocumentRef = 
        CGPDFDocumentCreateWithURL((CFURLRef) pdfURL);
CGPDFPageRef *myPageRef = 
        CGPDFDocumentGetPage(myDocumentRef, 1);

Tile everything

I want the PDF to load like the Google Maps application is loaded. That’s why you have to use the CATiledLayer, this does everything. You can specify the size, level of detail (for vectorized PDF’s)… For more info check out the Apple Developer Reference

Don’t forget to set the delegate for the CATiledLayer, this makes sure the drawLayer method is called.

CATiledLayer *tiledLayer = [CATiledLayer layer];
tiledLayer.delegate = self;
tiledLayer.tileSize = CGSizeMake(200, 200);
tiledLayer.levelsOfDetail = 1000;
tiledLayer.levelsOfDetailBias = 1000;
tiledLayer.frame = 
        CGRectIntegral(CGPDFPageGetBoxRect(myPageRef, 
                kCGPDFCropBox));

Afterwards the tiled layer delegate will do the rest if you implement this method:

- (void)drawLayer:(CALayer *)layer 
        inContext:(CGContextRef)context {
  CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
  CGContextFillRect(context, 
                    CGContextGetClipBoundingBox(ctx));
  CGContextTranslateCTM(context, 
                        0.0, layer.bounds.size.height);
  CGContextScaleCTM(context, 1.0, -1.0);
  CGContextConcatCTM(context, 
        CGPDFPageGetDrawingTransform(myPageRef, 
                        kCGPDFCropBox, layer.bounds, 
                        0, true));
  CGContextDrawPDFPage(context, myPageRef);
}

It looks a bit low level but it’s fairly easy to get it to work.

Zooming

Now the last thing you have to do is to tell the UIViewController which UIView has to be zoomed. That is why you need to set the delegate of the UIScrollView. This makes sure the following methods is called:

- (UIView *)viewForZoomingInScrollView:
                (UIScrollView *)scrollView {
	return mapView;
}

Here you return the UIView that needs to zoom depending on the finger gestures.

References

  • You can find an example iPad project on Github.
  • Here’s an example project made by Apple.

I love Categories

24 Aug 2010

One thing I really love about Objective-C is Categories.

What are they?

Categories make it possible to add a method to a class without subclassing.

For example:

Let’s assume that you want to create a method isEmpty. And you want to find a clean way to ask the NSString instance for it.

Then we’ll create a category on the NSString class in which we define a method isEmpty that returns a BOOL value.

How to create them?

Create a header (NSString+UtilityMethods.h) and an implementation file (NSString+UtilityMethods.m).

You define the category and the method in the header file like this:

@interface NSString (UtilityMethods)
- (BOOL)isEmpty;
@end

And in the implementation file like this:

@implementation NSString (UtilityMethods)
- (BOOL)isEmpty {
  return [self count] == 0;
}
@end

Now you can use the isEmpty method on every instance of NSString. (Don’t forget to include the NSString+UtilityMethods.h file)

NSString *text  = @"";
if ([text isEmpty]) {
  text = @"Categories are awesome";
}
NSLog(text);

The above snippet will print out “Categories are awesome”.

Another example with UIColor

This is how I clean up my code with categories. I create a category on UIColor to add methods that return every different UIColor used in my application. This way I only have to modify the colors here in orde to change them in the app.

Header file:

@interface UIColor (ApplicationColors)
+ (UIColor *)backgroundColor;
+ (UIColor *)textColor;
@end

Implementation file:

@implementation UIColor (ApplicationColors)
+ (UIColor *)backgroundColor {
  return [UIColor redColor];
}
+ (UIColor *)textColor {
  return [UIColor whiteColor];
}
@end

Nice isn’t it?

PDFKit and Ruby on Rails

18 Aug 2010

I finally found some time to write my first blog post for my awesome company! Here we go…

For a recent project I decided to use PDFKit to generate PDF files in Ruby on Rails. I’m used to generating these with Prawn, but as we all know it can be very hard to get it styled correctly.

Installation

First things first, we have to install the gem and then the wkhtmltopdf binary.

Installing the gem is easy:

gem install pdfkit

Hey, installing the binary is as easy:

sudo pdfkit --install-wkhtmltopdf

Now we can use PDFKit on our system.

Manual generation

To generate a pdf from a HTML web page just create the kit instance:

kit = PDFKit.new "<p>This is an awesome pdf.</p>"

And then convert it to a pdf file:

pdf = kit.to_file file_path

You can also pass an URL of a file to the PDFKit initializer method:

kit = PDFKit.new "http://www.10to1.be"
kit = PDFKit.new File.new(file_path)

Some Rails magic

But here is some real magic. You can just attach the pdf format to your controller actions. When you go to http://test.be/people/jelle.pdf, the PDF will be downloaded (or your browser will open it in a window).

Just include the following lines of code in your environment.rb (for Rails 2.x) or application.rb (for Rails 3.x):

require "pdfkit"
config.middleware.use PDFKit::Middleware

What happens now is that your show.html.erb file is rendered as usual but it is converted to the PDF format afterwards. So the PDf file gets the same style as the HTML page (with CSS styling included).

I used this for a project, and it works great!

Changing the filename

Now another handy way to change the filename for the PDF file. When you go to http://test.be/people/1.pdf, the downloaded filename will be 1.pdf. But what if I want the filename to be jelle-vandebeeck.pdf? Well, just include this line of code somewhere in your action:

def show
  @person = Person.find params[:id]
	
  # Change the filename of the downloaded pdf file
  headers["Content-Disposition"] = 
    "attachment; filename=\"#{@person.name}\""
end

For more information visit the Github page.