Sam Rayner

Hi, I’m Sam. I design and build apps for iOS and the web. I live in Sheffield, UK and play Ultimate far too much. You can reach me via email or on Twitter.

Discographer

As a fun little project over the last few weeks, I’ve built a Web app that helps you discover more music by artists in your iTunes library. It’s called Discographer, and uses the Spotify API to fetch albums, ticking off the ones you own so you can see what you might have missed.

iTunes Music Library.xml

It was a nice opportunity to play with some new web APIs (Drag and Drop, File and Flexbox), the main benefit being your library is never uploaded but parsed locally in your browser. The only thing transmitted is the name of any artist you click.

It was linked on Lifehacker a couple of days ago and I’ve since received some great feedback. Please give it a go and let me know what you think!


GitHub Spectacles

At Terracoding we regularly review pull requests from each other when working on a project. GitHub’s “Commits” and “Files Changed” tabs make it pretty painless but we wanted a way to easily view Ruby implementation files alongside their specs.

Octoclark Kentocat

I created a bookmarklet to arrange the file pairs 2-up for review. With the files side-by-side it’s easy to spot code that isn’t covered by tests and we get a better idea of what methods are meant to do. It also hides the code of deleted files which I never bother to read during review.

To install, drag this button into your bookmarks bar:

GitHub Spectacles

Try it out on this commit. Just click the bookmark when viewing the file changes.

The CoffeeScript for the bookmarklet is opensourced on GitHub.


Retiring Lando

Yesterday I finished porting this site to Middleman, a static blog generator written in Ruby. Since releasing Lando, I’ve barely written a line of PHP and, short of rewriting it in Ruby (which I unfortunately don’t have time to do), I’ve been put off working on Lando because of that.

I am very proud of how Lando turned out and have genuinely enjoyed using it to power my own site but it felt like it was time to switch to something written in the language I use day-to-day. If you use Lando, I am sorry I won’t be continuing its development. Rest assured it will stay open-sourced on GitHub and if anyone wants to take over managing it please get in touch.

If you’re looking for a replacement, translating my Lando templates to Middleman was remarkably simple, with the majority of helper functions and content properties matching across the two systems. I recommend it over Jekyll/Octopress which I found to be less flexible and hampered by their Liquid templates.

Lando and Han

Thanks to everyone who tried Lando out and gave feedback. I learned a lot building it.


Dynamic Regular Expressions

I faced an interesting challenge while developing Sprint.ly for Alfred recently; how to provide a preview of a story title as the user typed it. A story is a concept from Agile software development that encapsulates the who, what and why of a feature.

As a site owner, I want a contact form so that visitors can get in touch.

First off, I created a Story class to hold the components of a story and a title method to get them out as a formatted string:

class Story
  attr_accessor :who, :what, :why

  def title
    #declare our separators
    prefixes = {
      who: "As a",
      what: "I want",
      why: "so that"
    }

    #go all grammar nazi
    prefixes[:who] << "n" if @who && ["a","e","i","o"].include?(@who[0,1])

    #format the story into a string
    "#{prefixes[:who]} #{@who}, #{prefixes[:what]} #{@what} #{prefixes[:why]} #{@why}"
  end
end

The tricky bit would be taking a string as input and parsing it into those attributes. I needed the title broken up to submit the parts separately to the Sprint.ly API as well as generate the preview on the fly.

I wanted the preview to start out as “As a __, I want __ so that __“, with the blanks being filled in as the user typed. I also wanted to give the option of a aa iw st shorthand to speed up entry, and to allow the blanks to be filled in any order.

Unable to write a standard regex that would cut it, I ended up with this system of appending to a regex as matches were found:

def parse_title(title)
  #default values
  @who = "__"
  @what = "__"
  @why = "__"

  #define our capture triggers
  captures = {
    "who" => "as an?|aa",
    "what" => " i want| iw",
    "why" => " so that| st"
  }

  #get things started
  regex_str = '^\s*'

  #if the prefix is present, append a named capture group to the regex
  captures.each do |key,val|
    prefix = '(?:'+val+')\s+'
    if title.match(Regexp.new(prefix, true))
      regex_str << prefix+'(?<'+key+'>.+)'
    end
  end

  #apply the final regex built from the string
  matches = title.match(Regexp.new(regex_str, true))

  if matches
    matches.names.each do |name| 
      value = matches[name].strip
      #strip out any trailing comma from the who
      value.sub!(/,$/, "") if name == "who"
      #store each component in the instance variables
      self.send(name+"=", value)
    end
  end
end

The solution greedily matches everything after a trigger phrase until another trigger is encountered and then breaks the string down.

With that done, the last thing was to trigger parsing by calling a setter method on every keystroke:

def title=(value)
  self.parse_title(value)
end

And it works pretty well!

