April 2nd, 2019

When Should I Head Home From WWDC?

This question comes up every year, and I’ve seen it floating around Twitter today.

When should I head home from WWDC?

WWDC runs from Monday morning to Friday afternoon, but it’s mostly “done” by lunch time on Friday, with a few labs running into the afternoon. Most answers I see debate between heading home on Friday afternoon or Saturday morning.

I imagine it’s too late now since most people have probably booked their flights already, but allow me to propose an alternative.

Fly home on Monday. Especially if you’re heading back to Europe.

Let me explain.

You’ve just spent a week smack dab in front of a huge firehose of new information and exciting features. Your brain is still processing it all, and is full of exciting ideas of how you’ll spend the time between WWDC and the next public iOS release in the autumn.

Basically, you won’t rest until September.

Last year, instead of flying home right away I headed over the hills from San Jose to Santa Cruz, and spent the weekend basically doing nothing that required brainpower. I went biking on a rented bike, and took an open top train on a tour through the countryside.

Those two days were the best professional days of my entire 2018. Chilling out and letting the week’s craziness sink in at its own pace was a wonderful end to the week — instead of my WWDC week memories being capped with a stressful run to the airport and losing my weekend so I could be back at the office on Monday, it was capped with mountain biking and trains and sitting on a beach watching the sun go down.


Thanks to some local knowledge from a friendly hotel staff member, I was able to sit and watch the sun go down over the Pacific without a single other person in sight. A perfect relaxing end to one of the craziest weeks in the iOS dev calendar.

It’s incredibly important to look after your mental health, and crunching through the summer for the next iOS release is often draining. Just taking a couple of days to relax and let the new stuff settle in before hitting Xcode can do wonders.

Of course, this won’t be for everyone. However, I urge you to consider it! Hotels outside of the WWDC bubble are significantly cheaper, and if you’re travelling for your employer a lot of official company travel policies even say you’re not supposed to travel for work on weekends1!

Last year was the first time I tried this out, and I’m fairly sure this will be a standard tradition of mine going forwards. I didn’t even get a ticket last year — I was just in town for socialising and AltConf.

This year I did get a ticket, and I hope to see you there! Even better — I hope to see you chilling out somewhere the weekend after!

  1. Much to the annoyance of managers, I’ve found. I’ve had to push back multiple times to managers trying to make me travel on weekends “because it’s cheaper”. 


February 4th, 2019

Editing, Previewing and Deploying Nanoc Sites Using An iPad

This post is included in the iKennd.ac Audioblog! Want to listen to this blog post? Subscribe to the audioblog in your favourite podcast app!


For the first time in this blog’s history, I am going to try my very best to write, edit, polish and deploy a post using only an iPad (sort of). I’ll let you know if I was successful at the end!


Unfortunately, the power button on the iMac G3’s keyboard does nothing on an iPad.

The unfortunate reality of the iPad right now (in early 2019) is that for many workflows, it simply isn’t viable as a replacement for a “real” computer. For the workflows that can be done entirely on an iPad, those that manage to do so end up allowing us to modify an old joke:

How can you tell if someone uses an iPad as a laptop replacement? Don’t worry — they’ll tell you!

This isn’t to belittle their achievements — building a viable workflow for any serious task that requires more than one app on the iPad is a real challenge, and people are damn right to be proud of their collections of Shortcuts and URL callback trees.

However, slowly but surely the iPad is getting there as a desirable computer for getting work done. Personally, the 2018 iPad Pro crossed over this line for a couple of reasons, and for the first time in the iPad’s history, it’s a computer I want to carry around with me and use for “real” work.

Self-Inflicted Development Hell

Unfortunately for me, I’m a developer. Because of that, when I see a problem, I come up with a developer solution. Most people have been able to write articles for their blog on their iPad for years - they just use Safari to log into Squarespace, Wordpress, or whatever else they’ve chosen and write away.

My blog, however, uses nanoc. Nanoc is a program that takes a pile of files, processes them, and spits out another pile of files that happens to be a website. I then upload this pile of files to my webserver, and my article is live!

To do this, I simply open my terminal, cd into the directory of my blog, then run bundle exec nanoc to generate… and we can see why this doesn’t work on an iPad.

Developer Solutions to Developer Problems

So, what do I really want to do here? I want to be able to:

  1. Write blog posts on my iPad.

  2. Preview them on my iPad to check for layout problems, see how the photos look, make sure the links are correct, etc.

  3. Once I’m happy with a post, publish it to my blog.

Step one is easy enough - I find a text editor and type words into it. However, step two is where we fall over pretty hard. Many editors can preview Markdown files, but they only preview them “locally” - they don’t put the preview into my website’s layout, won’t display photos, and generally won’t parse the custom HTML I put into my posts sometimes.

To achieve this, we really need to be able to put the locally modified content through nanoc and display the output through a HTTP server. This is easy peasy on a traditional computer, but not so on an iPad.

Here we arrive at why I’m only sort of writing this post using an iPad — while I am sitting here typing this post on an iPad, I have a computers elsewhere helping me along a little bit. My solution has:

  • A continuous integration (CI) server watching my blog’s repository for changes, then building my blog with nanoc for each change it sees.

  • A static web server set up to serve content from a location based on the subdomain used to access it.

As I’m writing this, I’m committing the changes to a branch of my blog’s repository - let’s say post/nanoc-on-ipad. Once I push a commit, my CI server will pick it up, build it, then deploy it to the web server. I can then go to http://post-nanoc-on-ipad.static-staging.ikennd.ac to view the results. It’s not quite a live preview since my blog is ~400Mb of content and the build server takes a minute or two to process it all, but it’s enough that I can write my blog post with Safari in split view with my editor, and I can reload occasionally to see how it’s going.

My Setup

The first thing we need to do is get a CI server to build our nanoc site. I won’t actually cover that directly here - there are lots of CI services available, many of them free. Since nanoc is a Ruby gem, you can set up a cheap/free Linux-based setup without too much fuss.

I’m using TeamCity running on a Mac mini, mostly because I already had that set up and running for other things. TeamCity has a pretty generous free plan, and I get on with how it operates pretty well.


TeamCity’s web UI on iPad isn’t quite perfect, but it functions just fine.

