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.

Alfred Lyrics Search Workflow

Ever had the lyrics of a song stuck in your head but can’t for the life of you remember the artist or title? I get it all the time so wrote this Alfred workflow to search my iTunes library and start playing the first match it finds.

Download Lyrics Search

Get Lyrical

To get it working with your music collection you’ll need to make sure all of your tracks have lyrics downloaded for them. Don’t panic though, a great little Mac app called Get Lyrical can automate the process, tagging songs you select or tagging in the background as you play them.

Once your library is tagged with lyrics, install the workflow and type sing followed by the lyrics.

Under The Hood

For those interested in the technical side, the Workflow is just an Applescript:

on normalize(theString)
  --trim everything but letters and numbers
  return do shell script "echo " & quoted form of theString & " | tr '\r' ' ' | sed 's/[^[:space:][:alnum:]]//g'"
end normalize

tell application "iTunes"
  set theQuery to my normalize("{query}")
  --playlist 1 should be your whole music library
  set theTracks to tracks of library playlist 1
  set match to ""
  
  repeat with i from 1 to number of items in theTracks
    set theTrack to item i of theTracks
    set theLyrics to my normalize(lyrics of theTrack)
    if theQuery is in theLyrics then
      set match to (artist of theTrack & " - " & name of theTrack)
      exit repeat
    end if
  end repeat
  
  if match is not "" then
    play theTrack
    get match
  else
    get "No match found"
  end if
end tell

You’ll notice that it runs through your entire library in alphabetical order. Unfortunately, if the song you’re looking for is by ZZ Top the search is going to be a hell of a lot slower than if it were by ABBA.

Also, lyrical clichés like “oh baby” or “tell me why” are likely to produce a match earlier than you expect so try to search for longer or less common phrases.1

If anyone has suggestions for improvements please let me know. I like the simplicity of the script so don’t plan to produce a playlist of search results or anything like that but feel free to use the Applescript as a starting point for your own script!

  1. It’s actually pretty fun to guess at lyrics and see what songs they appear in. If a search for “hands in the air” or “in da club” return in less than 10 seconds your iTunes may be due a clear-out.


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.


Quick Tip - Previewing Icon Designs

Previewing an icon while editing in Photoshop

When designing an icon in Photoshop (or your illustration app of choice), a neat way to preview it downscaled to its intended size is to reveal a sliver of desktop behind the window and save your working copy there.

If you have icon previews enabled, you should see a nicely resampled image that updates every time you hit save! You can resize the preview by tweaking your desktop icon size (ctrl+click > Show View Options).1

I imagine this tip would work especially well if you’re lucky enough to have a HiDPI monitor. I still use Iconsider to preview icons on my iPhone’s retina display.

  1. You can do something similar within Adobe applications via Window > Arrange > New Window for X but zooming right out results in poor resampling and loss of detail.


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.


Shushify

Spotify is a great service but I don’t use it anywhere near enough to warrent paying £10 a month for a Premium subscription. I only use the app occasionally to check out a band I’ve had recommended before buying their album on iTunes or leaving it.

I do use Spotify just enough for the ads to get annoying though. To get around hearing them, I created a simple Mac app called Shushify.

Shushify icon Download Shushify

To listen without ads, just launch Shushify.app instead of Spotify.app. Spotify will open as normal but when an advert begins to play Shushify will temporarily mute your Mac’s audio.

Shushify works by querying Spotify for track info every time a new one starts playing (and every few seconds for good measure). The process will quit when you quit Spotify.

You’ll still get an awkard silence for the duration of adverts but that’s good enough for me. If you find yourself using Spotify a lot, please consider paying for a subscription; it really is a great service.

Shushify is offered as-is, unsupported. There’s a good chance Spotify will break it in the future but hopefully it’ll come in handy for some people until then.


Handy Scripts

I have a bunch of Applescripts and shell scripts that live in Alfred and make my computing life a little easier. Here’s a collection of the the ones I’ve written. Most of them are pretty niche but hopefully they’ll prove useful to somebody.

Toggle Invisible Files

If invisibile files are hidden, show them, and vice versa. (Note: will restart Finder).

Top Google URL

Grab the URL of the top result for a search (using the Google Search API) and copy it to the clipboard.

Open Tab in Chrome

Opens the frontmost Safari tab in Google Chrome.

Obfuscator

Takes a chunk of text and converts everything to HTML character entities. Useful for encoding email addresses for spam-free publication on the web. The result is copied to your clipboard.

App Installer

Move any .app files from ~/Downloads to /Applications, overwriting existing apps.

5by5 Show Notes

Open the links list for the 5by5 show that is currently playing in iTunes.

Post Archive

2016

2015

July

2014

February