Download Sprint.ly for Alfred to give it a spin. If you know a smarter way of doing this, I’d be really interested to hear it on Twitter or via email.


Sprintly Workflow for Alfred 2

Since the first Alfred 2 beta was released, I’ve been building a workflow for Sprintly, the agile task management web app we use at Terracoding. The basic features are done so here it is for any other Mega Supporters who use Sprintly.

Download Sprintly for Alfred

Using the sly command, you can switch products, list, add and manage items. Full documentation is on Github.

NOTE: The workflow requires a system Ruby ≥ 1.9. If you haven’t upgraded to Mavericks yet version 1.0.1 will work with RVM.

To install, run sly setup {email} {api_key}.

Screenshots

Feel free to make a pull request on Github or let me know what features you would like to see on Twitter: @samrayner.

Hopefully in the future there’ll be an update mechanism for workflows, but until then keep an eye on the repo for changes. I’ll keep the download link above up-to-date.


Using Ruby 1.9 with Alfred

I’ve been building a Sprint.ly Workflow for Alfred 2 (currently in beta) and wanted to use it as an opportunity to brush up on my TDD and Ruby-foo.

Alfred 2 let’s you run PHP, Python, Perl and Ruby scripts but they run from usr/bin/lib and Mountain Lion ships with (a pretty outdated) Ruby 1.8.7.

If you have a newer version of Ruby installed with RVM or rbenv, here’s how to bootstrap Alfred to use it instead. In your Workflow, set up a Ruby script action as normal and fill it with:

SCRIPT_FILE = "example"

def ruby_exec_path(manager)
  begin
    ruby_path = `~/.#{manager}/bin/#{manager} which ruby`
    if $?.exitstatus == 127
      raise Errno::ENOENT
    end
  rescue Errno::ENOENT
    ruby_path = ""
  end
  return ruby_path.strip
end

ruby_path = ruby_exec_path("rvm")
ruby_path = ruby_exec_path("rbenv") unless ruby_path.length
ruby_path = "ruby" unless ruby_path.length

parent_dir = File.expand_path(File.dirname(__FILE__))
puts `#{ruby_path} "#{SCRIPT_FILE}.rb" "{query}"`

This fetches your active Ruby and runs the script example.rb, passing the Alfred query as an argument.

Create example.rb in your Workflow directory and you can grab the query string inside it:

QUERY = ARGV[0]

Anything your file puts into STDOUT will be passed back to Alfred to use in your Workflow!

As well as letting you use The New Ruby Hotness, this has the added benefit of moving code out of Alfred and into files that are more easily tested and version controlled. The only down-side is that your Workflow will require users to be running RVM or rbenv if you share it.


Update (Aug 4): Good news! OS X Mavericks is set to ship with Ruby 2.0 by default so you shouldn’t need to use this hack for much longer.


Dynamic Height UITableView Cells in Xcode

I recently started building my first iOS app and quickly ran into problems trying to apply dynamic heights to UITableView cells depending on their content. This seems like the kind of common task that Apple would make simple through their APIs but after hours of research I discovered that:

  1. This is an extremely common problem.
  2. There is no single, agreed-upon solution.
  3. Existing solutions (this one from Cocoa is My Girlfriend seems to be the most widely linked to) appear to involve a lot of code repetition.

Frustrated, I became determined to create my own solution. The goals:

  1. Use a custom UITableViewCell NIB designed in Interface Builder and only adjust things in code when necessary.
  2. Be able to easily apply the custom cell to table views in my app Storyboard.
  3. Avoid code repetition wherever possible. We’re going to need to calculate heights once for the row then again for the cell so let’s streamline that.

Here’s what I came up with.

Prepare a Prototype Cell

First, create a new Empty User Interface. Drag a UITableViewCell object in, fill it with subviews and arrange them to your liking. You’ll probably want to set the cell Style to Custom in the Attributes inspector. This is your prototype cell; styles and positioning will be inherited from it.

I took the approach of including everything that could be shown in the cell in my NIB so I can hide rather than add elements in code later. Make sure your constraints are set up nicely to allow for the height of your cell changing1.

Next, create an Objective-C class that extends UITableViewCell and add properties to its header file for all of the cell elements you’ll want access to in your UITableViewController later:

@interface NaSQuotationTableViewCell : UITableViewCell
  @property (weak) IBOutlet UILabel *quotationLabel, *attributionLabel;
  @property (weak) IBOutlet UIButton *avatarButton, *imageButton;
@end

Go back to your NIB, select your cell, and hook up the IBOutlets in the Connections inspector by ctrl-dragging onto the appropriate subviews.

Finally, set the following on both the cell in your NIB and the prototype cell in each UITableView of your Storyboard.

  • Change the Custom Class in the Identity inspector to your newly created class (NaSQuotationTableViewCell).
  • Give it a unique Identifier in the Attributes inspector. Mine is QuotationCell.