The second thing we need is a web server. Now, when I suggested the idea of serving content based directly on the domain name being used, a web developer friend of mine made a funny face and started talking about path sanitisation, so I spun up a new tiny Linode that does literally nothing but host these static pages for blog post previewing. I set up an Ubuntu machine running Apache for hosting.

Now for the fun part!

Linking It All Together

We’re going to be taking advantage of wildcard subdomains so we can preview different branches at the same time. For my personal blog it isn’t something I’ll use that often, but it’s handy to have and is definitely cooler than just having a single previewing destination that just shows whatever happens to be newest.

In your DNS service, add an A/AAAA record for both the subdomain you want to use as the “parent” for all this, and a wildcard subdomain. For example, I added static-staging and *.static-staging records to ikennd.ac and pointed them to my server.

Next, we want to make Apache serve content based on the entered domain. Manually (or even automatically) adding Apache configuration for each branch is too much like hard work, but we can use mod_vhost_alias to help out out. It’s not a default module in the Apache version I had, so a2enmod vhost_alias to enable it.

My configuration looks like this:

DocumentRoot /ikenndac/public_html/content

<Directory /ikenndac/public_html/content> 
    Options None
    AllowOverride None
    Order allow,deny
    Allow from all
    Require all granted
</Directory>

<VirtualHost *:80> 
    ServerAlias *.static-staging.ikennd.ac
    VirtualDocumentRoot /ikenndac/public_html/content/%0/
    ErrorLog /ikenndac/public_html/static-staging.ikennd.ac.error.log
    CustomLog /ikenndac/public_html/static-staging.ikennd.ac.access.log combined
</VirtualHost>

That VirtualDocumentRoot line is the important part here. If I go to http://my-cool-blog.static-staging.ikennd.ac, thanks to that %0 in there, Apache will look for content in /ikenndac/public_html/content/my-cool-blog.static-staging.ikennd.ac.

Once this is set up and running, our web server is ready! The final part is to get the content from our CI build onto the web server in the right place.

nanoc has the deploy command, but as far as I can figure out, it doesn’t support dynamically setting the destination directory, so we can’t use that. Instead, my blog’s repository contains a script to do the work:

# Get the current branch name
BRANCH_NAME=`git rev-parse --abbrev-ref HEAD`

# Replace anything that's not a number or letter with a hyphen.
SANITIZED_BRANCH_NAME=`echo "${BRANCH_NAME}" | tr A-Z a-z | sed -e 's/[^a-zA-Z0-9\-]/-/g'`
SANITIZED_BRANCH_NAME=`echo "${SANITIZED_BRANCH_NAME}" | sed 's/\(--*\)/-/g'`

# Build the right directory name for our HTTP server configuration.
DEPLOY_DIRECTORY_NAME="${SANITIZED_BRANCH_NAME}.static-staging.ikennd.ac"

echo "Deploying ${BRANCH_NAME} to ${DEPLOY_DIRECTORY_NAME}…"

# Use rsync to get the content onto the server.
rsync -r --links --safe-links output/ "website_deployment@static-staging.ikennd.ac:/ikenndac/public_html/content/${DEPLOY_DIRECTORY_NAME}/"

A couple of notes about using rsync to deploy from CI:

  • Since CI runs headless, it’s unlikely you’ll be able to use a password to authenticate through rsync - you’ll need to set up SSH key authentication on your HTTP and CI servers. I won’t cover that here, but there are tutorials aplenty for this online.

  • If your CI still fails with auth errors after setting up SSH key authentication, it might be failing on a The authenticity of host … can’t be established prompt. If deploying to your HTTP server works from your machine but not in CI, SSH into your CI server and try to deploy from there.

Deploying the Final Result

The beauty of this process that that we’ve been deploying the entire time! If you follow git flow and your master branch only ever has finished content in it, you could point your main domain to the same directory that the CI server puts the master branch and you’re done! If your master branch isn’t that clean, you could make a new deployment branch and do the same there.

My “public” blog is hosted from a completely different machine than the one the CI publishes to, so that’s currently a manual step for me. However, it we be easy enough to modify my static-staging-deploy.sh script to rsync to a different place if it detects that it’s on the deployment branch.

Conclusion

Phew! This was a bit of a slog, but the outcome is pretty great. With everything connected together, I can work on my iPad and get a full-fat preview of my blog as I write. No “real” computer required (except the one running the CI server and the other one running the HTTP server)!


I kind of want a mouse…

It’s not perfect, of course. Like many “I can do real work on my iPad!” workflows, it’s a pile of hacks — but I’m at least part of that club now!

The real downside to this is the latency between pushing a change and it showing up online. This is mostly caused by my setup, though:

  • My CI server isn’t on a public-facing IP, which means GitHub webhooks can’t reach it. This means that the server has to poll for changes, adding quite a lot of time until the build actually starts.

  • It takes the CI server towards a minute to build my blog and deploy it to the HTTP server. The vast majority of this time is taken with processing all the photos and videos that have accumulated here over the years — splitting that out to a separate repository will significantly reduce the amount of time it takes.

All in all, though, I’m really happy with the outcome of this experiment. Real computers can suck it!

Apps Used To Write This Blog Post

I was pretty successful in writing this post on my iPad. I used the following apps:

  • Working Copy for text editing and git work.

  • Prompt for SSHing into my HTTP server to tweak some configuration.

  • Cascable for copying photos from my camera and light editing.

  • Affinity Photo for sizing photos down to the right dimensions for my blog.

Maybe next time I’ll even manage to do the Audioblog recording on my iPad!


February 2nd, 2019

Despair, Thy Name is App Store

I’m sitting here at 2am, the glow of my laptop screen illuminating my hands as I type. My wife is upstairs, worried about me but powerless to soothe my mind. She can’t sleep either.

Most of the time, it’s fine. It’s fun. I write an app and ship it to the world. I don’t make a huge amount of money from it, but topped up with a little bit of income from part-time consulting, I have a nice little business. I’m proud of it.

Sometimes, it’s not fun. The problem with running a small business is that you’re a tiny cog in multiple corporate machines. Not important enough to get noticed, but dependent enough on them that a tiny blip in their system can ruin you. The last time I was up at 2am, staring at my laptop with a worried wife upstairs was also because of Apple. That time it was App Review, or something. Probably something to do with subscriptions.

On Wednesday afternoon, I accidentally shipped the worst bug of my career. On Thursday morning, I fixed it, pushed an update to the App Store, and thankfully it got approved quickly.

Unfortunately, there’s currently a glitch in the App Store, and it’s still serving the broken version of my app to the world alongside the release notes and version metadata of the fixed one. “Fixed the crash!” it gleefully claims, cruelly delivering a very much unfixed binary. I’ve since uploaded a second update in the hopes that it’d get unstuck. No dice. The App Store is now serving a build from two versions ago alongside metadata from the current version.

There’s no way to call in to Developer Support that I can find any more, and the old numbers I have don’t work. The contact site is selling me the EU call centres have closed and won’t let me contact the US ones. None of them reopen until Monday now, anyway.

I’ve spent the entire day trying to fix this. An hour on the phone with EU Developer Support, who were trying to help but ultimately were powerless.

My only two options now are to let the fates decide when my problem gets fixed, or to completely remove my app from the App Store. Both options are bad. I can’t speak to anyone at Apple for well over 48 hours. Pulling the app makes it look like my business has disappeared and customer faith plummets. Leaving it up risks hitting that one user who’ll shout from the rooftops how you’re a scam artist and stealing people’s money.

When this tiny blip in the App Store’s CDN propagation goes away, I’ll forget about it soon enough. Hell, in the morning this post will probably seem melodramatic even to me, even if the problem is still ongoing. Especially if it’s resolved.

I’m writing this for the next time I’m sitting at my laptop at 2am, head in my hands, wondering why I’m gambling my livelihood and reputation on a company that takes 30% of my app’s sales and delivers, well… this.

This time it’ll be fine. The next time too, if I’m honest. But after that? I don’t know how many more times I can take this. Then again, this kind of stuff happening occasionally is pretty much par for the course in small business.

Sorry to complain. Stiff upper lip, and all that.


December 20th, 2018

Introducing the iKennd.ac Audioblog!

This post is included in the iKennd.ac Audioblog! Want to listen to this blog post? Subscribe to the audioblog in your favourite podcast app!


For well over a year, I’ve been talking about doing a podcast. In fact, in late November 2017 I made a handshake promise with a friend that we’d both get the first episode of our podcasts out “by Christmas at the latest!”, so I’d already been going on ab-out it for far too long a year ago. (She hasn’t released hers either, so we did at least do the same thing!)

The thing is, I have lofty plans for my podcast. It’ll have guests, and a theme that runs through each episode. Hopefully, it’ll carve out its own unique little niche within the rather crowded genre of developer podcasts and will provide some genuine value and interest to its listeners.

The problem with starting a podcast with the goal of having guests, and a central theme, and its own unique little niche, and genuine value and interest… is that it’s a lot of work. Especially if it’s your first podcast. I did make some decent strides — I have the equipment, and I did some work on the first few episodes (even finding guests!).

However, I don’t want to waste my guests’ time by putting out shoddy work, and I want to to start out the gate with a great first episode… and, well, writing this now, I realise how unrealistic my own expectations were, which explains over a year of procrastination.

So, I hereby announce… I am not starting a podcast just yet. (Pause for applause.)

The thing is, I really love contributing to the community. Even though my blog has been relatively quiet for the past couple of years, I’ve been giving talks here and there. In September, I gave a talk at the Swift and Fika conference here in Stockholm entitled Adventures in API Design, in which I mixed some stories of what I’ve been up to over the past couple of years with some useful advice about designing APIs. More recently, I spoke at a CocoaHeads meeting about writing command-line apps with Swift Package Manager, in which I talked about drone photography and some tips and tricks for writing little command-line apps in Swift.

I don’t have any formal training in giving talks, but with each one of these I give, I get better. I still say the word “so” far too much, and I forget to breathe half the time so I end up out of breath, but I’m improving! And, importantly, I’m having a lot of fun! And, most importantly, people tell me they like my talks and get something useful from them.

I really want to get started with my awesome, guest-filled podcast. However, I need to learn and get better at it before I can.

So, I hereby announce… The iKennd.ac Audioblog (Pause for applause?)

In order to get comfortable with my equipment and being behind a mic, with audio editing, and the whole experience of hosting a podcast, I’m starting an audioblog. It’s like an audiobook, but a for a blog! For each post on this blog going forward, I’m going to try to make an audio counterpart that’s of a decent quality and engaging to listen to. I have a couple of really meaty posts planned for the next couple of months, so recording these should give some great practice for when Daniel’s Awesome, Guest-Filled Podcast1 comes along sometime in 2019.

In fact, this post is on the audioblog! Why not give it a listen? I’m genuinely interested in hearing any feedback on audio quality, how I sound, if I manage to make the listening experience engaging, and so on.

You can subscribe to the audioblog via links at the top of this post.

  1. Title TBD. 


November 2nd, 2018

Why Publishing Some Nice Autumnal Photos Online Made Me Write An App

Don’t care about programming and just want to see some pretty photos of colourful trees? Check out Autumn From The Air on my new photos subsite. Enjoy!


A few months ago, I bought a drone with the idea of expanding the horizons of my photography hobby a little. I even had a dream photograph in mind - a rolling shot of my car driving along a mountain road. A few weeks later, I was standing on the side of a mountain road in the Alps, trying to take pictures of my car as my wife drove it up and down a section of mountain road.

As it turns out, taking a long exposure of moving object A with moving camera B while standing in stationary position C is incredibly difficult. Over a couple of sessions I took hundreds of photographs, and got four that I’m happy with.


This photograph took many, many tries to get.


This photograph did not.

This was amazing! I have a flying camera! It’s basically an infinitely adjustable tripod! I can even take rolling shots like this without hanging out of the back of a car!!


It’s just a fancy tripod, really.

Bureaucracy Strikes!

Excited about the possibilities of this magic new camera, I came home and started learning and experimenting, having a lot of fun in the process. However, here in Sweden the laws surrounding aerial photography are very strict — you’re not allowed to publish any aerial photographs without approval from Lantmäteriet, a Swedish agency dealing with land and property.

There’s a valid discussion on how sensible this law is for private drone usage, since it’s a law written in mind for imagery taken with planes and helicopters. Still, the law’s the law, and I had a great set of autumn photos I wanted to share. Lantmäteriet has an online form for this, which requires each image’s location, street address, and property allocation (which is looked up on Lantmäteriet’s own map).

This submission process is, quite frankly, a massive pain in the ass. It took me 45 minutes to build the submission for 24 photos - manually pasting the coordinate into Maps, doing an address lookup, then going to the Lantmäteriet map to perform the other lookup there, scrolling and clicking around the map because you can’t give it WGS841 coordinates.