Apply Dynamic Cell Heights

The first thing you’ll want to do is import the new cell class in the header of your UITableViewController:

#import <UIKit/UIKit.h>
#import "NaSQuotationTableViewCell.h"

@interface NaSQuotationsTableViewController : UITableViewController<UITableViewDelegate,UITableViewDataSource>
@end

In the implementation file of your UITableViewController, add a property for your cell prototype:

@interface NaSQuotationsTableViewController ()
  @property (strong) NaSQuotationTableViewCell *cellPrototype;
  @property NSArray *quotes, *names; //for dummy content
@end

Then load the prototype NIB when the view loads: - (void)viewDidLoad { [super viewDidLoad]; //load prototype table cell nib static NSString *CellIdentifier = @”QuotationCell”; [self.tableView registerNib:[UINib nibWithNibName:@”NaSQuotationTableViewCell” bundle:nil] forCellReuseIdentifier:CellIdentifier]; self.cellPrototype = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];

  //fill dummy content
  self.quotes = [NSArray arrayWithObjects:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla hendrerit quam eu nisl pellentesque aliquam.", @"Vestibulum ligula quam, gravida ut convallis semper, bibendum in turpis.", @"Nam quis sapien purus.", @"Donec suscipit lectus in arcu eleifend ac posuere lacus egestas. Nunc gravida quam in urna ultricies at pharetra magna sodales. Nulla placerat mi tincidunt nulla posuere id interdum mi pretium. Aliquam nulla tortor, egestas pharetra sollicitudin a, sagittis ut enim. Vivamus venenatis consectetur commodo.", @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla hendrerit quam eu nisl pellentesque aliquam. Vestibulum ligula quam, gravida ut convallis semper, bibendum in turpis. Nam quis sapien purus. Nunc tincidunt eleifend porta. Donec suscipit lectus in arcu eleifend ac posuere lacus egestas. Nunc gravida quam in urna ultricies at pharetra magna sodales.", nil];

  self.names = [NSArray arrayWithObjects:@"Sam Rayner", @"Sam Millner", @"Rob White", @"Dom Wroblewski", @"Jack Smith", nil];
}

Now we have access to the prototype cell labels, we can pass their attributes into NSString’s sizeWithFont: method to calculate the required row height:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  return [self.quotes count];
}

- (NSString *)quotationTextForRow:(int)row {
  return [self.quotes objectAtIndex:row];
}

- (NSString *)attributionTextForRow:(int)row {
  return [@"– " stringByAppendingString:[self.names objectAtIndex:row]];
}

- (CGSize)sizeOfLabel:(UILabel *)label withText:(NSString *)text {
 return [text sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  //set width depending on device orientation
  self.cellPrototype.frame = CGRectMake(self.cellPrototype.frame.origin.x, self.cellPrototype.frame.origin.y, tableView.frame.size.width, self.cellPrototype.frame.size.height);
  
  CGFloat quotationLabelHeight = [self sizeOfLabel:self.cellPrototype.quotationLabel withText:[self quotationTextForRow:indexPath.row]].height;
  CGFloat attributionLabelHeight = [self sizeOfLabel:self.cellPrototype.attributionLabel withText:[self attributionTextForRow:indexPath.row]].height;
  CGFloat padding = self.cellPrototype.quotationLabel.frame.origin.y;
  
  CGFloat combinedHeight = padding + quotationLabelHeight + padding/2 + attributionLabelHeight + padding;
  CGFloat minHeight = padding + self.cellPrototype.avatarButton.frame.size.height + padding;
  
  return MAX(combinedHeight, minHeight);
}

We can reuse sizeOfLabel: in tableView:cellForRowAtIndexPath: to render the labels with the correct heights:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  //register cell identifier from custom cell NIB
  static NSString *CellIdentifier = @"QuotationCell";
  NaSQuotationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
  
  //set width depending on device orientation
  cell.frame = CGRectMake(cell.frame.origin.x, cell.frame.origin.y, tableView.frame.size.width, cell.frame.size.height);
  
  CGFloat quotationLabelHeight = [self sizeOfLabel:cell.quotationLabel withText:[self quotationTextForRow:indexPath.row]].height;
  CGFloat attributionLabelHeight = [self sizeOfLabel:cell.attributionLabel withText:[self attributionTextForRow:indexPath.row]].height;
  
  cell.quotationLabel.frame = CGRectMake(cell.quotationLabel.frame.origin.x, cell.quotationLabel.frame.origin.y, cell.quotationLabel.frame.size.width, quotationLabelHeight);
  cell.quotationLabel.text = [self quotationTextForRow:indexPath.row];
  
  cell.attributionLabel.frame = CGRectMake(cell.attributionLabel.frame.origin.x, cell.attributionLabel.frame.origin.y, cell.attributionLabel.frame.size.width, attributionLabelHeight);
  cell.attributionLabel.text = [self attributionTextForRow:indexPath.row];
  
  return cell;
}