Zzzzzzz…

Like Everything, This Can Be Solved With Software!

I finished my submission, then immediately got to work automating this, because screw doing that again.

The most complicated part of the process is converting the WGS84 coordinates in my images’ geotags into the SWEREF coordinate system that Lantmäteriet uses. It turns out that doing this well is hard, and I found some existing code to port over - it’s several hundred lines!

After a few evenings of hacking, my 45 minutes of manually looking up things on two different maps can be reduced to typing this into my terminal:

$ lantmateriet-lookup -i *.jpg -html results.html

…then waiting 30 seconds while it does its magic. Lovely! Under the hood, it’s:

  • Extracting a geotag from each image using ImageIO.
  • Using CoreLocation to do a reverse geocode to get an address.
  • Converting the WGS84 geotag coordinate into SWEREF.
  • Doing a lookup on what is definitely a public Lantmäteriet API to get the property allocation.
  • Writing the results of all that into a table for submission to Lantmäteriet.

This is going to save a bunch of time for anyone that takes photos with a drone in Sweden, so I’ve made it open-source - you can find it here: lantmateriet-lookup on GitHub.

…I Was Promised Autumn Photos?

While I was faffing about with all of this, Lantmäteriet approved my original submission. Hooray!


The slow currents of Mälaren disturb mud around an underwater rock.

I put together a photo story of my favourite aerial photos of the area around where I live, which you can find over on my new photos subsite: Autumn From The Air. Enjoy!

  1. WGS84 is the coordinate system used by GPS and many other mapping and navigation systems. If you see a GPS or map coordinate as you’re going about your business, it’s very likely that it’s a WGS84 coordinate. 


April 6th, 2018

App Store Subscriptions And You

For the average iOS developer, implementing App Store subscriptions is easily the most legalese-filled part of the entire process of making an app and shipping it to the world.

What makes this more difficult is that right now, App Store subscriptions for “normal” apps (i.e., those that aren’t content services like magazines or Netflix) are reasonably new, and Apple appears to be finessing the rules over time. This can cause a frustrating situation as you try to do your best but end up getting repeated rejections due to your app not meeting the rules.

At the time of writing, I’ve been shipping an app that uses subscriptions for eight months, and have had multiple subscription-related rejections happen between my first release and now due to changing rules and changing enforcement of existing rules. The information in this post is a combination of my experience, as well as conversations with App Review both via email and phone. Hopefully the additional context provided by speaking to a human being from App Review on the phone will be as helpful to you as it was for me.

Important: This post was as correct as I could make it at the time of writing (early April 2018). The App Store review guidelines are a constantly changing thing, particularly in the area of subscriptions. You must do your own due diligence.

The Paid Applications Contract

A lot of the confusion from this stems from the fact that half of the rules for subscriptions aren’t in the App Store Review Guidelines, but are instead located inside your Paid Applications Contract. You can find this in the Agreements, Tax, and Banking section of iTunes Connect.

Assuming that you have the standard contract, in-app subscription terms can be found in section 3.8.

Important: Your ability to sell apps on the App Store depends on your adherence to and understanding of this contract. Since this is a legal document, I will not be able to help you with it. This post is intended to be a guideline only, and I can’t be held responsible if you encounter problems following it. If you have questions or problems with this contract, consult a lawyer.

What We’re Building

This is a screenshot of my app’s store. It provides users the option of buying a one-off In-App Purchase or one of two subscription options. This store page was approved by App Review in early April 2018.

As well as getting the in-app UI correct, you must also include details of your subscriptions in your app’s App Store description. We cover this towards the end of the post.

You Must Be Clear About Pricing And Billing Frequency

You must be very clear about several things:

  • That the user will be paying for a recurring subscription.
  • How much the user will pay each time.
  • How often they will pay.
  • The pricing must be in the user’s App Store currency.

You’ll see in my screenshots that my device is set to English, but I’m being presented prices in Swedish krona (SEK). This because I live in Sweden (so my cards are all in SEK) but I’m bad at Swedish, so my iPhone is set to English. You can’t use the system locale for In-App Purchase pricing - instead, the SKProduct objects you get in the App Store will contain the locale you should use for displaying prices.

A common thing to do is to offer multiple subscription options, giving the user better value for money if they commit to a longer subscription period. This is fine, but you must list the actual amount that will be charged in your pricing.


While this is good at showing the increased value of the longer subscription, exactly how much money is charged when is unclear. This is not allowed.


How much money is charged when is much clearer here. This is allowed.

You Must Be Clear About Trials

If you offer a free trial, you need to be clear about that as well.

Important: If the user takes up a subscription with a free trial then later cancels, if they want to re-subscribe they will not get a second free trial. You must reflect this in your UI so you don’t end up promising a free trial that the user won’t get.

One way to do this is to fully parse your application’s receipt — each subscription period will have an entry in the receipt. If you have one or more entries for your subscription’s identifier and all of them have expired, the user had a subscription in the past and won’t receive a free trial if they re-subscribe.


The user is eligible for a free trial, so we make it clear that they’ll get a free trial and then they’ll be charged.


If the user is not eligible for a trial, we don’t mention it at all.

You Must Include The Correct Legalese

This is the one that seems to cause the most problems, since legalese is hard and there’s a lot of it.

It’s very important to Apple that it’s impossible for the user to buy a subscription without seeing the legalese. This means that you can’t hide it behind a “Subscription Terms” button - this would be a fork in the flow, and is not allowed.

An exception to this is that you are allowed to have the legalese scroll off the bottom of the screen, as long as it is completely clear that there’s more content to read, and that you’re not hiding all of the legalese “below the fold”.


Here, the legalese is entirely hidden off the bottom of the screen. Even though the user can scroll to it, this is not allowed.


Here, it's very clear that there's legalese to read, and that you can scroll to read more. This is allowed.

You must also include a link to your website’s Terms & Conditions, as well as your Privacy Policy, alongside your buy buttons and legalese. It is allowed to have these be one page.

You can find the legalese you need to include in section 3.8b of your Paid Applications Contract. It can be a little confusing since the language is still very much aimed at magazines in places, but you should write language that makes sense for your app rather than just copy and pasting. Don’t be too put off - it’s possible to be very efficient with words and include everything without too much text.


Here we can see my store page on an iPhone X, which is big enough to display everything without scrolling. The legalese paragraphs and Terms/Privacy buttons are visible here.

At the time of writing, the standard contract requires that we state the following information to users.

Important: This was correct in my contract at the time of writing (early April 2018). You must check your own contract!

Title of publication or service

The name of your app or subscription. Ours is Cascable Pro.

Length of subscription

In my examples here, this is in the subtitle of the buy buttons.

Price of subscription

Also in the subtitle of the buy buttons.

Payment will be charged to iTunes Account at confirmation of purchase

Covered in the first paragraph of my legalese.

Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period

Covered in the first paragraph of my legalese.

Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal

Covered in the first paragraph of my legalese.

Subscriptions may be managed by the user and auto-renewal may be turned off by going to the user’s Account Settings after purchase

Covered in the first paragraph of my legalese.

Links to Your Privacy Policy and Terms of Use

Green button at the bottom of my legalese.

Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable

This one is a little confusing at first, since the language seems geared towards magazines and in most apps it’s impossible to buy something that would render an active subscription invalid. However, after speaking to App Review, I was advised that even if it didn’t completely make sense for normal use of my app, I should include it unless I had very strong opinions about it not being there — at which point they’d have to have internal discussions about what to do. I feel like “internal discussions” means “a very long wait”, so I was eager to avoid this.

Since it is technically possible to buy two Cascable Pro products at once if you really try hard1, I wrote the second paragraph of my legalese with this in mind.

You Must Also Include Subscription Details In Your App Store Description

When submitting your app to the App Store, you must also detail your subscriptions in the same manner as in the app, including:

  • A list of the subscriptions, including their durations and prices.
  • The same legalese as you put on your in-app store page.
  • A link to your Terms & Conditions and Privacy Policy pages.

Since your app’s description is static content, the rules are a little more lax regarding the prices. So far, I’ve been fine listing the “normal” prices of the subscriptions in US dollars in all languages. You should still be able to run promotions etc without updating the price in your app description.


Good luck!

  1. Install the free version of Cascable on two devices with the same Apple ID. Purchase a subscription on the first device, then purchase a different subscription on the second without doing a “Restore Purchases”. Tada! Two subscriptions. 


January 16th, 2017

Excuse Me Sir, But Can I Rattle Your MacBooks?

Back in 2001 I had a G4 Cube that I loved dearly, and a then state-of-the-art iPod that plugged into one of its two Firewire ports. Unfortunately, that Cube loved to fry its Firewire ports — several trips to the repair centre meant walking miles to my friend’s house so I could rip my CDs to his second-generation iMac and then onto my iPod.

Since then, I’ve had great luck with Apple products. Apart from a PowerMac G5 that couldn’t survive having Coke poured into it and the odd iPhone that didn’t like being smashed into the ground, I’ve had 15 years of mostly trouble-free experience with Apple hardware.

Unfortunately, this has come to an end with the 2016 MacBook Pro. Now, I’m not normally one to complain about stuff on my blog, but I feel the journey I’m still undergoing with this machine is kind of fascinating — and an interesting insight into what happens when good customer service and poor products clash. Also, this is by far the worst experience I’ve had with Apple hardware in my life.

MacBook Pro #1 & #2: A Normal DoA Experience

In December, my wife borrowed my MacBook Pro for something and called to me: “Did it always make this noise?”, demonstrating a metallic, springy-sounding noise when she placed it onto a table. We shall call this metallic, springy-sounding noise Rattle A, which will be important later.

No, it did not.