There we have it! Dynamic table cell heights depending on the text included in them. Hopefully this will give you a decent starting point for achieving the look you want in your app. Good luck!

  1. I confused myself for a good twenty minutes by applying a height Equal constraint to a UILabel and wondering why changing the frame height in code wasn’t having any affect. Cheers to my mate Sam for pointing out I wanted Less Than or Equal.


Wanted - User Agent Spoof Safari Extension

Since uninstalling Flash on my Mac I’ve found it frustrating following links to video and audio websites that require it. YouTube, SoundCloud, TED and CollegeHumor (to name a few) all offer HTML5 players to mobile devices but force a Flash player on desktop visitors.

To get around it, I either use my Open in Chrome Applescript or this Applescript to spoof the user agent string. To use it, you’ll first need to enable the Develop menu in Safari preferences > Advanced and enable access for assistive devices in System Preferences > Accessibility.

tell application "Safari" to activate
tell application "System Events"
  click menu item "Safari iOS 5.1 — iPad" of ((process "Safari")'s (menu bar 1)'s (menu bar item "Develop")'s (menu "Develop")'s (menu item "User Agent")'s (menu "User Agent"))
end tell

The script works well, reloading the current tab complete with working player, but it feels like something that could be automated.

What I would love is a Safari extension that takes a list of domains and automatically spoofs the user agent string or at least redirects to the mobile version on visiting them. Unfortunately, being JavaScript-based, I’m not sure the former is possible with a Safari extension but the (slightly more limited) latter solution might be.

If anyone has any ideas on getting this done, please give me a shout on Twitter @samrayner! Hopefully, eventually, such a script won’t be necessary and sites like YouTube will do a better job falling back to HTML5.


Update (Sep 12): Looks like I’m not the only one who wants this.

Unfortunately, it turns out most of the offending websites rely solely on the user agent to serve HTML5 media. m.X.com doesn’t always exist and, when it does, often behaves differently when viewed with a desktop user agent. Fellow developers – please use object detection not UA detection.

It looks like a Safari extension is out of reach, but a Chrome extension already exists thanks to a recent update to the Chrome Extensions API. Hopefully Apple will follow suit.


Introducing Lando

Managing the content of websites has always felt like a bit of a chore. Things like editing pages, posting blog entries and importing media should be simple but the Web-based admin interfaces of most CMSs make it fiddley. Form inputs and textareas weren’t designed for authoring content and, while WYSIWYG editors help, I dread looking at the HTML produced.

Up until a few months ago, I worked around the problem by editing content locally and pasting changes across but live and local versions quickly got out of sync. It was time to knuckle down and come up with something better.

A New Hope

Meet Lando: a new kind of CMS that lets you manage your website in the cloud. Lando lets you save plain-text files and media in special Dropbox folders and have them appear on your site. No forms, no fuss. You can create things like galleries and slideshows automatically from a folder of images and, with everything in the cloud, you can update your website from anywhere.

Lando Calrissian

Lando is written in PHP and comes in a package to upload to your own server. There’s a simple installation wizard to guide you through connecting to your Dropbox account. Lando then communicates with the Dropbox API every so often to check for changes to your website content files or you can force a refresh by logging in as an admin on your website and clicking update.

Lando installs 5 folders in your Dropbox: Pages, Posts, Drafts, Collections and Snippets. Pages, blog posts and drafts are created as folders containing a main text file and any supporting media files. Content can be formatted as Markdown, Textile or HTML. Collections are folders of files that can be included anywhere in your site as a list of links, a gallery or a slideshow. Snippets are text files that can be included anywhere too, useful for content that appears on multiple pages.

Having your content as files on your hard drive is great:

  • You can rename, delete and move stuff easily.
  • You can make local backups of your website.
  • You can enjoy the comfort and efficiency of editing in a familiar desktop or mobile environment rather than filling in forms in a browser.

Having content automatically sync to Dropbox is awesome too:

  • You can update your website from any connected device, even on the go with Dropbox-powered Android and iOS text editors.
  • You can edit offline and your changes will be applied next time you connect.
  • You can use Dropbox’s Share feature to easily collaborate with people on a single blog post or your entire website!
  • Your files are automatically versioned online so you can roll-back your content if something gets lost or mistakenly changed.

I’ve developed Lando as my third-year dissertation project over the past 18 months. Since then, I really have enjoyed using it to manage sites. This new version of samrayner.com is powered by Lando and it’s been a blast getting up and running so quickly with a system I know will make maintenaining it a breeze.

Lando is available free and open-source on GitHub. Documentation can be found at landocms.com/docs. Please give it a go and let me know what you think!

Post Archive

2016

2015

July

2014

February