A call to Apple later and a new MacBook Pro (MBP #2) is being assembled and shipped to me. Great! Unfortunately, since I ordered a machine with a custom spec, it’s coming all the way from China. At the moment, it’s no big deal — the occasional DoA product is part of life.


MBP #1’s rattle.

A couple of weeks later, the new machine arrived at my door. I unbox it, and give it a little side-to-side shake. Immediately out of the box, it makes a plasticky clonking sound which you can feel through your hands. We shall call this plasticky clonking sound Rattle B.

After some bitching on Twitter, another call to Apple and about 45 minutes on hold gets me put through to some senior department. Very sorry for my bad luck, a second replacement (MBP #3) is being assembled and shipped to me, again from China. The agent agreed that it’d be silly to transfer my data to MBP #2 when MBP #3 is on its way, so a return for MBP #2 is arranged. The next day, it leaves my house.


MBP #2’s rattle.

MacBook Pro #3: Excuse Me Sir, But Can I Rattle Your MacBooks?

This is where it starts to get a bit abnormal.

MBP #3 turns up, and immediately out of the box it exhibits Rattle B. I call Apple again, and eventually get to a nice lady in after-sales who’s very sympathetic to my bad luck, and is adamant that they’ll keep sending me MacBook Pros until I get one that doesn’t rattle.

However, I’ve been doing some of my own research and I’m starting to think that Rattle B is a systemic problem. I explain my (entirely anecdotal) thinking and we come up with a plan: I’ll go to the Apple Store and see if any machines on display there exhibit the same problem. If not, I’m just having terrible luck, right?

So, at opening time on Saturday morning I walk into the Apple Store and try to explain to the employees there that:

  1. I want to shake their MacBook Pros.
  2. I’m not crazy.

After surprisingly little convincing, they let me go ahead. In the eight MacBook Pros I tried, two of them exhibited Rattle B.


A rattling MacBook Pro at the Apple Store.

I return home resigned to having a MacBook Pro with Rattle B. Annoying, but I don’t tend to shake my MacBook Pro much, so it’s not a huge issue to live with. I take the machine out of the box, unwrap the plastic and set it down on the table.

Clank.

Praying that I’m hallucinating, I pick it up and set it down again.

Clank.

MBP #3 exhibits both Rattle A and Rattle B. Superb. Time for a Twitter rant.


MBP #3’s rattle.

MacBook Pro #4: Maybe I Am Crazy!

At 10am this morning, the phone rings with the promised callback from the lady I spoke to on Friday.

After explaining my results at the Apple Store and the fact MBP #3 is the worst one so far, we come up with another plan, and we see what happens when your customer service greatly outclasses the quality of your product:

MBP #4 is being assembled and shipped, again from China. However, this time it’s being shipped to the Apple Store, where I can inspect it and hand it straight off for repair if it continues to show these problems.

I’d like to repeat that last part, for emphasis: An agreed plan with customer service is for the product to be shipped to a store with the expectation that it’ll immediately go in for repair.

What Next?

If this were almost any other company (or if I were new to Apple), I’d have given up at MBP #2. However, Apple have 15 years of good experience in the bank, as well as very good customer service trying their hardest to make this current issue right.

However, all that goodwill is goneMBP #4 will be their last chance. The Apple Store is a 1hr 30min round trip from my home, something I’ll probably have to do twice — once to find out MBP #4 rattles too, and again to collect it after it’s been repaired.

Here’s a timeline, for brevity:

2016-12-17 First call to Apple about MBP #1.
2017-01-02 MBP #2 arrives.
2017-01-02 Call to Apple about MBP #2.
2017-01-04 MBP #2 is collected for return to Apple, MBP #3 is ordered.
2017-01-09 MBP #3 leaves China.
2017-01-13 MBP #3 arrives.
2017-01-14 "Excuse me, but can I rattle your MacBooks?" at the Apple Store.
2017-01-16 Call Apple, MBP #4 is ordered for delivery to the Apple Store.

Some reaction I’ve received on Twitter is questioning why I care so much about a rattle. This machine cost 32,595 SEK (~$3,650 USD, ~£2,990 GBP, ~€3,400 EUR), and for that ludicrous amount of money, I expect a computer with all of its components attached together properly. I don’t think that’s unfair, and so far Apple customer support agrees with me.

The interesting question comes if MBP #4 still rattles. While I’m fortunate that this machine isn’t (yet) my primary computer, I have a business to run and unfortunately I’m a Mac and iOS developer, which basically requires that I own a Mac. I really want this MacBook Pro to replace my iMac so I can have a more portable work machine, but if Apple can’t sell me a computer I’m happy with — what then?

In the words of the greats: I’m not angry, I’m just disappointed. Maybe I should develop for Windows Phone instead.


October 28th, 2016

Launching Cascable 2.0

Cascable is the app I’ve been working on since early 2013 — firstly as a side project, then as a full-time endeavour starting mid-2015. You can read more about this journey in my Secret Diary of a Side Project series of posts, the first one of which can be found here.


“It won’t be as stressful as the 1.0 release”, I lied to my myself as much as my wife when she asked me how I was feeling about launching Cascable 2.0 the next day. I’d woken up a couple of times during the night in the past couple of weeks gnashing my teeth, causing a big chip in one of my teeth.

The truth is, the 2.0 launch ended up being much more stressful than 1.0, although I genuinely didn’t see it coming. Cascable 1.0 was a product of a side project — it shipped a few months after I quit Spotify, and a lot of that post-Spotify time was working on ancillary details like the website, marketing, documentation, and so on.

Getting to 2.0

Version 2.0 shipped on August 11th, 2016 and was the result of nine solid months of work, starting in October 2015 with this tweet:

Nine months is a very long time to be working on a single update, and it can be really damaging to your self esteem, particularly when working alone. Roughly 300 tickets were solved between starting 2.0 and shipping it. That’s 300 issues. 300 things wrong with my code. 300 times myself or someone else had opened up JIRA and created a ticket to describe something was missing or broken with my code.

Of course, this is part and parcel of being a developer. However, you typically have other developers working alongside you to share the burden and a reasonable release cadence that (hopefully) provides real-world evidence that your work is good enough for production.

In the weeks before the launch, I didn’t feel stressed at all — we’d had a very long TestFlight period with over 100 testers over all the different camera brands Cascable now supports and all of the major issues were ironed out. I’d enforced a feature-freeze at the beginning of June, and a ship-to-App Store date of July 29th. That’s two months in feature freeze and two weeks between uploading to the App Store and releasing — plenty of time to iron out any issues before shipping, and plenty up time to iron out any App Store problems before releasing.

Plus, this time I had help in the form of Tim, who’d been diligently working away at the website for weeks — this time, it was finished by the time I’d hit code freeze and better than ever - much more content and some lovely extras like a nicely made video.

Everything should be wonderful, right? Lots of time to iron out bugs, help with shipping and over 100 people using the app for a few months should make this launch something to be excited about.

However, those nine months of JIRA tickets had taken their toll. My self-confidence was incredibly low, and I was scared to death that we’d launch and some stupid mistake I’d made would cause the app to crash for everyone, ruining the app’s (and my) credibility. Cascable would be a laughing stock, and I’d have to go find a real job again.

On top of this, with 2.0 Cascable would be transitioning from paid-up-front to free with In-App Purchases to unlock the good stuff. It’s a move we needed to make — a $25 up-front payment is an impossible sell on mobile — but a huge risk of doing this (and well-known enough that it was the first thing every developer friend I have mentioned when I told them of this plan) is receiving a massive amount of support email from free users and unfair one-star reviews.

“You realise that you’ll immediately get people downloading it without looking then leaving you one-star reviews because it isn’t Instagram right?”, said one.

As the Cascable launch approached, my belief in my own abilities was at an all-time low, and I was expecting to be buried in an avalanche of one-star reviews and email.

Launch Day

Launch day came, and the app was sitting in iTunes Connect, waiting for me to click the “Release” button. An attempt at having it happen automatically was stymied by a problem with iTunes Connect that resulted in hours on the phone with iTunes Connect support, which ended up making the problem worse. In the end, I had to yank the previous version from sale a few days before 2.0’s launch. D’oh!


This is not the history of a smooth release process!

I clicked the “Release” button, and braced myself for a horrible week.

But, the avalanche never came. Instead, we got great coverage, a big pile of downloads and some really positive reviews.

Looking back, I consider it a very successful launch. Neither my wildest dreams nor my deepest fears came true — the switch to freemium didn’t make me an overnight millionaire, but we didn’t get buried by one-star reviews and support email either.

It’s amazing what shipping code can do to your self-esteem. After a couple of quick point-releases to fix some crashes that did crop up — all of them reasonably rare, thankfully — Cascable’s crash-free sessions metric is in the very high-90% range (on the day of writing, it’s at 98.5%). Of course that can be improved, but between the subjective reviews and this objective data, I’ve completely regained my confidence that I’m able to write and ship a decent product. Hooray!

It’s worth noting again what an incredible difference having someone helping out on stuff that isn’t code. I don’t think Tim would be upset with me if I said that he’s by no means a professional website builder, nor is he a professional video editor. Yet, thanks to him, I had a burden lifted from my shoulders and Cascable’s launch had that extra layer of quality to it that I’ve never been able to achieve on my own.

So, with all of that self-congratulation out of the way, let’s look at some cold, hard data!

How did the launch actually go?

The established launch pattern for iOS apps is to have a huge launch spike that tails off fairly sharply. This “long tail” is a tough thing to endure, and can be fatal.

Our spike followed normal trends. Here’s our downloads over the first few days of 2.0:


Downloads during the launch.

However, if we compare that to the number of purchases over the same period, a couple of things stick out:


Purchases during the launch.

First, the spike for purchases was a couple of days after the spike for downloads. Second, the purchases graph doesn’t lose quite as much momentum as the downloads graph, which (along with our retention data) shows that a decent proportion of that download spike was from drive-by users — people who had seen the app as part of the initial media push, tried it once, and never used it again.

Was switching to Freemium the right thing to do?

I believe that Cascable is a pro-level tool and should command a pro-level price — particularly for a niche app in the physical photography sector. Yes, $25 is a huge barrier to entry on mobile, and our 1.x sales show that. However, the problem we need to solve is showing users that the app is worth the price it commands.

At the most basic level, yes, it was the right thing to do. Cascable is earning more money than it was than when it was paid-up-front. However, there’s a lot more to it than that!

For several months, my plan was to have the app work with basic features for free, and implement a single In-App Purchase for $25 to unlock the whole app. However, after some discussion, we ended up shipping four separate In-App Purchases, as follows:

Product Cost Description
Cascable Pro: Photo Management $10 Support for RAW images, bulk copying, filtering and searching, image editing.
Cascable Pro: Remote Control $10 Powerful camera remote control and shot automation tools.
Cascable Pro: Photo Management $10 Support for RAW images, bulk copying, filtering and searching, image editing.
Cascable Pro: Night Mode $10 A dark theme for the app.
Cascable Pro: Full Bundle $25 All of the above.

The biggest detractor to this is development complexity. Different parts of the app need different feature checks, and we need to communicate to the user what they need to purchase to get which feature in a non-confusing way. Indeed, the latter point was worrying me up until launch due to the fact we decided that creating a support article with a big-ass table to explain it all was necessary.

In practice, though, I think the user experience isn’t too bad. We’ve only had one support ticket from someone who’d accidentally bought the wrong thing so far, which makes us hopeful it isn’t too confusing for our users.

The upside to all this added complexity is that we get to reduce sticker-shock (“$25?! Screw that!”) and up-sell to the user. We’re trying to avoid the aggressive sales pitch if at all possible, and don’t start asking for money until the user wants to do something that isn’t free.

Here’s a typical flow. Feel free to download Cascable and follow along!

Here’s a typical screenshot of Cascable running as a free user. Notice there’s absolutely no indication they haven’t paid for the app.

Here, the user has encountered a feature that requires them to part with some money. At this point, we don’t pop up a store or otherwise interrupt their flow:

In some places, particularly in lists, we place a “Pro” button in place of the switch or button that would invoke a particular feature:

If they tap on a “Pro” button or a “More Information…” button, they’ll get the In-App Purchase store showing the cheapest available purchase that’ll unlock the feature they’re trying to work with, along with a little video previewing everything that purchase will unlock. The video is shipped as part of the app bundle, so there’s no waiting for it to download.

If the user attempts to purchase the presented In-App Purchase, they’ll be presented with this dialog:

This is where we get a chance upsell the user to the more expensive (but better value for money) purchase. If the user taps “View Pro Bundle”, the purchase will be cancelled and they’ll be shown the video and description of the bundle. Otherwise, the purchase of the requested item will continue.

Finally, once the user has purchased the unlock for a feature, the original message is replaced with controls for the feature itself.

As you can see, even though payment and billing logic is provided by the App Store infrastructure, there’s still a ton of work to do if you want to provide a somewhat rich In-App Purchase experience for your users. Which you do want to do — that little “Give me money!” button is difficult for users to tap!

A little extra touch we added to give some extra gratification to our paid users is a friendly, heart-adorned version of Colin (our unofficial name for the anthropomorphised camera mascot used throughout the app):

This version of Colin is slightly more whimsical than the tone of the rest of the app, but I really love this version of him, and he’s reserved just for our paid users.

So, does our store work?

The following data is taken from a five week period during that long tail after the big spike.

Over the five-week period this data is from, our average conversion ratio from viewing the store to making a purchase was 21%. This compares to a conversion ratio of 4% from all users of the app to making a purchase.

I’m pretty happy with 21% — less so with the 4%. What this data shows us is that we need to get people more interested in the expanded feature set — enough to go into the store to take a more detailed look.

Overall, our paid:free ratio is about 20%, which I don’t feel is too bad.

Does our upsell work?

This graph shows the Entry Point to the In-App Purchase store within Cascable - that is, the product they first see when the store is shown to them. Once they’re in the store, users can swipe left and right to browse all the available options, but the data for that isn’t graphed here. As you can see, the entry point is reasonably evenly spread between the three individual $10 unlocks, with the $25 bundle coming in last. This is because the only way to see the bundle first is to navigate to the “Purchases” item in Settings and tap the button next to the bundle. The rest are encountered when using the app normally.


In-App Store entry point by product over five weeks during our long tail.

This next graph shows the products purchased over the same period. As you can see, the Full Bundle significantly outperforms the other products, despite the fact that it’s more expensive and isn’t the product the user is shown first in most circumstances.


In-App Store purchases by product over five weeks during our long tail.

I think it’s a reasonable conclusion that the upsell is having a positive effect on sales. However, we don’t have enough data to say whether or not this is definitely the best approach. For that, we’d need to compare our upsell to the following scenarios:

1) What if we still had four separate In-App Purchases at the same prices, but without the upsell from the $10 ones?

2) What if there was only one $25 In-App Purchase as originally planned?

However, my feeling is that we’ve hit a nice middle-ground. With no upsell, I’m reasonably confident that we’d sell less $25 bundles, and with no $10 options I think the sticker-shock factor would be too high.

What Next?

Cascable 2.0 shipped in August , followed by an immediate feature update alongside the iOS 10 launch in September. In its current state, I consider the “2.x” app reasonably feature complete — engineering-wise, my tasks are to keep up-to-date with new cameras from our supported manufacturers, keep on top of customer requests, and regroup for Cascable 3.0.

The aim is to make Cascable AB a sustainable business. While it’s not quite there yet, we’re certainly on the right track and the income graph is creeping up towards the expenditure graph.

As tempting as it is to dive into Cascable 3.0 right now, I’ve been looking at nothing but that app for a year now, and I’m risking burnout. Instead, over the next few months we’re taking a radical departure from my own historic approach (SOLVE PROBLEMS BY PROGRAMMING!! codes harder) and will be putting effort into marketing the iOS app we have.

For me, it’s time to take a step back, hand Cascable’s reigns over to Tim for a while, and focus on the long-term future of the company in the form of other engineering projects. This way, I can come back to Cascable 3.0 fresh and excited about the new features.

With that in mind, this next couple of months will be focused on the goal of making this company sustainable in the long term in ways that aren’t adding new features to the existing app — it’s feature complete enough that adding individual features won’t make that critical difference.

First Approach: Get more people to use Cascable

First, we’re experimenting with various advertising streams to get users into the app and using it. So far, we’re only in the first phase of this and are trying out Facebook, Instagram, Twitter, Google AdWords and App Store Search ads. It’s too early to draw any conclusions from this, but it seems that App Store Search ads are significantly outperforming the rest.

Additionally, we’re reaching out to photography websites, magazines, camera manufacturers, etc to try and get coverage. It’s difficult for a tiny and unknown company like ours to wriggle through the noise, but we’re starting to get noticed.

Second Approach: Get more people to convert to paid users

We recently shipped an update to Cascable that adds an “Announcements Channel”. This allows us to publish content online for presentation to users inside the app. We’re trying to make this visible to the user without being annoying — no push notifications, no noises, no alerts. Hopefully the little unread indicator won’t be too abrasive to our users.

Our intent is to publish high-quality content roughly once per week at most, mainly in the form of previewing and linking to articles on our website about how to get the most out of Cascable’s features — for example, a detailed article on using Cascable’s automation tools to make time-lapse videos, long exposures of the night sky, and so on.

The channel allows us to present different content depending on what purchases the user has made, so for paid users we can say “Here’s how to make this awesome stuff with what you already have!” and free users we can frame it more towards “Look at the cool stuff you could do if you had this!”.

The intention is to increase conversion from free users while at the same time increasing the happiness of our paid users by helping them get the most of what they have. This will be a tricky line to walk well, though.

Third Approach: Don’t put all our eggs in the iOS basket

Relying on one platform for income gives me the heebie-jeebies, particularly when that platform is one as difficult to reliably make money on as iOS.

In a previous Secret Diary of a Side Project post, I discussed how I’ve been taking the extra effort to make sure our core camera connection stack is architected in a manner that keeps it cleanly separated from the Cascable app and fully functional on macOS as well as iOS.

With Tim working on the first two approaches, I’ve started working on branching out to macOS. Thanks to a fully functional core library, I’ve been able to cash in on this past work and start incredibly quickly — I built a functional (and reasonably polished) prototype of a Mac app in less than two weeks, and we’re aiming to ship it by early December.

Conclusion

As much as being an overnight success is the dream, it doesn’t tend to happen like that in the real world. After a couple of years of hard work, it looks like a sustainable business is starting to get within reach — Cascable’s progress looks remarkably similar to that of my (mostly) successful foray into indie development all the way back in 2005. In fact, Cascable is doing better than my old company was after the same time period, but back then I lived in my parents’ house basically for free — Cascable has a much higher bar to reach in order to be considered “successful”!

As always, feel free to get in touch with me on Twitter.


April 12th, 2016

Secret Diary of a Side Project: No Longer Alone

Secret Diary of a Side Project is a series of posts documenting my journey as I take an app from side project to a full-fledged for-pay product. You can find the introduction to this series of posts here.


It’s been nearly ten months since my last Secret Diary post, and since then I’ve been doing nothing but keeping my head down and plodding along:

  • First, I shipped a couple of bugfix updates.
  • In August 2015, I released a feature update that added some powerful new stuff.
  • In September 2015, I released a feature update that added support for some new platform goodies — WatchOS 2 and iOS 9 split screen.

Other than a couple of minor bugfix updates, there’s been nothing new released since then. So, what’s going on?

Crossroads

It was clear that in its current course, Cascable wasn’t going to be sustainable — a fact everyone (including myself) could see coming a mile away. A niche-level product with limited hardware support and a $25 upfront cost isn’t going to fly in today’s mobile world.

That said, the people who do buy Cascable seem to love it. I’ve had some great reviews and many lovely emails from happy users.

So, what to do? Obviously, moving to a free up-front business model and adding support for more cameras is what we do with the app (and is what I’ve been working on since December), but what about the company?

After a week or two of self-reflection and chatting with those close to me, it came down to the choice of spending my remaining budget in one of two ways:

  1. Carry on by my lonesome for three years.

  2. Hire someone for one year.

This was an interesting choice. Having the freedom to not have to care about income for three years (until mid-2019!) is an opportunity I don’t think I’ll have access to again in my lifetime. However, it severely limits the pace at which I can move and the things I can achieve with Cascable, particularly when taking into account my skill set. In the end, the choice was easy.

Employee #1

As of last week, Cascable has employees! Tim is Cascable’s Head of Stuff That Isn’t Programming, and is responsible for doing all the things I’m either bad at or don’t have time for — all the things that are actually super important for a successful business (marketing, product direction, pricing, etc etc).

Now, the thing with employees is that you no longer have the freedom to fuck around. They’re people who depend on you to have your shit together enough to run payroll and otherwise deal with the stuff that puts food on their table. In keeping with that theme, this will be the very last Secret Diary post I write - thinking about Cascable as a “side project” is completely inappropriate now other people are involved.

Thankfully, having Tim on board means that the weight is lifted from my own shoulders slightly, so I should be able to allow myself the time to write blog posts more often. Hooray!


January 3rd, 2016

Garmin VIRB XE Review Updated

Back in August 2015, I reviewed a new action camera on the market - the Garmin VIRB XE. I really liked it, and sold my GoPro cameras in favour of it. Since then, several software updates have come along, changing the experience quite a lot — particularly if you use the data recording and display features.

As such, I’ve updated my review to reflect what the camera is like in early 2016. Spoiler: It’s better!

 

You can find my full and updated review here.