http://ikennd.ac/Daniel Kennett2024-02-12T15:00:00ZDaniel Kennetthttp://ikennd.ac/tag:ikennd.ac,2024-02-12:/blog/2024/02/swift-on-windows-with-swifttoclr/Proof of Concept Project: Combining Swift and C# on Windows with SwiftToCLR2024-02-12T15:00:00Z2024-02-12T15:00:00Z<p>There are many ways to write “cross-platform” apps - ranging from going all-in on the cross-platform idea and writing a web app in something like Electron, to writing two completely separate apps that happen to look the same and do the same thing. And of course, the internet is <em>full</em> of… let’s say “vibrant” discussion on what’s the best way to do things.</p>
<p>My personal preference is to write the UI layer in a native technology stack in order to take advantage of a particular platform’s look-and-feel, with the “core” logic in a cross-platform codebase that the native layer can interact with. In an ideal world, we’d be able to implement this incredibly complex tech stack:</p>
<p class="center"><img class="no-border" width="440" src="/pictures/swift-on-windows/tech-stack.png"></p>
<p>A drawback of this approach is that it does tend to limit your choice of programming languages for the cross-platform codebase. Programming languages all tend to have their own <a href="https://en.wikipedia.org/wiki/Application_binary_interface">ABIs</a>, and you need to rely on there being a “bridge” available between the two languages you want to use. In practice, this often means finding an intermediate ABI that both languages can interoperate with - quite a lot of languages have compatibility with the C ABI, for instance.</p>
<p>Since I primarily work on Mac and iOS apps, I write code in Swift every day. It’s been getting a lot more love on the cross-platform front than its predecessor in the ecosystem, Objective-C (Swift even has <a href="https://www.swift.org/download/">official Windows builds</a>!), and it’d be <em>great</em> if we could ship CascableCore in Swift to multiple platforms.</p>
<p>However, the challenge comes not necessarily from compiling our Swift code on Windows, but from using it from <em>other</em> languages. Specifically in this case, I’d like to write a C# app using <a href="https://learn.microsoft.com/en-us/windows/apps/winui/">WinUI 3</a> that uses our CascableCore camera SDK. However, there just isn’t an existing bridge between Swift ABI and the C#/CLR one.</p>
<p>Well, maybe there’s a solution. Swift recently introduced C++ interoperability… maybe we could use that to bridge between the two worlds?</p>
<p><em>How hard could it be?</em></p>
<p>That little question, dear reader, led me down quite the rabbit hole. This blog post is a brave re-telling of that story, tactfully omitting the defeats and unashamedly embellishing the victories — just as any story worth its salt does.</p>
<p>If you already know what C++/CLI and a CLR is and don’t need my life story, you can hop straight over to the <a href="https://github.com/cascable/swift-on-windows-poc">SwiftToCLR proof-of-concept repository</a>. The readme there is still pretty long, but it’s a more technical document with the aim of getting folks more familiar with the technologies at hand to get stuck in.</p>
<p>Otherwise, stick around! It’s been a… journey. An exciting, fun, frustrating, tedious journey. However, I learned a lot, and hopefully you’ll enjoy coming along for the ride.</p>
<h3 id="what-are-we-trying-to-achieve">What Are We Trying To Achieve?</h3>
<p>My company has an SDK called <a href="https://developer.cascable.se/">CascableCore</a>, which talks to cameras from various manufacturers (such as Canon, Nikon, Sony, etc) via the network or USB. Its job is to deal with each camera’s particular protocols and oddities as it presents a unified set of APIs to apps that use the SDK. This SDK is used by <a href="https://cascable.se/">our own apps</a>, as well as those from a number of third-party developers.</p>
<p class="center"><img class="no-border" width="750" src="/pictures/swift-on-windows/cascablecore-examples.png"></p>
<p>There’s nothing particularly platform-specific about this task — networks and USB are cross-platform <em>by design</em> — so CascableCore is a great candidate to be a cross-platform codebase. It’d give us the option to expand our apps to more platforms in the future, as well as expand the potential customer base for the SDK itself.</p>
<p>CascableCore’s codebase currently looks like this — a bunch of Objective-C and some Swift. All new code is written in Swift, but still — there’s a hefty amount of Objective-C in there:</p>
<p class="center"><img width="328" src="/pictures/swift-on-windows/core-languages.png"></p>
<p>Despite its GNU roots, Objective-C isn’t particularly multi-platform in the real world, so no matter <em>what</em> we do we’ll be rewriting a significant amount of code to go multi-platform — and, <em>rationally</em> speaking, C++ is probably not a bad choice. We could do that RIGHT NOW.</p>
<p>However, dear reader, I’ll let you in on a little secret if you promise not to tell anyone. Lean closer. Ready?</p>
<p>…I hate C++.</p>
<p>Don’t tell anyone, OK?</p>
<p>My dislike of C++ is, if I’m honest, mostly irrational — I’ve just seen one horrendous <a href="https://en.cppreference.com/w/cpp/language/templates">C++ template</a> too many. But, we could just… not do that in our own code, y’know?</p>
<p>On the more rational side, though, we <em>are</em> a small company and our expertise <em>is</em> largely in Swift simply as a consequence of only having Mac and iOS apps at the moment. We’ve already dabbled in Swift on other platforms, too — <a href="https://photo-scout.app/">Photo Scout</a>’s backend is written in Swift/<a href="https://vapor.codes">Vapor</a> running on Linux servers, and it’s been a great success. Since most of CascableCore’s work is platform-agnostic, once the initial work is done we can (in theory) use our existing Swift expertise to maintain and improve CascableCore with only a relatively small additional cross-platform maintenance overhead.</p>
<p>And… since we’re being honest, it’s just plain <em>fun</em> to explore new technologies, especially in more esoteric ways. Even if we don’t end up shipping CascableCore in Swift on Windows, I learned a lot and (largely) had fun doing it. What’s the downside?</p>
<p>Anyway, I’d being keeping half an eye on the Swift on Windows story over the past few months/years until a few months ago <a href="https://social.lol/@biscuit/111426362823414489">this post on Mastodon</a> pulled on a thread in my brain:</p>
<p><iframe src="https://social.lol/@biscuit/111426362823414489/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="600" allowfullscreen="allowfullscreen"></iframe><script src="https://social.lol/embed.js" async="async"></script></p>
<p>This ended up being a perfect storm of circumstances:</p>
<ul>
<li>Swift on Windows seems to be decently viable now.</li>
<li>Swift had recently introduced the C++ interoperability feature, opening up possibilities for interacting with other languages.</li>
<li>I like to slow down a little and do interesting/”hack day” projects in December.</li>
<li>I <em>really</em> wanted a reason to justify getting a <a href="https://frame.work/">Framework laptop</a>.</li>
</ul>
<p>Not long later, my Framework laptop arrived and I was off to the races — a two-week timebox to explore this as I wind down for the Christmas break? Heck yeah.</p>
<p class="center"><img width="750" src="/pictures/swift-on-windows/laptop.jpg"> <br>
<em>I, er, went a little overboard on the unboxing photos.</em></p>
<h3 id="the-proof-of-concept-project">The Proof-of-Concept Project</h3>
<p>When putting together projects like this, it’s always nice to be able to use “real” code. Luckily, we have the <a href="https://github.com/Cascable/cascablecore-simulated-camera">CascableCore Simulated Camera</a> project, which is a CascableCore plugin that implements the API without needing a real camera to hand. This is a <em>perfect</em> candidate for this project — it’s implementing a real, shipping API without the need for us to figure out network or USB communication on Windows. It’s everything we need and nothing we don’t. Also, happily, it’s already all in Swift.</p>
<p>What <em>isn’t</em> in Swift, unfortunately, is the CascableCore API itself. It was introduced before Swift, and has remained a set of Objective-C headers to this day. We’ll need to redefine these in Swift. Oh, and port <a href="https://github.com/cascable/StopKit">StopKit</a>, which is an Objective-C dependency.</p>
<p>Finally, we need a little bit of glue. CascableCore “proper” has a central “camera discovery” object that implements USB and network discovery, along with interfacing with plugins such as the simulated camera. We’re not bringing that over to the Windows proof-of-concept, so we need something in its place so we can actually “discover” our simulated camera on Windows.</p>
<p>Getting all this into place took a few days — the simulated camera was <em>largely</em> fine other than needing to remove some Objective-C features (such as Key-Value Observing) and use of Apple-only APIs (such as CoreGraphics). Porting StopKit and rebuilding the Objective-C API protocols into Swift ones took a couple of days, and the glue at the end a day or so.</p>
<p>Let’s have a look at a little demo project on the Mac:</p>
<p class="center"><img class="no-border" src="/pictures/swift-on-windows/mac-demo.png"></p>
<p>This little app discovers and connects to a camera, shows the camera’s live view feed, shows some camera settings, and lets you change them. It’s a simple enough app, but implements a decent chunk of the CascableCore API: issuing camera commands, observing camera settings, and receiving a stream of live view images. If we can get this working on Windows, we can get <em>everything</em> working.</p>
<p>Let’s try to build this demo app on Windows!</p>
<h3 id="figuring-out-the-core-problem">Figuring Out The Core Problem</h3>
<p>The first step is to get the Swift code compiling on Windows, which was easy enough in our case (see above). The next is to instruct the Swift compiler to emit C++ headers for our targets:</p>
<pre><code class="language-swift"><span class="n">swiftSettings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">.</span><span class="n">interoperabilityMode</span><span class="p">(.</span><span class="n">Cxx</span><span class="p">),</span>
<span class="p">.</span><span class="n">unsafeFlags</span><span class="p">([</span><span class="s">"-emit-clang-header-path"</span><span class="p">,</span> <span class="s">".build/CascableCoreSimulatedCamera-Swift.h"</span><span class="p">])</span>
<span class="p">]</span></code></pre>
<p>I will note that the Swift Package Manager doesn’t officially support emitting C++ headers yet, hence the clunky unsafe build flag. This <em>has</em> been working fine for me, but the official way to do this is via another build system such as CMake.</p>
<p>At any rate, we now have a C++ header for calling into our Swift code! Now to Google “Calling C++ from C#” and… <em>ah</em>.</p>
<p>Telling the story of two days of Googling would be <em>exquisitely</em> boring, so I’ll skip ahead to why this is actually rather difficult after a quick foray into runtimes.</p>
<h3 id="a-rumble-of-runtimes">A Rumble of Runtimes</h3>
<p>A <strong>runtime</strong> can be thought of as a “support structure” for your code, providing functionality at runtime like memory management, thread management, error handling, and more. Swift, for instance, uses ARC (Automatic Reference Counting) for memory management, and the runtime is the thing that actually does the allocation, reference counting, and deallocation of objects.</p>
<p>C# runs in the <strong>CLR</strong> (<strong>C</strong>ommon <strong>L</strong>anguage <strong>R</strong>untime), which is a garbage-collected runtime that’s a lot more complex than the Swift one, providing additional things like just-in-time compiling.</p>
<p>The thing about a runtime - especially the more complex ones like the CLR - is that they need everything in the “bubble” they operate to conform to the same rules for everything to work correctly. The CLR’s garbage-collection works because all of the objects in there are laid out in a particular way and behave the same way. A random Swift object floating around inside the CLR wouldn’t be able to take part in garbage collection since the compiled Swift code has no knowledge of such a thing — and the converse is true, too: a random C# object floating around inside the Swift runtime wouldn’t be able to take part in ARC since it don’t have the ability to call the Swift runtime’s reference-counting methods.</p>
<p>There are two ways around this: exiting the bubble entirely and doing things manually, or “teaching” another language about your runtime.</p>
<p>Most runtimes <em>do</em> tend to have a way of “exiting” the bubble. C# calls this <code>unsafe</code> code, and Swift has a number of <code>withUnsafe…</code> methods. When in unsafe code, your memory management guarantees are gone (or exist in a very limited scope) and you, the programmer, are responsible for dealing with memory management yourself.</p>
<p>However, Swift’s C++ interop feature is pretty neat in that it actually, in a way, “teaches” C++ about Swift’s memory management. The Swift C++ interop header for the tiniest of tiny examples is what I describe as “5000 lines of chaos” - lots of imports and macros and templates that form a bridge from C++ into the Swift runtime, allowing you to use Swift objects directly in C++ while still taking part in ARC. Great!</p>
<p>The CLR <em>also</em> has a way of teaching C++ about the CLR’s memory management in the form of a special “dialect” of C++ called <a href="https://en.wikipedia.org/wiki/C%2B%2B/CLI">C++/CLI</a>. Great!</p>
<p>Well…</p>
<h3 id="why-this-is-actually-rather-difficult">Why This Is Actually Rather Difficult</h3>
<p>We’re finally getting down to the <em>core</em> of the problem here. Let’s lay out some facts, including a couple more that I discovered during that two days of excruciatingly boring Googling mentioned above:</p>
<ul>
<li>
<p>Swift’s C++ headers contain a lot of additional infrastructure that “teaches” C++ about Swift’s memory management.</p>
</li>
<li>
<p><strong>NEW FACT!</strong> Swift’s C++ headers have a <em>lot</em> of <code>Clang</code>-specific features in them, to the point where they require <code>Clang</code> to build against them.</p>
</li>
<li>
<p>C++/CLI is a special dialect of C++ containing additional infrastructure that “teaches” C++ about the CLR’s memory management.</p>
</li>
<li>
<p><strong>NEW FACT!</strong> C++/CLI can only be compiled by <code>MSVC</code>, the Microsoft Visual C++ compiler (or perhaps more accurately - <code>Clang</code> can’t compile C++/CLI).</p>
</li>
</ul>
<p>This is a little bit like those party games where everyone makes a statement about someone else and you have to combine everything to figure out who’s lying. If you haven’t managed that yet:</p>
<ul>
<li>
<p><code>MSVC</code> can’t compile the Swift C++ interop header.</p>
</li>
<li>
<p><code>Clang</code> can’t compile C++/CLI.</p>
</li>
<li>
<p>This means that we can’t create a C++/CLI wrapper from our Swift C++ interop header.</p>
</li>
</ul>
<p>Crap.</p>
<p>Luckily, <code>Clang</code>’s <em>compiled</em> output is <a href="https://clang.llvm.org/docs/MSVCCompatibility.html">(at least somewhat) ABI-compatible</a> with <code>MSVC</code>, so although <code>MSVC</code> can’t compile the Swift C++ interop header, it <em>can</em> link against the compiled output.</p>
<p>This, thankfully, opens a route through — we can make an <strong>additional</strong> wrapper layer, compiled with <code>Clang</code>, that wraps the generated Swift/C++ APIs in, er… I guess… “normal?” C++ that <code>MSVC</code> can deal with. The end-to-end chain would then be:</p>
<p class="center"><img class="no-border" width="600" src="/pictures/swift-on-windows/layer-chain.png"></p>
<p>While this is a chain of four steps, we thankfully “only” need two wrappers:</p>
<ul>
<li>
<p>We have our Swift code that’s compiled by <code>Clang</code>, giving us a compiled binary and a C++ header.</p>
</li>
<li>
<p><strong>Wrapper 1</strong>: Compiled by <code>Clang</code>, wraps the <code>Clang</code>-generated Swift C++ interop header with a “normal” C++ one that <code>MSVC</code> can understand. The wrapper implementation calls the API defined in the C++ interop header.</p>
</li>
<li>
<p><strong>Wrapper 2</strong>: Compiled by <code>MSVC</code>, wraps the “normal” C++ header with a C++/CLI one that gets us into the CLR, and therefore up to C#. The wrapper implementation calls the API defined in <strong>Wrapper 1</strong>.</p>
</li>
<li>
<p>We have our C# code, compiled by <code>MSVC</code>, running in the CLR. It calls the API defined in <strong>Wrapper 2</strong>.</p>
</li>
</ul>
<p>This isn’t actually that <em>difficult</em> - it’s just very <em>tedious</em>. Each link in the chain has its own types, and they need to be translated in both directions (i.e., a C# <code>string</code> needs to end up as a Swift <code>String</code> when calling a method, then a Swift <code>String</code> being returned needs to end up as a C# <code>string</code> on the way back).</p>
<p>A simple, manually-made test project ends up looking like this:</p>
<p class="center"><img class="no-border" src="/pictures/swift-on-windows/manual-poc.png"></p>
<p>It’s not pretty, but it works!</p>
<h3 id="making-this-not-suck">Making This Not Suck</h3>
<p>Manually building two wrapper layers is, well, kind of a pain. For CascableCore it’d actually <em>largely</em> be a one-off cost — the API is fairly mature and stable, and we try not to change it unless we have to. Still, not fun.</p>
<p>Our case is fairly rare, though. Having to adjust two wrapper layers for every change you make as you work on Swift code is annoying enough to make you give up and not bother, so what can we do to make this better?</p>
<p>If you study the snippets of code in the screenshot above, a fairly strong pattern emerges even from such a small example.</p>
<p>For each “level”, we need to:</p>
<ol>
<li>
<p>Make a class that holds a reference to an object from the level below,</p>
</li>
<li>For each method on that wrapped class, have a corresponding method in the wrapper that:
<ul>
<li>Takes appropriate parameters for the method being wrapped,</li>
<li>Translates them all into types appropriate for the level below,</li>
<li>Calls the wrapped method with the translated parameters,</li>
<li>If needed, translates the returned value into a type appropriate for the current level and returns it.</li>
</ul>
</li>
<li>…<a href="https://www.youtube.com/watch?v=rjY0xsoozs8">there’s no step three</a>!</li>
</ol>
<p>That’s <em>extremely</em> repetitive and well-defined work, and it’s a perfect candidate for…</p>
<p>…drumroll please…</p>
<p><strong>Automated code generation!</strong></p>
<h3 id="introducing-swifttoclr">Introducing SwiftToCLR</h3>
<p><code>SwiftToCLR</code> is the main “result” of this proof-of-concept project, and the thing that took by far the most amount of time and trouble. I’ll spare you the journey here, but if you’re interested in it there’s a more detailed discussion over on the <a href="https://github.com/cascable/swift-on-windows-poc">project’s GitHub repository</a>.</p>
<p>SwiftToCLR is a command-line tool, written in Swift, that takes your C++ interop header from Swift (as well as a couple of other bits and pieces) and generates the header and implementation for <em>both</em> wrapper layers discussed above. The example usage here is on Windows, but it does work on macOS too.</p>
<p><strong>Note:</strong> You may start to notice mentions of “unmanaged” and “managed” code here and there. This is a result of the project’s focus on the CLR — “managed code” is how the CLR refers to code running within the garbage-collected runtime, and “unmanaged code” is code running outside of that environment.</p>
<pre><code class="language-bash">C:<span class="se">\></span><span class="w"> </span>.<span class="se">\S</span>wiftToCLR.exe<span class="w"> </span>CascableCoreBasicAPI-Swift.h
<span class="w"> </span>--input-module<span class="w"> </span>CascableCoreBasicAPI
<span class="w"> </span>--cxx-interop<span class="w"> </span>.<span class="se">\s</span>wiftToCxx
<span class="w"> </span>--output-directory<span class="w"> </span>.
Using<span class="w"> </span>clang<span class="w"> </span>version:<span class="w"> </span>compnerd.org<span class="w"> </span>clang<span class="w"> </span>version<span class="w"> </span><span class="m">17</span>.0.6
Successfully<span class="w"> </span>wrote<span class="w"> </span>UnmanagedCascableCoreBasicAPI.hpp
Successfully<span class="w"> </span>wrote<span class="w"> </span>UnmanagedCascableCoreBasicAPI.cpp
Successfully<span class="w"> </span>wrote<span class="w"> </span>ManagedCascableCoreBasicAPI.hpp
Successfully<span class="w"> </span>wrote<span class="w"> </span>ManagedCascableCoreBasicAPI.cpp
C:<span class="se">\></span></code></pre>
<p>Since this was a timeboxed project, right now it <em>only</em> generates the source files (which can be compiled with Visual Studio by setting up a couple of simple targets). The most immediate and high-impact improvement to SwiftToCLR would be to extend it to actually build them too — just a single command to get compiled binaries to dump into your C# project would be <em>amazing</em>.</p>
<p>Let’s have a quick look at the layers here. Given the following Swift example:</p>
<pre><code class="language-swift"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">APIClass</span> <span class="p">{</span>
<span class="kd">public</span> <span class="kd">init</span><span class="p">()</span> <span class="p">{}</span>
<span class="kd">public</span> <span class="kd">var</span> <span class="nv">text</span><span class="p">:</span> <span class="nb">String</span> <span class="p">{</span> <span class="k">return</span> <span class="s">"API!"</span> <span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">sayHello</span><span class="p">(</span><span class="n">to</span> <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="p">-></span> <span class="nb">String</span> <span class="p">{</span>
<span class="k">return</span> <span class="s">"Hello from Swift, </span><span class="si">\(</span><span class="n">name</span><span class="si">)</span><span class="s">!"</span>
<span class="p">}</span>
<span class="kd">public</span> <span class="kd">func</span> <span class="nf">doOptionalWork</span><span class="p">(</span><span class="n">optionalString</span><span class="p">:</span> <span class="nb">String</span><span class="p">?)</span> <span class="p">-></span> <span class="nb">String</span><span class="p">?</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">optionalString</span> <span class="p">==</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="s">"I did some work"</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre>
<p>The Swift/C++ interop header will be over 5000 lines. Here’s an excerpt of our class’ definition in there:</p>
<pre><code class="language-c++"><span class="k">class</span><span class="w"> </span><span class="nc">SWIFT_SYMBOL</span><span class="p">(</span><span class="s">"s:9BasicTest8APIClassC"</span><span class="p">)</span><span class="w"> </span><span class="n">APIClass</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">_impl</span><span class="o">::</span><span class="n">RefCountedClass</span><span class="w"> </span><span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="w"> </span><span class="k">using</span><span class="w"> </span><span class="n">RefCountedClass</span><span class="o">::</span><span class="n">RefCountedClass</span><span class="p">;</span>
<span class="w"> </span><span class="k">using</span><span class="w"> </span><span class="n">RefCountedClass</span><span class="o">::</span><span class="k">operator</span><span class="o">=</span><span class="p">;</span>
<span class="w"> </span><span class="k">static</span><span class="w"> </span><span class="n">SWIFT_INLINE_THUNK</span><span class="w"> </span><span class="n">APIClass</span><span class="w"> </span><span class="n">init</span><span class="p">()</span><span class="w"> </span><span class="n">SWIFT_SYMBOL</span><span class="p">(</span><span class="s">"s:9BasicTest8APIClassCACycfc"</span><span class="p">);</span>
<span class="w"> </span><span class="n">SWIFT_INLINE_THUNK</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="w"> </span><span class="n">getText</span><span class="p">()</span><span class="w"> </span><span class="n">SWIFT_SYMBOL</span><span class="p">(</span><span class="s">"s:9BasicTest8APIClassC4textSSvp"</span><span class="p">);</span>
<span class="w"> </span><span class="n">SWIFT_INLINE_THUNK</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="w"> </span><span class="n">sayHello</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">&</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="n">SWIFT_SYMBOL</span><span class="p">(</span><span class="s">"s:9BasicTest8APIClassC8sayHello2toS2S_tF"</span><span class="p">);</span>
<span class="w"> </span><span class="n">SWIFT_INLINE_THUNK</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">></span><span class="w"> </span><span class="n">doOptionalWork</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">>&</span><span class="w"> </span><span class="n">optionalString</span><span class="p">)</span><span class="w"> </span><span class="n">SWIFT_SYMBOL</span><span class="p">(</span><span class="s">"s:9BasicTest8APIClassC14doOptionalWork2of14optionalStringSSSgAA0F4TypeO_AGtF"</span><span class="p">);</span>
<span class="w"> </span><span class="c1">// (Various internal and private definitions skipped)</span>
<span class="p">};</span></code></pre>
<p>Given this header, SwiftToCLR will output the following “normal” C++ wrapper:</p>
<pre><code class="language-c++"><span class="k">class</span><span class="w"> </span><span class="nc">APIClass</span><span class="w"> </span><span class="p">{</span>
<span class="k">public</span><span class="o">:</span>
<span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">shared_ptr</span><span class="o"><</span><span class="n">BasicTest</span><span class="o">::</span><span class="n">APIClass</span><span class="o">></span><span class="w"> </span><span class="n">swiftObj</span><span class="p">;</span>
<span class="w"> </span><span class="n">APIClass</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">shared_ptr</span><span class="o"><</span><span class="n">BasicTest</span><span class="o">::</span><span class="n">APIClass</span><span class="o">></span><span class="w"> </span><span class="n">swiftObj</span><span class="p">);</span>
<span class="w"> </span><span class="n">APIClass</span><span class="p">();</span>
<span class="w"> </span><span class="o">~</span><span class="n">APIClass</span><span class="p">();</span>
<span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="w"> </span><span class="nf">getText</span><span class="p">();</span>
<span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="w"> </span><span class="nf">sayHello</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&</span><span class="w"> </span><span class="n">name</span><span class="p">);</span>
<span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">></span><span class="w"> </span><span class="n">doOptionalWork</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">>&</span><span class="w"> </span><span class="n">optionalString</span><span class="p">);</span>
<span class="p">};</span></code></pre>
<p>…and the following C++/CLI wrapper:</p>
<pre><code class="language-c++"><span class="k">public</span><span class="w"> </span><span class="n">ref</span><span class="w"> </span><span class="k">class</span><span class="w"> </span><span class="nc">APIClass</span><span class="w"> </span><span class="p">{</span>
<span class="nl">internal</span><span class="p">:</span>
<span class="w"> </span><span class="n">UnmanagedBasicTest</span><span class="o">::</span><span class="n">APIClass</span><span class="w"> </span><span class="o">*</span><span class="n">wrappedObj</span><span class="p">;</span>
<span class="w"> </span><span class="n">APIClass</span><span class="p">(</span><span class="n">UnmanagedBasicTest</span><span class="o">::</span><span class="n">APIClass</span><span class="w"> </span><span class="o">*</span><span class="n">objectToTakeOwnershipOf</span><span class="p">);</span>
<span class="k">public</span><span class="o">:</span>
<span class="w"> </span><span class="n">APIClass</span><span class="p">();</span>
<span class="w"> </span><span class="o">~</span><span class="n">APIClass</span><span class="p">();</span>
<span class="w"> </span><span class="n">System</span><span class="o">::</span><span class="n">String</span><span class="o">^</span><span class="w"> </span><span class="n">getText</span><span class="p">();</span>
<span class="w"> </span><span class="n">System</span><span class="o">::</span><span class="n">String</span><span class="o">^</span><span class="w"> </span><span class="n">sayHello</span><span class="p">(</span><span class="n">System</span><span class="o">::</span><span class="n">String</span><span class="o">^</span><span class="w"> </span><span class="n">name</span><span class="p">);</span>
<span class="w"> </span><span class="n">System</span><span class="o">::</span><span class="n">String</span><span class="o">^</span><span class="w"> </span><span class="n">doOptionalWork</span><span class="p">(</span><span class="n">System</span><span class="o">::</span><span class="n">String</span><span class="o">^</span><span class="w"> </span><span class="n">optionalString</span><span class="p">);</span>
<span class="p">};</span></code></pre>
<p>I won’t paste the entire implementation here, but here’s an example from the “normal” layer in which we’re translating optional strings in both directions. The code is <em>particularly</em> verbose here, but given it’s autogenerated code that is unlikely to ever be looked at, I think that’s alright.</p>
<pre><code class="language-c++"><span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">></span><span class="w"> </span><span class="n">UnmanagedBasicTest</span><span class="o">::</span><span class="n">APIClass</span><span class="o">::</span><span class="n">doOptionalWork</span><span class="p">(</span><span class="k">const</span><span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">></span><span class="w"> </span><span class="o">&</span><span class="w"> </span><span class="n">optionalString</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">></span><span class="w"> </span><span class="n">arg0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">optionalString</span><span class="p">.</span><span class="n">has_value</span><span class="p">()</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">>::</span><span class="n">init</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="p">)</span><span class="n">optionalString</span><span class="p">)</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">>::</span><span class="n">none</span><span class="p">());</span>
<span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">Optional</span><span class="o"><</span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="o">></span><span class="w"> </span><span class="n">swiftResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">swiftObj</span><span class="o">-></span><span class="n">doOptionalWork</span><span class="p">(</span><span class="n">arg0</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">swiftResult</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">swift</span><span class="o">::</span><span class="n">String</span><span class="w"> </span><span class="n">unwrapped</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">swiftResult</span><span class="p">.</span><span class="n">get</span><span class="p">();</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o"><</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">></span><span class="p">((</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">)</span><span class="n">unwrapped</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">std</span><span class="o">::</span><span class="n">nullopt</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span></code></pre>
<p>So… great, right?! Let’s go! Wait… <em>more</em> roadblocks?</p>
<h3 id="limitations-of-swifts-c-interop">Limitations of Swift’s C++ Interop</h3>
<p>The keen-eyed amongst you may have noticed that in my usage example above, I was giving <code>SwiftToCLR</code> a header file called <code>CascableCoreBasicAPI-Swift.h</code>. Why a “basic” API?</p>
<p>Swift’s C++ interop feature is still pretty young, and has a number of limitations that directly impact our CascableCore API. There’s a deeper discussion in the readme on the <a href="https://github.com/cascable/swift-on-windows-poc">project’s GitHub repository</a>, but the three that impact us the most are:</p>
<ul>
<li>
<p>Protocols aren’t exposed through C++. CascableCore’s API is almost <em>entirely</em> defined in protocols.</p>
</li>
<li>
<p>Swift’s <code>Data</code> type isn’t exposed through C++. We use <code>Data</code> to hand image data over to client apps, including frames of the live view stream.</p>
</li>
<li>
<p>Swift closures aren’t exposed through C++. This is a <em>huge</em> one - CascableCore’s API uses callbacks extensively since working with cameras is intrinsically asynchronous. They’re used to observe changes to camera settings, receive frames of the live view stream, find out if a sent command was successful, and more.</p>
</li>
</ul>
<p>So, what to do? All of these problems do have workarounds, with the closure limitation being particularly gnarly to combat. After a bit of pondering, I decided that they were outside of the scope of this project (especially considering the timebox I had). This is a long-term endeavour, and hopefully Swift’s C++ interop featureset will improve over time.</p>
<p>Instead, I built the “CascableCore Basic API”, which is a simplified API that wraps the “full” one (this project is <em>full</em> of wrappers, crikey):</p>
<ul>
<li>
<p>Objects are defined as classes rather than protocols.</p>
</li>
<li>
<p><code>Data</code> objects in Swift are exposed as “unsafe” methods to copy the data to a pointer via <code>Data</code>’s <code>copyBytes(to:count:)</code> method.</p>
</li>
<li>
<p>There are no callbacks/closures. To find changes, you need to poll (<em>boooo!</em>).</p>
</li>
</ul>
<p>It’s clunky, but it works!</p>
<h3 id="holy-crap-are-we-finally-writing-c">Holy Crap Are We Finally Writing C#?</h3>
<p>I have to admit, there were times where I thought I’d have to abandon this project. A month into my two week timebox, every corner I turned brought up a new problem. Some clear and understandable (“Oh wait, optionals!”), others less so (“Why does this code run fine in a <code>swift test</code> but crash when called from C#?”).</p>
<p>However, one day everything finally “clicked” and suddenly this demo app was coming together <em>fast</em>. Holy crap, it works!!</p>
<p class="center"><img width="750" src="/pictures/swift-on-windows/windows-app.png"></p>
<p>I tried to write the demo app as I <em>should</em>, so I abstracted away the polling (<em>boooo!</em>) with a couple of classes — <code>PollingAwaiter</code> and <code>PollingObserver</code> — that vend events for the app to observe as if the polling limitation wasn’t present.</p>
<p>Otherwise, the Windows demo app is pretty bog-standard, which is exactly what I hoped the outcome would be. It’s written in C# using XAML and WinUI 3 for the UI, and the whole thing is a standard Visual Studio app project. There’s nothing special about it at <em>all</em>, other than having to link to Swift.</p>
<p>Hiding under this boringness are a <em>trove</em> of unanswered technical questions. Again, these are discussed more in the <a href="https://github.com/cascable/swift-on-windows-poc">project’s GitHub repository</a>, but some of the larger ones:</p>
<ul>
<li>
<p>Why do we get <em>very</em> weird crashes when our Swift code is built for static linking? (<strong>Sidebar:</strong> You really <em>must</em> explicitly mark your targets as <code>.dynamic</code> in your package manifest to get SPM to build dynamic binaries (i.e., <code>.dll</code> files), otherwise you’ll lose days to chaos as I did.)</p>
</li>
<li>
<p>How do we best solve the problem of the lack of closures?</p>
</li>
<li>
<p>What’s the real-world performance impact of translating every parameter through two wrapper layers? <code>System::String</code> → <code>std::string</code> → <code>swift::String</code> and back is hardly ideal — especially when arrays get involved — and I didn’t have to time to run meaningful performance measurements.</p>
</li>
<li>
<p>When run in this context (i.e., a C# app managing the process’ lifecycle), Swift code doesn’t get a working main dispatch queue (or runloop, or…). This is largely expected (<code>dispatch_get_main_queue()</code> has some <a href="https://developer.apple.com/documentation/dispatch/1452921-dispatch_get_main_queue">relevant notes in its documentation</a>), but it’d be <em>very</em> useful to be able to sync the C# app’s UI thread with the main dispatch queue.</p>
</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>So, what became of this experiment? Well, I <em>did</em> manage to build the same app on macOS and Windows with the same underlying Swift codebase, which I’m incredibly happy about!</p>
<p class="center"><img src="/pictures/swift-on-windows/side-by-side.png"></p>
<p>I’ve learned a ton, and I feel like I now have a reasonably well-informed opinion of Swift on Windows (which was the primary “business” goal of this project, I suppose).</p>
<p>Swift is undoubtedly an “Apple platforms-first” language, particularly the tooling. Like with Swift on Linux, we get a second-class Foundation (although that’s actively being worked on <em><a href="https://github.com/apple/swift-foundation">right now</a></em>). The <a href="https://www.swift.org/blog/vscode-extension/">Swift plugin for Visual Studio Code</a> works on Windows and is pretty great, if it wasn’t for the fact that no matter what I try, <code>sourcekit-lsp.exe</code> <em>continuously</em> spins at 100% CPU usage unless I disable code completion. Building our project with SPM’s default configuration gives a ton of <code>.o</code> files to manually assemble, only to get inscrutable crashes deep in the runtime (explicitly flagging everything to be a <code>.dynamic</code> library fixes both of these).</p>
<p>On <em>all</em> platforms, the Swift/C++ interop feature set is extremely limited — the lack of closures in particular is a particularly big one. That polling workaround I implemented <em>will not</em> make it to production.</p>
<p>However.</p>
<p>None of that changes the fact that once I’d overcome these hurdles, I was able to take a Swift codebase that can be compiled for iOS, macOS, and Windows and build a meaningful demo project in C# on top of it in just a couple of days. Once it’s up-and-running, it’s <em>amazing</em>.</p>
<p>We don’t be dropping everything to build Windows versions of CascableCore and our apps just yet — we have a <em>lot</em> of other work on our plate. However, my experience was <em>very</em> confidence-inspiring, and I can <em>genuinely</em> see a path to shipping real products to real users using a cross-platform CascableCore and this hybrid C#/Swift approach.</p>
<p>I’m also <em>very</em> excited about the future of Swift on Windows, and will be staying up-to-date with what’s going on. There’s also a number of meaningful improvements that can be made to <code>SwiftToCLR</code> right now, and hopefully I’ll be able to chip away at those as time goes on. If this project can push things in a positive direction even <em>slightly</em>, I’ll consider that a huge bonus.</p>
<p>If you find this project interesting, please do head over the <a href="https://github.com/cascable/swift-on-windows-poc">the GitHub repository</a> and take a look. The readme there goes a lot more in-depth to the technical details of this thing, and contains instructions for compiling and diving into the code yourself — everything mentioned above is open-source.</p>
<p>As always, I’m <a href="https://mastodon.social/@iKenndac">@iKenndac on Mastodon</a> and am happy to chat there (although please do note my policy of ignoring unsolicited private mentions — talk to me in public!) about this — especially if you’re experienced with any of the approaches taken here. I’d love to hear your feedback!</p>
<h3 id="special-thanks">Special Thanks</h3>
<p>I’d like to thank a couple of folks who’ve been particularly inspiring and helpful for this project. They’ve helped me navigate a tricky and unbeaten path, for which I’m very grateful:</p>
<ul>
<li>
<p><strong><a href="https://social.lol/@biscuit">Michael Thomas</a></strong>: This whole thing started when I saw a <a href="https://social.lol/@biscuit/111426362823414489">post of his on Mastodon</a> that pulled a thread in my mind that cost me a new laptop and over a month of my life. I <a href="https://mastodon.social/@ikenndac/111569362462083741">do love the laptop</a>, though, and this project has been a ton of fun.</p>
</li>
<li>
<p><strong><a href="https://www.foureyes.me/">Brian Michel</a></strong> works at The Browser Company, and is part of a team building a whole web browser in Swift on Windows! Their approach is different to this one, but equally as interesting. You can see some <a href="https://github.com/thebrowsercompany/windows-samples">examples of their work</a> on GitHub.</p>
</li>
</ul>
tag:ikennd.ac,2023-09-05:/blog/2023/09/swift-on-the-server-talk-additional-reading/Server-Side Swift For Small Startup Success: Additional Reading2023-09-05T13:00:00Z2023-09-05T13:00:00Z<p>This year at <a href="https://www.iosdevuk.com">iOSDevUK</a>, I gave a talk on using Swift on the Server with Vapor to build an app’s backend in Swift.</p>
<p>You can download the slides <a href="/pictures/ServerSideSwiftServicesForSmallStartupSuccess.pdf">here</a>.</p>
<p>This post contains some links to additional resources.</p>
<h3 id="the-basics">The Basics</h3>
<ul>
<li>
<p><a href="https://vapor.codes">Vapor</a> is the Swift framework used in the projects mentioned.</p>
</li>
<li>
<p><a href="https://photo-scout.app">Photo Scout</a> is the app used in most of the examples.</p>
</li>
</ul>
<h3 id="technical">Technical</h3>
<ul>
<li>
<p>When deploying a Swift/Vapor app to Heroku, you’ll need to use the <a href="https://github.com/vapor-community/heroku-buildpack">Vapor Heroku Buildpack</a>.</p>
</li>
<li>
<p>Vapor is built upon <a href="https://github.com/apple/swift-nio">SwiftNIO</a>.</p>
</li>
<li>
<p>We’re all very much looking forward to the unified Foundation project coming to fruition. More details <a href="https://www.swift.org/blog/future-of-foundation/">here</a>, with the GitHub repo <a href="https://github.com/apple/swift-foundation">here</a>.</p>
</li>
<li>
<p>For sending metrics to <a href="">Datadog</a> from your project, you’ll need the <a href="https://github.com/DataDog/swift-dogstatsd">dogstatsd SPM package</a>, and for Heroku the <a href="https://docs.datadoghq.com/agent/basic_agent_usage/heroku/">Datadog Heroku buildpack</a> to get the required local agent.</p>
</li>
</ul>
tag:ikennd.ac,2023-02-09:/blog/2023/02/introducing-photo-scout/Introducing Photo Scout!2023-02-09T15:00:00Z2023-02-09T15:00:00Z<p><img class="no-border" style="border-radius: 36px; box-shadow: 10px 10px 20px #ccc;" width="200" src="/pictures/photo-scout-icon.png"></p>
<p>I’m really excited to announce <strong>Photo Scout</strong> to the world! It’s going into a prerelease TestFlight period starting from today, with a public release sometime in spring or early summer.</p>
<p>The tagline of Photo Scout is “You tell us where. We tell you when.” It’s an app for anyone that likes to take photos — give it a set of criteria, and it’ll tell you (with push notifications, if you want) when you can take that photo. It goes beyond just weather and golden hours — you can place the sun in a particular place in the sky, match against phases of the moon, and more (with more coming). There’s some really amazing creative potential!</p>
<p class="center"><img class="no-border" width="600" src="/pictures/photo-scout-tf1-screenshots.png"></p>
<p>Actually, rather than trying to list out <em>what</em> it can do, why don’t I tell you <em>why</em>:</p>
<p align="center">
<video style="border: 1px solid #999; border-radius: 20px;" width="400" height="400" controls="">
<source src="/pictures/photo-scout-testflight-intro.mp4" type="video/mp4"></source>
Your browser does not support the video tag.
</video>
</p>
<p>You can find out more about the app and sign up to be notified when you can join the TestFlight over on the <a href="https://photo-scout.app/">Photo Scout website</a>. You can also follow along with development on the <a href="https://indieapps.space/@photoscout">app’s Mastodon account</a> or on <a href="https://mastodon.social/@ikenndac">my personal Mastodon account</a>. The TestFlight will stay fairly small for the first week or two to make sure the servers don’t fall over, but if you ask nicely on Mastodon you may well get in early too!</p>
<h3 id="how-photo-scout-came-to-be">How Photo Scout Came To Be</h3>
<p>The last time I released a <em>completely</em> new app was <a href="https://cascable.se/ios/">Cascable</a> back in 2015. The first commit into that project was nearly ten years ago! There’ve been other apps along the way — notably <a href="https://cascable.se/pro-webcam/">Pro Webcam</a> — but they’ve all been built around that core technology stack of working with DSLR/mirrorless cameras.</p>
<p>I have a note on my computer full of random feature ideas for Cascable that’ve been gathered over the years. Some of them are sensible, some of them are ridiculous, and some of them are good ideas but not for that app. One of them has been there for a <em>long</em> time, and has always stuck with me:</p>
<blockquote>
<p>It’d be cool if the app could notify me when I could take a picture of the milky way</p>
</blockquote>
<p>I really liked the idea, but it wasn’t the right fit for an app for remote controlling and transferring images from a camera — so in the note it stayed.</p>
<p>In 2020-2021 or so, a few desires coalesced:</p>
<ul>
<li>
<p>The desire to learn something new.</p>
</li>
<li>
<p>The desire to expand Cascable’s target market with an app that doesn’t need an expensive external camera to use.</p>
</li>
<li>
<p>The desire to start growing the size of Cascable (the company).</p>
</li>
</ul>
<p>That idea met all of those desires, especially since I actively <em>wanted</em> such an app… then, what started as the odd “Hey, what do you think about an app that…” conversation with friends slowly gained momentum through UI mockups, market research, an engineering prototype, then finally a point of no return — it was time to invest serious time and money into giving this a go!</p>
<h3 id="what-next">What Next?</h3>
<p>The plan is as follows:</p>
<ul>
<li>
<p>A smaller TestFlight phase starting from today to make sure the app’s servers don’t fall over with more than a couple of users.</p>
</li>
<li>
<p>Then, over the coming weeks, increase the TestFlight size and add features and polish for a public release sometime in spring or early summer.</p>
</li>
</ul>
<p>Everything about this project is built using knowledge brand new to me. It’s almost entirely SwiftUI, which is new for me. I’ve approached the app in a completely new, design-first way, which is new for me. It has a backend written in Swift with <a href="https://vapor.codes">Vapor</a>, both of which are new for me. It has AR components with some custom 3D programming, which is… well, you get the picture.</p>
<p>I’ve learned a lot — at times it felt like being at university again! — and there’s a lot about Photo Scout that I’m <em>really</em> pleased with (it has a theme song?!). Over the coming weeks as the TestFlight progresses and opens up to more people, I’ll be writing some articles on here about some of the things I thought turned out really well, and some things that were more challenging.</p>
<hr>
<p>So! If Photo Scout looks interesting to you, do <a href="https://photo-scout.app/">take a look at the site</a> and sign up if you want to take it for a spin, and get in touch on the <a href="https://indieapps.space/@photoscout">app’s Mastodon account</a> or on <a href="https://mastodon.social/@ikenndac">my personal Mastodon account</a> if you’re interested in this earlier “Oh God the servers are on fire” phase.</p>
tag:ikennd.ac,2023-01-13:/blog/2023/01/identity-crisis/Identity Crisis2023-01-13T19:00:00Z2023-01-13T19:00:00Z<h3 id="prologue">Prologue</h3>
<p>This post is less of a “blog post” and more of… I dunno, a chapter of a memoir (were I important enough to have such a thing). It was originally written over several weeks in the latter months of 2022 as a way to unjumble the last few years of my life and to have it down <em>somewhere</em>, at least — one of the largest regrets I have of my Dad passing (other than the fact that he, er, died in the first place) was that he died before I was old enough for him to share the stories of his life with me. Every time I hear a snippet about my Dad from someone who knew him before I was born — “After he fled Cuba during the Revolution, he–” <em>Excuse me?!</em> — it’s kinda wild. So, boring as my life is so far compared to parts of his, I have a desire to <a href="/blog/2013/02/my-life-in-pictures/">document it</a> so future people who care about me won’t have the same sorrow.</p>
<p>This <em>was</em> going to stay in scruffily-scrawled fountain pen ink shoved into a drawer until some poor soul has the task of clearing out all of my crap when <em>I’m</em> gone, but slowly the idea of putting it up here has become less awful over time. Nobody likes to share their low points (much less this widely), but people I respect tell me there’s strength in failure, and I’d like to draw a nice, clean line under this whole affair so I can focus on the next thing.</p>
<p>So, below you’ll find 5,000 words or so — or, on the audioblog, 36 minutes or so — about the past four years of my life as <del>an indie developer</del> a small business owner. Enjoy!</p>
<hr>
<p>If we take a look at my career so far, we can see two things. First — somehow — I’ve been a professional developer for over seventeen years now, which is kind of incredible. The second is that out of those seventeen years, only four and a half of them were actually at a “regular” job.</p>
<p class="center"><img width="640" src="/pictures/identity-crisis/career@2x.png"></p>
<p>For the rest of the time, I’ve made my way through life identifying as an “indie developer”, despite the fact that neither KennettNet (my first company, 2005–2012) or Cascable (my second company, 2015–) were ever really one-man enterprises. However, they were small companies for which the bulk of the development work was done by me (although even <em>that</em> isn’t true for some significant time periods). Still, in the early days of KennettNet, I struck lucky and wrote an app that sold well with very little non-development (read: marketing) work. I wrote the app, signed up for a payment provider, listed it on <a href="https://en.wikipedia.org/wiki/VersionTracker">VersionTracker</a> and away I went — truly an “indie developer”.</p>
<p class="center"><img src="/pictures/secret-diary/kennettnet.jpg"> <br>
<em>It’s not official until you have a sign.</em></p>
<h3 id="section">2011–2015</h3>
<p>When KennettNet failed — a long story for another day — I got a job at Spotify and, for the most part, enjoyed my time there. I worked on fun challenges and shipped things I was (and still am) proud of, but grew increasingly frustrated that my career progression there seemed to be circling into a funnel towards management. I firmly hold the belief a good developer should be able to progress through their career entirely doing development work if they want to — effectively becoming an artisan of their craft, as dated as that may sound. At Spotify, I never wanted to play the game of checking the progression boxes they wanted everyone to check to progress through the system. “I’m a developer — just let me be good at my job!”, I’d bluster. Thanks to being afforded the chance to work on some impactful projects I <em>did</em> manage to make salary and career progress based off the back of my work, but it was always a struggle without my nicely-checked boxes.</p>
<p>As a “car guy”, one of the more fantastical weekends of my time at Spotify was being flown out to San Francisco with a friend-slash-colleague for a hackathon at TechCrunch Disrupt. Over a very blurry twenty-four hours, we mashed together the Spotify app with Ford’s then-fledgling Sync AppLink platform to create a <a href="https://www.cnet.com/roadshow/news/ford-kicks-off-hackathon-contest-with-spotify-integration/">tech demo of Spotify in a car</a>. I’d somehow managed to completely miss that TechCrunch Disrupt was somewhat of a Big Deal™, so I sauntered onto the stage and somehow managed to give a successful live tech demo with speech recognition before we headed to the airport and slept the entire flight home.</p>
<table class="alt">
<tr><td style="padding: 20px;">
<strong>Side anecdote:</strong> The plan was for my friend and I to give the demo together, but he ended up not doing it due (if I recall correctly) to nerves and/or tiredness. Since I had no idea of Disrupt's significance, I was just "Sure whatever it's just a tech demo, who cares" and did it on my own. My friend was rather upset that I forgot to mention his name on stage — I promise it wasn't on purpose, I was <em>very</em> tired and had no idea of the significance of that particular stage.</td></tr>
</table>
<p>Back at Spotify, folks were pleased with the demo and I wanted to build car integrations more and more. Of <em>course</em> Spotify should be in every car! I pestered the people I could pester, and always got the same answers anyone working in a large corporation has heard a thousand times — lots of empty words surrounding the core underlying ones of “budget” and “priorities”.</p>
<hr>
<p>Almost as soon as I’d fixed the financial shitstorm that the failure of KennettNet caused, I started getting the “indie itch” again and started to plan an unpaid sabbatical to give it a bash. After yet another rebuttal on doing car integrations, I signed the paperwork — I was going to be indie again!</p>
<p>The next day, a higher-up who had once been my direct manager ran over to my desk.</p>
<p>“What’s this I hear about your leave? I thought you wanted to do car stuff?”<br>
I explained that I’d heard the word “priorities” one too many times.<br>
“Sign this.” An Apple NDA.</p>
<p>A few months later, <a href="https://twitter.com/iKenndac/status/440460725824552960">Apple announced</a> that Spotify would be one of the first third-party apps to have CarPlay integration, and we shipped it <a href="https://twitter.com/iKenndac/status/517615458514911232">later that year</a>. In the meantime, a car integrations team had been started at Spotify (which for a little while was literally just me and a product owner). I worked on lots of interesting things, and got to travel to and work directly with engineers from a number of car manufacturers. It was an absolute blast.</p>
<p>Unfortunately, my unwillingness to play the career progress game came back to bite me eventually. I put my heart and soul into the projects I worked on, working really hard to make them be the best I could. That worked for a while — being the “passionate engineer” meant that my ability to produce results and largely be left to “get on with it” counterbalanced things like my less than professional reaction to learning a project had been canned the Monday after I lost an entire weekend to making sure it’d pass certification on time — but in the end I wasn’t going anywhere without that “Give three or more presentations at employer branding events” checkmark on my progression sheet.</p>
<p>All this time, the “indie itch” never went away. As I saw much greener engineers get promoted ahead of me because they were better at playing the game, it became strong enough that I dug out my abandoned sabbatical paperwork, resubmitted it, and tried again. Finally, I was free to be an indie developer again. To <strong>develop</strong>. No more bureaucracy getting in the way of being a great <strong>developer</strong>. Hell. Yes.</p>
<p class="center"><img src="/pictures/identity-crisis/crap.jpg"> <br>
<em>Leaving Spotify with a box of crap from my desk.</em></p>
<h3 id="section-1">2019</h3>
<p>The first version of the Cascable app was <a href="https://ikennd.ac/blog/2015/06/secret-diary-of-a-side-project-part-6/">released in 2015</a>, and had been trundling along as I developed features and <a href="https://ikennd.ac/blog/2016/10/launching-cascable-2/">experimented with business models</a> in an effort to increase revenue — which slowly but surely climbed as time went on. Every major update I vowed to allocate more time to marketing tasks, and every major update it got pushed aside for more development work and polish. It wasn’t perfect, but the app’s sales combined with some part-time client work here and there made ends meet nicely.</p>
<p>This continued until early 2019, when SanDisk approached the company to add support for one of their hardware accessories to the app. I was thrilled to be approached by such a well-known brand (and the marketing opportunities that’d bring), but adding the support would mean a big rebuild of the app’s photo management features. It needed doing <em>anyway</em>, though, and this rebuild ended up being the tentpole of the next big update — Cascable 4.0!</p>
<p>I was <em>convinced</em> this would be the big one. The new photo management feature was leagues ahead of the old one, and things were turning out <em>great</em>. Unfortunately, rendering grids of images turned out to be a lot more complex than I’d expected — and then, I got the golden email. I was going to WWDC 2019! What a perfect deadline.</p>
<p>After some discussion with my wife, I went all in… and it was brutal. Twelve-hour days, seven days per week through March, April, and May. But at least it’d be temporary. My wife took over all of my household tasks and I brought my work computer home to cut out the commute — I’d roll out of bed, sit at the computer for twelve hours, then roll back into bed to sleep. But at least it’d be temporary. Personal care and grooming went to hell (although hygiene thankfully survived — I was scruffy but <em>clean</em>), as did the perception of time.</p>
<p class="center"><img src="/pictures/identity-crisis/profile-pictures.jpg"> <br>
<em>My “regular” profile picture against one taken in May 2020. It’s remarkable what good lighting and a smile can hide — but the clues are certainly there.</em></p>
<p>It nearly killed me, but I <a href="https://cascable.se/blog/cascable-4-released/">shipped it</a>. Thank <em>Christ</em> it was temporary! I was <em>so</em> proud of the release — it was some of my best work to date, and with a SanDisk partnership to boot. I shipped the app, then flew off to San Jose for WWDC for a wonderful time. Being an indie developer is <em>great!</em></p>
<hr>
<p>Unfortunately, the worst possible thing happened — absolutely nothing. Nobody gave a shit. Apart from some press coverage focused on the SanDisk integration, Cascable 4.0 had the worst launch in the history of the app. Nobody cared, and sales didn’t budge. My best development work to date — in my whole career — and nobody cared.</p>
<p>This would do bad things to someone in a <em>good</em> state of mind, but after months of soul-crushing and unhealthy levels of work <em>fuelled</em> by the promise of an uplift? It was nearly a death blow. Burnout hit <strong>hard</strong>, and I could barely even bring myself to <em>think</em> about the app, let alone work on it. I did what I always do in times of trauma — I withdrew into myself. Everywhere you look — my blog, my social media, the release notes of the app — you’ll see a sharp falloff from mid-2019 or so.</p>
<h3 id="spring-2020">Spring 2020</h3>
<p>I started to recover from the burnout in early 2020, with client work supporting the business as I regrouped and started to think about the future again. Although the 4.0 release had been catastrophic, I was fortunate that the status quo remained — app sales alone didn’t support the company, but they were healthy enough (and not <em>decreasing</em>) that part-time client work continued to fill the shortfall. Despite the setback, the roof over my head wasn’t in danger and the company had client work and an internal roadmap in place that’d take me well into the summer.</p>
<p>The COVID-19 pandemic delivered the one-two punch of the bottom falling out of the event photography industry (and thus app sales), and the bottom falling out of our major client’s industry (and thus client income). My recovery collapsed, and I was pretty certain that my indie career was absolutely done for. Again. At least I’d have a pandemic to blame for it this time.</p>
<p>Out of sheer desperation, I managed to pull a completely new app out of nowhere and get it to market — and, crucially, revenue — in no time flat. That app was <a href="https://cascable.se/pro-webcam/">Cascable Pro Webcam</a>, an app that lets you use a ‘real’ camera as a webcam. Its existence is very much in response to the slew of people working from home for the first time, and the increased demand for (and subsequent shortage of) webcams. I was worried that it was a cash grab at first, but the app turned out great — it was fun to write a Mac app again — and sold well. Let’s call it a “rapid reaction to a turbulent market”, then. At any rate, it (and a COVID relief grant from the government) absolutely saved the company from folding. It even got <a href="https://techcrunch.com/2020/06/10/how-to-get-your-nice-camera-set-up-as-a-high-quality-webcam/">covered by TechCrunch</a>!</p>
<p>Able to breathe once more, it was clear that part of me hadn’t made it through the panic unscathed. I just couldn’t do it any more — something had to change, and my health was plummeting. The most troubling thing of all, though, was that I couldn’t quite put my finger on <em>what</em> I couldn’t do any more or exactly what it was that needed changing. Cascable had being going on for five years at this point, and other than a brief moment in 2018 where it got <em>far</em> too close to the end of its runway, the combination of app sales and client work had always kept it healthy. Even the panic that produced Pro Webcam showed that I could fight and adapt if needed. With the additional revenue stream of that app, the company was even <em>more</em> resilient. So what’s the problem?</p>
<h3 id="summer-2020">Summer 2020</h3>
<p>This ate at me until one day in late summer, I found myself packing a month’s work of clothing, technology, and HomePods into a car some complain isn’t suitable for a long weekend, saying goodbye to an outwardly supportive wife with unmistakeable fear in her eyes, and heading deep into the mountains of southern France. I tend to — especially when it comes to extreme decision-making — be better at solving problems from the outside, and you can’t get much more ‘outside’ than a three-day drive to a month-long AirBnB rental promising no concerns other than keeping the <a href="https://en.wikipedia.org/wiki/Pain_au_chocolat">pain au chocolat</a> consumption under control. With the day-to-day running of the company out of my mind, the hope was to be free enough to figure out what the problem was, and what I could do to fix it.</p>
<p class="center"><img src="/pictures/identity-crisis/boot.jpg"> <br>
<em>When luggage space is at a premium, the HomePod still makes the cut.</em></p>
<p>As days of European motorways slid past the window, trepidation blossomed into terror. What if the only way to save my health — mental or otherwise — was to shut down Cascable and move on to something else? Could I <em>ever</em> recover from burnout so severe that I’m fleeing to the opposite end of the continent to try to even <em>understand</em> it? Another episode of the <a href="https://www.iheart.com/podcast/1119-fake-doctors-real-friends-60367049/">Scrubs podcast</a> would drown that out, at least for the time being.</p>
<p>As motorways gave way to mountain passes, my soul started to calm a little.</p>
<hr>
<p>Guillestre is one of my favourite places in the world. It’s a small village nestled in a valley in the Alps, surrounded by peaks on all sides — although there’s nothing particularly special about it. There’s not much to do, and there are more spectacular views to be found. However, I know it well enough to get around and know some of its nooks and crannies, and the sleepy village pace of life forces you to slow down. It’s very calming.</p>
<p class="center"><img src="/pictures/identity-crisis/guillestre.jpg"> <br>
<em>“Nothing particularly special”.</em></p>
<p>Once settled in, I started a daily-ish journal to try and get my head in order, first trying to reconcile reality with my state of mind. It wasn’t lost on me that I’d jumped into my two-seater sports car and pissed off to the south of France for a month to try and “figure things out” — I privilege I’m very lucky to be afforded. This, of course, just made me feel worse. My days would swing wildly — I’d be joyful and proud of my achievements one day, then come crashing down the next, admonishing myself for my poor mental and physical health. “It’s a wonder you still have a wife that can bear to look at you,” a particularly low point reads.</p>
<p>As this all started to unravel, my hopes were fading that I could ever reach a solution that didn’t involve shutting down the company. This was more than just burnout — my mental and physical health were so bad that it was clear that Cascable was actively harmful to me. But why?</p>
<p>Eventually, I did arrive at some sort of breakthrough. My entire life, I’ve identified myself as a developer, as a <em>coder</em>. And, trite as it sounds, I care deeply about the pieces of code that I write — it is, after all, the sum of my life and experience as a developer up to the point it’s written. This is workable enough in a larger workplace — other people get to handle the direction of the company and which products to make, and the developers get to put their energy into their craft. Of course on a larger scale development time is just another investment, and those same “other people” can just as easily change course and cancel your projects. However, in a larger company you can grumble at management and move on to the next thing you’re handed. Having a deep, emotional connection to an entire business and its day-to-day details is, well, not a good thing.</p>
<p>The deeper realisation was that the dual-income approach had an implicit tension that was hard to resolve. If neither app sales or client work completely supported the business, everything would have undue pressure put onto it — onto <em>me</em> as the person that had to fix both. “This update <em>must</em> increase revenue.” “I <em>must</em> find a client soon.” I can’t work on one thing without worrying about the other, and that’s not sustainable — and I’m constantly annoyed that client work is taking time out of working on updates that <em>could</em> earn more money and reduce the need for client work in the first place.</p>
<p>Additionally — and getting <em>right</em> down to the core of my identity — is that I considered having to take client work as a failure. My goal is to be an “indie developer”, and selling programming hours to someone else, to <em>me</em>, is a failure of that goal. When I’d grumbled about this in the past, my ever-supportive wife had pointed out that to be able to work 50% on my own projects and 50% for someone else is an incredible achievement that many would <em>love</em> to be able to do. She’s right, of course. Hell, when people ask me how to “go indie”, my answer is to find part-time work to fund the endeavour — unless you have a year or two of salary sitting in the bank, what else can you do? Furthermore, I know multiple people running their own businesses — programming and not — that use consulting hours as an additional income stream to support the business. It’s an intelligent and pragmatic way to run a small company, and I don’t look down on <em>anyone</em> that runs things this way.</p>
<p>And yet. Despite all of this rationality… it nags me. It pulls me down. It grips its tendrils into my being with a single, debilitating word: “failure”.</p>
<p>After weeks of solitude and introspection, I was finally starting to understand… but still had nothing in the form of solutions.</p>
<p>More tendrils. <em>“Failure”.</em></p>
<hr>
<p>Thankfully, the exploration of my physical health was a simpler affair. I describe Guillestre as “not particularly special”, but it’s smack bang in the middle of <a href="https://en.wikipedia.org/wiki/Hautes-Alpes">one of the most beautiful regions on Earth</a>. An <a href="https://www.bikeradar.com/advice/buyers-guides/best-electric-mountain-bikes/">eMTB</a> rental place opened a few years ago, and during my stay I’d been renting a bike 2-3 times per week. I’d explored the valley by bike plenty of times before, but having an <em>e</em>MTB unlocked routes previously unavailable to me — particularly in my physical state at the time. Almost every time I went out, I’d round a new corner and exclaim “HOLY SHIT” to nobody in particular as a new vista flooded into view. Early in the trip I’d picked one of the peaks and decided that it’d be fun to actually get up there — two weeks later, when I did manage it, it took my breath away so sharply that I had to get off the bike and fight back tears for a moment. If anyone asks, it was the altitude.</p>
<p>The pure joy this brought me gave very clear answers very quickly. Neglecting my health was robbing myself of the joy of exploring the outdoors as well as making my entire life worse. Reversing that course would be a huge step in helping everything else.</p>
<p class="center"><img src="/pictures/identity-crisis/ceillac.jpg"></p>
<p>Towards the end of one <a href="https://www.strava.com/activities/4064168244">particularly enjoyable ride</a>, I was blasting along a trail when, out of nowhere, the bike was no longer underneath me and trees were whipping by at a terrifying rate. I awoke in a crumped pile at the bottom of a tree halfway down a ravine with my Apple Watch wailing and on the brink of calling the emergency services. I nearly let it. A few minutes of exploratory movements slowly ruled out a broken leg, and I started the agonising clamber back up to the path — made significantly more challenging by finding the bike halfway up. eMTBs are heavy at the best of times, but with a failed front tyre and what feels like a broken leg that’s somehow still working, it was agony. Once at the path, I carried the bike very slowly — and in a lot of pain — off the side of the mountain and called for help.</p>
<p>Luckily, I escaped what should have been broken bones and and airlift to hospital with “only” a hairline-fractured rib and extreme bruising down the side of my torso, hip, and leg. Even the bike survived largely unscathed — a couple of new spokes and it was good to go. Revisiting the crash site revealed what had happened: a moss-covered rock had caught a spoke in the front wheel, ripping the bike out from under me and sending me flying down the ravine, which thankfully was just “extremely steep” rather than “a vertical cliff”. I’d bounced off a large, flat rock before colliding with a cluster of small trees. Had the rock been pointy, things would have been a lot worse. Had the trees not been there, I’d have gone much further down into the ravine, possibly into the river at the bottom. Finally, the data on my bike computer showed that I’d been going much faster than was sensible for the trail, which is something an eMTB is great at doing.</p>
<p>Lessons learned, I hopped back onto a bike as soon as I was physically able — I couldn’t let myself get scared away from something that brings so much joy.</p>
<hr>
<p>Three weeks after my arrival — and a few days after my crash — I hobbled out of the car and onto the platform of the local train station to greet the sunrise and the overnight train that was carrying my wife.</p>
<p>Over the following days I recounted my three weeks of solitude, sharing the joys of the bike rides and the darkness of the bad days, trying my best to make the jumble of thoughts somewhat coherent. This process started to help them arrange themselves better in my mind, and ever so slowly, a way out started to form.</p>
<p>A couple of days before our return to Sweden, we came down from the mountains for a day trip to Monaco to eat a horrendously fancy lunch and people-watch rich folk (it’s fun — try it some time!). The novelty of an (admittedly delicious) €80 fish lunch while watching impossibly well-dressed socialites abandon their Ferraris in the street, safe in the knowledge a valet would appear out of nowhere to deal with them obviously set my mind free, because on the drive home my wife and I had one of those conversations that end up defining the trajectory of your life.</p>
<p>The road slowly ascended up into the mountains, clinging to the side of a large riverbed. At other times of the year, the banks swell with snowmelt cascading down towards the Mediterranean ocean. Today, a tiny trickle is barely visible. The river, the road, and my car are enveloped by cliffs hundreds of metres high on either side, swallowing the light from the sunset and leaving just greyscale everywhere the car’s headlights can’t reach. As civilisation dwindles, my way out has become clear:</p>
<ol>
<li>
<p>I must make an effort to improve and prioritise my physical health. An obvious one to get started.</p>
</li>
<li>
<p>I must let go of my identity as an “indie developer” and the attachment I have to individual pieces of coding work — particularly the idea that “code quality” and “commercial success” have absolutely anything to do with one another. I need to think and act like a small business owner, not a developer, and be at peace that decisions made in that mindset may be at odds with what a developer might want.</p>
</li>
<li>
<p>I must resolve the tension that paid client work brings to my own aspirations of what being a successful small business owner looks like. This means no longer accepting paid client work because I <em>have</em> to — client work must make sense for the company’s expertise and products. If a piece of client work doesn’t suit the company’s strengths or make the company stronger, it shouldn’t be accepted. If I can’t do this within six months, I need to throw in the towel and shut down the company.</p>
</li>
</ol>
<p>Greyscale gives way to complete darkness as the road narrows and gets even twistier. Together, we come to a conclusion that’s as clear as the stars above — in order to move forward, I have to let go of the identity I’ve held for myself for nearly twenty years, and learn to change how I define my own self-worth. To let go of what previously defined whether I’d done a good job or not. To somehow <em>not</em> take it personally when my best programming work doesn’t result in commercial success. On top of all that, I needed to figure out how to allow the company to survive without selling half of its time to external clients within six months.</p>
<p>The Herculean nature of my “way out” probably should have crushed my spirits even more. However, simply finding an answer was such a breakthrough that it felt like half the challenge was already overcome.</p>
<p>The mountain pass had long lost any semblance of civilisation. Street lights were a distant memory, and we’re crawling up hairpins at 20 km/h — my little car slowly scaling the mountain. I feel free. The way forward is going to be tough, but at the very worst it’ll be over in six months.</p>
<h3 id="summer-2022-18-months-later">Summer 2022 — 18 Months Later</h3>
<p>“Alright, I think it’s time to make a decision.”</p>
<p>My wife and I are trying to find answers that weren’t there in note-covered cards strewn across our dining room table. A laptop displays market research and mock advertising as I tap through a prototype app I’d hacked together over the course of a couple of weeks.</p>
<p>“At some point, we need to abandon this idea or jump in with both feet and go for it. The core question is: Is this idea good enough to invest a lot of time and,” — I switch over to a spreadsheet labelled <em>Estimated MVP Costs</em> — “a <em>lot</em> of money in to see if it’ll actually work?”</p>
<hr>
<p>In the eighteen months or so since returning from that trip to France, things have been, slowly but surely, recovering. The most meaningful event was a successful partnership with a camera manufacturer to integrate them with the Cascable app. This, on top of the financial contribution, helped me successfully switch my mindset — for the most part — away from “developer” to “business owner”, and I’m able to take a more pragmatic approach to my decision making. Alongside the camera manufacturer integration, we did a <a href="https://cascable.se/blog/cascable-60-new-remote-phase-one/">very large overhaul</a> of an ageing component of the app. Much like the disastrous Cascable 4.0 update in 2019, this was a modernisation of an existing feature-set. <em>Unlike</em> 2019, there was no pressure for it to increase revenue — it was done because it needed doing, and that was all. <em>Business Owner</em> Daniel decided it was time to revamp the app’s App Store presence, so a decent investment was put into making sorely-needed new screenshots, video, and marketing copy.</p>
<p>Since then, sales have risen and combined with more B2B revenue from <a href="https://developer.cascable.se">CascableCore</a>, the company is able to focus 100% of its time to Cascable projects. It’s hard to pinpoint exactly what caused app sales to rise — perhaps this time the revamp of an existing feature was meaningful to revenue. Perhaps the better App Store presence has boosted things. Perhaps my attitude shift and the confidence boost from landing the camera manufacturer deal has let me move forward in a better way. Most likely, it’s a little of each.</p>
<p>I’m not perfect, of course. I continue to repeatedly declare that I’m going to dedicate more time to making marketing content, and I repeatedly fail to do so. I’m trying, though! Old habits die hard. The tendrils of failure continue to pop up now and then and assert their grip — every time I see another indie post success or brag about sales, they slither into my soul for a moment — but by now I can largely brush them away, and they’re controlled enough that I can identify them as a personality trait that can likely be soothed with counselling.</p>
<p>The freedom gained from letting go of the “this single app must earn all of the revenue and if it doesn’t I’m a failure” mindset has allowed me to poke at an idea for a new app that’s been rattling around in my head for years. Indie Developer Daniel would have just jumped right in and started writing code, but <em>Business Owner</em> Daniel is here now. We did market research with user surveys and questionnaires, feeling out the market a little. We put together sample marketing, figuring out who this might be marketed towards and what features would be important to them. We had screenshots and adverts before a single line of code was written — and then I wrote a small prototype to make sure the idea would, you know, actually work technically. Nearly twenty years at this, and I’ve never done it this way ‘round before — usually it’s code first, find the market later. If <em>that’s</em> not a great example of “success can hide a lot of failures”, I don’t know what is.</p>
<hr>
<p>“The core question is: Is this idea good enough to invest a lot of time and,” — I switch over to a spreadsheet labelled <em>Estimated MVP Costs</em> — “a <em>lot</em> of money in to see if it’ll actually work?”</p>
<p>A moment of nervous silence.</p>
<p>“Yes. I think it is.”</p>
<p>More silence.</p>
<p>“Me too.”</p>
<h3 id="epilogue--loose-ends">Epilogue & Loose Ends</h3>
<h4 id="physical-health">Physical Health</h4>
<p>A few weeks after returning from France, I drummed up the courage to go into a gym and ask about a personal trainer with the goal of getting into a routine to turn my momentum around and slowly start improving my health. By sheer happenstance, I got paired with a trainer whose attitude towards the craft inspired me so much that my intended few weeks just kept on going — I’m continuing training to this day. Thanks to her, I did a mountain bike race last year, and am working towards doing it again this year with an even better time. This is <em>far</em> beyond any goal I’d originally set, and I still can’t quite believe it myself.</p>
<p class="center"><img src="/pictures/identity-crisis/race.jpg"> <br>
<em>I was trying to pull off a “determined” look, but ended up with “bemused”.</em></p>
<h4 id="excellent-humans">Excellent Humans</h4>
<p>Thanks to my tendency to turn in on myself during times of pain, a number of people were immeasurably helpful to me without actually realising the magnitude of what I was going through. I have a tradition of reaching out to people who have had an especially meaningful impact on my life at the end of each year so they’ve all largely been thanked in person, but still:</p>
<ul>
<li>
<p>Thank you to the folk who helped me with negotiating the camera manufacturer partnership in 2020/21 — your business acumen saved my bacon.</p>
</li>
<li>
<p>Thank you to Claude for fishing me out of a ravine with a broken ego and a broken bike.</p>
</li>
<li>
<p>Thank you to my personal trainer who guided me through a world full of people very much Not Like Me to get me on the right health path (and then somehow to a race).</p>
</li>
<li>
<p>Thank you to various friends and strangers who, with no knowledge of my situation, performed perfectly innocuous kind gestures that happened to be incredibly meaningful.</p>
</li>
<li>
<p>And of course, thank you to my wife who — even at the best of times will put up with my shit — stood by me as a I broke down, had the strength to let me leave for three weeks of solitude thousands of kilometres away, then helped me put myself back together again. Words, gifts, acts, nor cold hard cash could ever communicate my gratitude.</p>
</li>
</ul>
<h4 id="final-word">Final Word</h4>
<p>If you made it this far, thank you! As I mentioned at the beginning, publishing this (kind of against my better judgement still) is aimed to draw a line under these past few years so I can leave the sorrow behind and take the lessons forward.</p>
<p>At the time of writing (well, typing it up), I’m fully focused on the new app idea mentioned above, and the aim is to launch a limited beta test of it around the end of January or so. I’m excited! If you’d like to follow along, you can do so by following me on <a href="https://mastodon.social/@ikenndac">Mastodon</a>. I also plan to post some of the more interesting technical things on this blog — back to business as usual, finally.</p>
tag:ikennd.ac,2020-07-05:/blog/2020/07/vacation-in-saudi-arabia/Vacation In Saudi Arabia2020-07-05T10:15:00Z2020-07-05T10:15:00Z<p>Back in February, <em>just</em> before the world went entirely to shit, I went on holiday to Saudi Arabia. The experience was pretty incredible, and one I’ve decided to write about alongside some of my favourite photos from the trip.</p>
<p class="center"><img src="/pictures/saudi-beetle.jpg"></p>
<p>You can read the post in full over at <a href="https://photos.ikennd.ac/vacation-in-saudi-arabia">Vacation In Saudi Arabia</a> on my photos subsite. Enjoy!</p>
tag:ikennd.ac,2020-04-05:/blog/2020/04/boundaries-when-working-from-home/Successfully Working From Home: It's All About Boundaries!2020-04-05T16:00:00Z2020-04-05T16:00:00Z<p>As the COVID-19 social distancing settles in, the novelty of working from home is starting to wear off and, <em>even worse</em>, we’re starting to realise that instead of the awesome feeling of “I’m always at home!”, we’re starting to suffer from… “Which also means… I’m always at work!”</p>
<p>Working from home can very easily end up eveloping our entire lives, making it feel like there’s no escape. It starts when you decide “Oh, since I’m not commuting, I can spend that extra time working!”, and ends when you’re sitting in bed checking work emails at midnight.</p>
<p>A few years ago, I worked from home fulltime for towards a year. Here are my tips for staying sane, staying productive, and most of all, staying healthy. As you’ll see, <em>everything</em> revolves around a critically important theme: <strong>boundaries</strong>.</p>
<p><strong>Disclaimer:</strong> I’m not a mental health expert, and this entire set of tips is within giant “in my experience” and “I find that…” modifiers. Please take inspiration here if you can, but don’t force yourself to this way of working.</p>
<p><strong>Another Disclaimer:</strong> This post is aimed at people who work using computers and are trying to transition into healthily working from home <strong>in a childless environment</strong>.</p>
<h3 id="problem-1-boundaries-in-workspace">Problem 1: Boundaries In Workspace</h3>
<p>The great thing about travelling outside your home to work is that it puts “work” in a completely separate physical space — which makes it really easy for your brain to map it to a separate mental space as well. It’s important to be aware that your “work space” is both the physical place where you perform your work, and the mental place in which your mind exists while doing it.</p>
<p>Travelling to work moves you to a new place physically, and gives your mind a comfortable routine that allows it to prepare for the workday ahead. In a similar way, travelling <em>home</em> from work leaves your work behind both physically and mentally — giving your mind a chance to wind down and relax.</p>
<p>This all falls completely to pieces when you’re working from home and your workplace is a laptop on your dining room table. There’s no physical or mental separation between home and work — and if you can’t leave work behind mentally, you’ll find yourself “quickly checking Slack” while dinner is cooking or “just looking at this email” before bed, and you’ll completely lose that separation that’s so important.</p>
<p>Luckily, there are many things we can do to help our minds keep work and life separate, even within the home.</p>
<h4 id="find-an-office-in-your-home-and-always-call-it-by-that-name">Find an “office” in your home, and always call it by that name</h4>
<p>This is easier in a house with spare bedrooms than in a one-bedroom apartment, but it can be done anywhere. Having a single, dedicated office space in your home for work will really help maintain boundaries — giving you place to “go to work”, and perhaps more importantly, leave. Even if you put your laptop on your dining room table to work, tape off that part of the table with something that won’t damage it. <em>That</em> is your office.</p>
<p class="center"><img src="/pictures/working-from-home/imacs.jpg"> <br>
<em>I currently have this ridiculous setup at home, because I brought my work computer back from my main office. My “office” is now the left-hand side of this desk.</em></p>
<p class="center"><img src="/pictures/working-from-home/table-tape.jpg"> <br>
<em>Taping off a corner of table creates a completely valid office.</em></p>
<p>Once you have an office (or an “office”), be strict! The only thing you do in the office is work. When it’s time to work, go to the office, and when it’s no longer time to work, leave. If you share your home with other people, sit down and have a disscussion with them to explain that your office at home should be treated as if it’s your office at work — when you’re there, you should be treated as if you were in an office somewhere else. “Sorry, I’m in the office right now — I’ll do that when I get back home.” is a completely valid thing to say.</p>
<h4 id="if-you-work-with-a-computer-keep-your-home-computer-and-your-work-computer-separate-even-if-theyre-the-same-computer">If you work with a computer, keep your home computer and your work computer separate… even if they’re the same computer</h4>
<p>Now you have your physical location sorted, it’s time to work on your mental space.</p>
<p>If you’re lucky enough to have more than one computer, this is easy. However, if you do only have the one, this can be achieved by creating a new user on your computer and dedicating it to work. Only put work stuff on your work computer/user, and only non-work stuff on your home computer/user.</p>
<p>This artificial boundary provides two benefits: It doesn’t clutter your home computer/user with work stuff (and vice versa), and it makes transitioning from one to the other a physical action in getting up and moving to the other computer, or clicking a button or two to specifically tell it “I want you to be in work mode now”. This physical action will help your mind separate the two things as well.</p>
<p class="center"><img src="/pictures/working-from-home/users.png" width="505"> <br>
<em>My wife says my work picture is the less professional of the two… BUT I’M WEARING A TIE!</em></p>
<p>A particularly nice thing to do — especially if you’re sharing one computer with yourself — is to configure a different colour scheme for your home and work computer/user. It’s amazing how different the same machine can feel with a different colour scheme, and it’ll help your mind settle in and focus on what you’re doing.</p>
<p class="center"><img src="/pictures/working-from-home/split-screenshot.jpg"> <br>
<em>Having a very clear visual distinction between your computer being in “home” mode and “work” mode can help your mind do the same.</em></p>
<h3 id="problem-2-boundaries-in-time">Problem 2: Boundaries In Time</h3>
<p>I’ll get this out of the way early: <strong>The idea that you can counter a drop in productivity by working more hours per day is a fallacy.</strong> If you would typically do an 8 hour workday in the office, doing <em>more</em> hours than that at home won’t help if you’re suffering a productivity drop. You’ll still get less work done, and you’ll feel like shit because you hurt your work-life balance for no reason.</p>
<p>In order to keep a healthy work-life balance when both are happening in one building, you need to be strict with your <em>time</em> boundaries as well as your workplace ones. This actually goes both ways — it’s important not to let your work time take over your home time, but it’s equally as important to not let your home time take over your work time.</p>
<p>What does this mean?</p>
<p>Well, it may be tempting to take a little 30 minute break from work during the day to, say, do the laundry. So, off you go, breaking your physical workspace boundary in the process. As you’re doing the laundry, you notice that the utility room is a bit dusty, so you whip out the vacuum — it’s only an extra 5 minutes, right? Well, since I have the vacuum out…</p>
<p>The next thing you know, it’s an hour later. No biggie, right? You’re working from home! You’ll just work an extra hour into the evening!</p>
<p>This sounds harmless, but how would you feel if you worked an extra hour at your normal workplace? It’s never a nice feeling, and you get home more tired and more grumpy than you normally would have. Dinner ends up being later, giving you less time in the evening to unwind before bed.</p>
<p>I’m not normally a fan of slippery-slope arguments, but this is one of them. It’s so easy to just blur the lines “just this once”, but as time goes on, things blur together until you have no separation between work and life at all — you just kinda “do stuff” all day, then sleep, then do the same the next morning.</p>
<p>Let’s see what we can do to help ourselves:</p>
<h4 id="be-as-strict-with-time-as-you-usually-are">Be as strict with time as you usually are</h4>
<p>If you normally get to work at 9am, take an hour lunch, then go home at 5pm, keep that routine up at home. When it’s time to go home, either turn off that computer and leave it in your office, or log out of your work account and log in to your home one as you bring your computer “home” with you from work. As with maintaining your workplace, if you live with other people, explain to them how important it is that your work times are respected. Continuing to use phrases like “I’m at work right now, I’ll do it when I get home.” really help here.</p>
<p>If you get tempted to sneak in some quick housework or something else that’s suddenly possible because you’re physically at home, try not to get distracted by that when it pops into your mind. Instead, write it down on a little “To-do when I get home” list — if you finish work early, you can “go home” early and get those things done!</p>
<h4 id="replace-your-normal-commute-with-a-stationary-one">Replace your normal commute with a stationary one</h4>
<p>It’s tempting to decide to give yourself more work or more home time in lieu of a commute, but your commute is an important part of your day — allowing your mind to get ready for work, and to wind down afterwards.</p>
<ul>
<li>
<p>If your normal commute consists of sitting on public transport as you listen to podcasts/music/etc, you can continue to do that. Searching for “train view” on YouTube provides multi-hour long videos like <a href="https://www.youtube.com/watch?v=1Rq9b_bn6Bc">this one</a> — you can still stare out of the window of the train even if you’re stuck at home!</p>
</li>
<li>
<p>If you exercise by the form of walking or biking, you can do that too. Biking indoors can be expensive — you need an exercise bike or a “turbo trainer” to mount your real bike to. Jogging or walking on the spot is easier without needing extra equipment, but do be careful not to hurt yourself.</p>
</li>
</ul>
<h3 id="problem-3-covid-19">Problem 3: COVID-19</h3>
<p>This is a bit of a special section, since it’s hyper-specific to the time this is being written. It’s difficult to write “tips” for this without getting preachy, so I’ll keep it brief:</p>
<p>It’s understandable that you’re anxious right now, and scared. There’s so many things going on that you can’t control, and a thousand people in your Twitter/Facebook/Slack linking articles every few minutes.</p>
<ul>
<li>
<p>Heightened anxiety right now is to be expected, and reduced productivity along with it. That’s OK.</p>
</li>
<li>
<p>Try to filter the information firehose a little — by muting those particularly noisy people on social media, by avoiding areas of the internet full of speculation, by looking up news from a source that focuses on your local area, and so on.</p>
</li>
<li>
<p>While working, try to switch off the firehose entirely. Get rid of Twitter, Facebook, Slack channels dedicated to COVID-19, the lot. You can keep up-to-date and safe without up-to-the-second feeds scrolling past all day long.</p>
</li>
<li>
<p>While not working, try to focus on helping those you can help, rather than dewlling on those you can’t. Keeping yourself healthy means you can help keep your family and friends healthy — calling a family member to help keep spirits up will do far more good for both of you than sitting on your computer fretting about the death toll in a country halfway around the world away.</p>
</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>Maintaining a healthy work-life balance is difficult when both of those things happen in the same place. I’ve found that successfully maintaining that balance, with healthy productivity when working and healthly time away when you’re not requires that you’re very strict in a few areas:</p>
<ul>
<li>
<p>You must be strict about <em>where</em> you work and where you don’t.</p>
</li>
<li>
<p>You must be strict about <em>when</em> you work and when you don’t.</p>
</li>
</ul>
<p>Some of the suggestions here sound silly on the surface, but they have an important underlying idea: maintaining a strict separation between home and work, and retaining that buffer between the two with a stationary commute.</p>
<p>Being healthy also requires that you keep in mind the most important sentence in this entire post: <strong>The idea that you can counter a drop in productivity by working more hours per day is a fallacy.</strong> Especially in the beginning, you’ll have horribly unproductive days. And that’s completely OK. Turn off your computer at 5pm, leave work behind, and try again tomorrow. You got this!</p>
tag:ikennd.ac,2019-04-02:/blog/2019/04/when-should-i-head-home-from-wwdc/When Should I Head Home From WWDC?2019-04-02T20:00:00Z2019-04-02T20:00:00Z<p>This question comes up every year, and I’ve seen it floating around Twitter today.</p>
<blockquote>
<p>When should I head home from WWDC?</p>
</blockquote>
<p>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.</p>
<p>I imagine it’s too late now since most people have probably booked their flights already, but allow me to propose an alternative.</p>
<p>Fly home on <em>Monday</em>. Especially if you’re heading back to Europe.</p>
<p>Let me explain.</p>
<p>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.</p>
<p>Basically, you won’t rest until September.</p>
<p>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 <em>nothing</em> that required brainpower. I went biking on a rented bike, and took an open top train on a tour through the countryside.</p>
<p>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 <em>wonderful</em> 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.</p>
<p class="center"><img src="/pictures/pacific-bay-drone.jpg"> <br>
<em>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.</em></p>
<p>It’s <em>incredibly</em> 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.</p>
<p>Of course, this won’t be for everyone. However, I urge you to consider it! Hotels outside of the WWDC bubble are <em>significantly</em> 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 weekends<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>!</p>
<p>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.</p>
<p>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!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>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”. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
tag:ikennd.ac,2019-02-04:/blog/2019/02/nanoc-on-ipad/Editing, Previewing and Deploying Nanoc Sites Using An iPad2019-02-04T21:00:00Z2019-02-04T21:00:00Z<p>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!</p>
<p class="center"><img src="/pictures/nanoc-on-ipad/nanoc-on-ipad.jpg"> <br>
<em>Unfortunately, the power button on the iMac G3’s keyboard does nothing on an iPad.</em></p>
<p>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 <em>can</em> be done entirely on an iPad, those that manage to do so end up allowing us to modify an old joke:</p>
<blockquote>
<p>How can you tell if someone uses an iPad as a laptop replacement? Don’t worry — they’ll tell you!</p>
</blockquote>
<p>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 <a href="https://support.apple.com/guide/shortcuts/welcome/ios">Shortcuts</a> and URL callback trees.</p>
<p>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.</p>
<h3 id="self-inflicted-development-hell">Self-Inflicted Development Hell</h3>
<p>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.</p>
<p>My blog, however, uses <a href="https://nanoc.ws">nanoc</a>. 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!</p>
<p>To do this, I simply open my terminal, <code>cd</code> into the directory of my blog, then run <code>bundle exec nanoc</code> to generate… and we can see why this doesn’t work on an iPad.</p>
<h3 id="developer-solutions-to-developer-problems">Developer Solutions to Developer Problems</h3>
<p>So, what do I really want to do here? I want to be able to:</p>
<ol>
<li>
<p>Write blog posts on my iPad.</p>
</li>
<li>
<p>Preview them on my iPad to check for layout problems, see how the photos look, make sure the links are correct, etc.</p>
</li>
<li>
<p>Once I’m happy with a post, publish it to my blog.</p>
</li>
</ol>
<p>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.</p>
<p>To achieve this, we really need to be able to put the locally modified content through <code>nanoc</code> and display the output through a HTTP server. This is easy peasy on a traditional computer, but not so on an iPad.</p>
<p>Here we arrive at why I’m only <em>sort of</em> 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:</p>
<ul>
<li>
<p>A continuous integration (CI) server watching my blog’s repository for changes, then building my blog with <code>nanoc</code> for each change it sees.</p>
</li>
<li>
<p>A static web server set up to serve content from a location based on the subdomain used to access it.</p>
</li>
</ul>
<p>As I’m writing this, I’m committing the changes to a branch of my blog’s repository - let’s say <code>post/nanoc-on-ipad</code>. 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 <code>http://post-nanoc-on-ipad.static-staging.ikennd.ac</code> to view the results. It’s not quite a <em>live</em> 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.</p>
<h3 id="my-setup">My Setup</h3>
<p>The first thing we need to do is get a CI server to build our <code>nanoc</code> site. I won’t actually cover that directly here - there are lots of CI services available, many of them free. Since <code>nanoc</code> is a Ruby gem, you can set up a cheap/free Linux-based setup without too much fuss.</p>
<p>I’m using <a href="https://www.jetbrains.com/teamcity/">TeamCity</a> 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.</p>
<p class="center"><img src="/pictures/nanoc-on-ipad/teamcity-ipad.png"> <br>
<em>TeamCity’s web UI on iPad isn’t quite perfect, but it functions just fine.</em></p>
<p>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 <a href="https://www.calleerlandsson.com/about/">web developer friend of mine</a> 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.</p>
<p>Now for the fun part!</p>
<h3 id="linking-it-all-together">Linking It All Together</h3>
<p>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 <em>definitely</em> cooler than just having a single previewing destination that just shows whatever happens to be newest.</p>
<p>In your DNS service, add an <strong>A/AAAA</strong> record for both the subdomain you want to use as the “parent” for all this, and a wildcard subdomain. For example, I added <code>static-staging</code> and <code>*.static-staging</code> records to <code>ikennd.ac</code> and pointed them to my server.</p>
<p>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 <code>mod_vhost_alias</code> to help out out. It’s not a default module in the Apache version I had, so <code>a2enmod vhost_alias</code> to enable it.</p>
<p>My configuration looks like this:</p>
<pre><code class="language-apache"><span class="nb">DocumentRoot</span><span class="w"> </span><span class="sx">/ikenndac/public_html/content</span>
<span class="nt"><Directory</span><span class="w"> </span><span class="s">/ikenndac/public_html/content</span><span class="nt">></span><span class="w"> </span>
<span class="w"> </span><span class="nb">Options</span><span class="w"> </span><span class="k">None</span>
<span class="w"> </span><span class="nb">AllowOverride</span><span class="w"> </span><span class="k">None</span>
<span class="w"> </span><span class="nb">Order</span><span class="w"> </span>allow,deny
<span class="w"> </span><span class="nb">Allow</span><span class="w"> </span>from<span class="w"> </span><span class="k">all</span>
<span class="w"> </span><span class="nb">Require</span><span class="w"> </span><span class="k">all</span><span class="w"> </span>granted
<span class="nt"></Directory></span>
<span class="nt"><VirtualHost</span><span class="w"> </span><span class="s">*:80</span><span class="nt">></span><span class="w"> </span>
<span class="w"> </span><span class="nb">ServerAlias</span><span class="w"> </span>*.static-staging.ikennd.ac
<span class="w"> </span><span class="nb">VirtualDocumentRoot</span><span class="w"> </span><span class="sx">/ikenndac/public_html/content/</span>%0/
<span class="w"> </span><span class="nb">ErrorLog</span><span class="w"> </span><span class="sx">/ikenndac/public_html/static-staging.ikennd.ac.error.log</span>
<span class="w"> </span><span class="nb">CustomLog</span><span class="w"> </span><span class="sx">/ikenndac/public_html/static-staging.ikennd.ac.access.log</span><span class="w"> </span>combined
<span class="nt"></VirtualHost></span></code></pre>
<p>That <code>VirtualDocumentRoot</code> line is the important part here. If I go to <code>http://my-cool-blog.static-staging.ikennd.ac</code>, thanks to that <code>%0</code> in there, Apache will look for content in <code>/ikenndac/public_html/content/my-cool-blog.static-staging.ikennd.ac</code>.</p>
<p>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.</p>
<p><code>nanoc</code> has the <code>deploy</code> 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 href="https://github.com/iKenndac/new-blog/blob/master/static-staging-deploy.sh">a script</a> to do the work:</p>
<pre><code class="language-bash"><span class="c1"># Get the current branch name</span>
<span class="nv">BRANCH_NAME</span><span class="o">=</span><span class="sb">`</span>git<span class="w"> </span>rev-parse<span class="w"> </span>--abbrev-ref<span class="w"> </span>HEAD<span class="sb">`</span>
<span class="c1"># Replace anything that's not a number or letter with a hyphen.</span>
<span class="nv">SANITIZED_BRANCH_NAME</span><span class="o">=</span><span class="sb">`</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">BRANCH_NAME</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span>A-Z<span class="w"> </span>a-z<span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-e<span class="w"> </span><span class="s1">'s/[^a-zA-Z0-9\-]/-/g'</span><span class="sb">`</span>
<span class="nv">SANITIZED_BRANCH_NAME</span><span class="o">=</span><span class="sb">`</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">SANITIZED_BRANCH_NAME</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">'s/\(--*\)/-/g'</span><span class="sb">`</span>
<span class="c1"># Build the right directory name for our HTTP server configuration.</span>
<span class="nv">DEPLOY_DIRECTORY_NAME</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">SANITIZED_BRANCH_NAME</span><span class="si">}</span><span class="s2">.static-staging.ikennd.ac"</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Deploying </span><span class="si">${</span><span class="nv">BRANCH_NAME</span><span class="si">}</span><span class="s2"> to </span><span class="si">${</span><span class="nv">DEPLOY_DIRECTORY_NAME</span><span class="si">}</span><span class="s2">…"</span>
<span class="c1"># Use rsync to get the content onto the server.</span>
rsync<span class="w"> </span>-r<span class="w"> </span>--links<span class="w"> </span>--safe-links<span class="w"> </span>output/<span class="w"> </span><span class="s2">"website_deployment@static-staging.ikennd.ac:/ikenndac/public_html/content/</span><span class="si">${</span><span class="nv">DEPLOY_DIRECTORY_NAME</span><span class="si">}</span><span class="s2">/"</span></code></pre>
<p>A couple of notes about using <code>rsync</code> to deploy from CI:</p>
<ul>
<li>
<p>Since CI runs headless, it’s unlikely you’ll be able to use a password to authenticate through <code>rsync</code> - 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.</p>
</li>
<li>
<p>If your CI still fails with auth errors after setting up SSH key authentication, it might be failing on a <em>The authenticity of host … can’t be established</em> 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.</p>
</li>
</ul>
<h3 id="deploying-the-final-result">Deploying the Final Result</h3>
<p>The beauty of this process that that we’ve been deploying the entire time! If you follow <a href="https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow">git flow</a> and your <code>master</code> branch only ever has finished content in it, you could point your main domain to the same directory that the CI server puts the <code>master</code> branch and you’re done! If your <code>master</code> branch isn’t that clean, you could make a new <code>deployment</code> branch and do the same there.</p>
<p>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 <code>static-staging-deploy.sh</code> script to <code>rsync</code> to a different place if it detects that it’s on the <code>deployment</code> branch.</p>
<h3 id="conclusion">Conclusion</h3>
<p><em>Phew!</em> 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)!</p>
<p class="center"><img src="/pictures/nanoc-on-ipad/side-by-side.png"> <br>
<em>I kind of want a mouse…</em></p>
<p>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!</p>
<p>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:</p>
<ul>
<li>
<p>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.</p>
</li>
<li>
<p>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.</p>
</li>
</ul>
<p>All in all, though, I’m really happy with the outcome of this experiment. Real computers can suck it!</p>
<h3 id="apps-used-to-write-this-blog-post">Apps Used To Write This Blog Post</h3>
<p>I was pretty successful in writing this post on my iPad. I used the following apps:</p>
<ul>
<li>
<p><a href="https://workingcopyapp.com">Working Copy</a> for text editing and <code>git</code> work.</p>
</li>
<li>
<p><a href="https://panic.com/prompt/">Prompt</a> for SSHing into my HTTP server to tweak some configuration.</p>
</li>
<li>
<p><a href="https://cascable.se">Cascable</a> for copying photos from my camera and light editing.</p>
</li>
<li>
<p><a href="https://affinity.serif.com/en-gb/photo/ipad/">Affinity Photo</a> for sizing photos down to the right dimensions for my blog.</p>
</li>
</ul>
<p>Maybe next time I’ll even manage to do the Audioblog recording on my iPad!</p>
tag:ikennd.ac,2019-02-02:/blog/2019/02/despair-thy-name-is-app-store/Despair, Thy Name is App Store2019-02-02T01:00:00Z2019-02-02T01:00:00Z<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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 <em>second</em> 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.</p>
<p>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.</p>
<p>I’ve spent the <em>entire</em> day trying to fix this. An hour on the phone with EU Developer Support, who were trying to help but ultimately were powerless.</p>
<p>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.</p>
<p>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. <em>Especially</em> if it’s resolved.</p>
<p>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.</p>
<p>This time it’ll be fine. The next time too, if I’m honest. But after <em>that</em>? 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.</p>
<p>Sorry to complain. Stiff upper lip, and all that.</p>
tag:ikennd.ac,2018-12-20:/blog/2018/12/audioblog/Introducing the iKennd.ac Audioblog!2018-12-20T17:00:00Z2018-12-20T17:00:00Z<p>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!)</p>
<p>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.</p>
<p>The <em>problem</em> 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!).</p>
<p>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.</p>
<p>So, I hereby announce… <strong>I am not starting a podcast just yet.</strong> <em>(Pause for applause.)</em></p>
<p>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 <a href="http://swiftandfika.com">Swift and Fika</a> conference here in Stockholm entitled <a href="https://www.youtube.com/watch?v=f4ihOnvU68Y">Adventures in API Design</a>, 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 <a href="https://www.youtube.com/watch?v=cVqMvBr7YKU">writing command-line apps with Swift Package Manager</a>, in which I talked about drone photography and some tips and tricks for writing little command-line apps in Swift.</p>
<p>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, <em>most</em> importantly, people tell me they like my talks and get something useful from them.</p>
<p>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.</p>
<p>So, I hereby announce… <strong>The iKennd.ac Audioblog</strong> <em>(Pause for applause?)</em></p>
<p>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 <em>audioblog</em>. It’s like an audio<em>book</em>, 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 Podcast<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> comes along sometime in 2019.</p>
<p>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.</p>
<p>You can subscribe to the audioblog via links at the top of this post.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Title TBD. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
tag:ikennd.ac,2018-11-02:/blog/2018/11/lantmateriet-lookup/Why Publishing Some Nice Autumnal Photos Online Made Me Write An App2018-11-02T16:00:00Z2018-11-02T16:00:00Z<p>Don’t care about programming and just want to see some pretty photos of colourful trees? Check out <a href="https://photos.ikennd.ac/autumn-from-the-air">Autumn From The Air</a> on my new photos subsite. Enjoy!</p>
<hr>
<p>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.</p>
<p>As it turns out, taking a long exposure of moving object A with moving camera B while standing in stationary position C is <em>incredibly</em> difficult. Over a couple of sessions I took hundreds of photographs, and got <em>four</em> that I’m happy with.</p>
<p class="center"><img src="/pictures/drone-mx5-rolling.jpg"> <br>
<em>This photograph took many, many tries to get.</em></p>
<p class="center"><img src="/pictures/drone-mx5-static.jpg"> <br>
<em>This photograph did not.</em></p>
<p>This was amazing! I have a <em>flying</em> 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!!</p>
<p class="center"><img src="/pictures/drone.jpg"> <br>
<em>It’s just a fancy tripod, really.</em></p>
<h3 id="bureaucracy-strikes">Bureaucracy Strikes!</h3>
<p>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.</p>
<p>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 <em>great</em> 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).</p>
<p>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 WGS84<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> coordinates.</p>
<p class="center"><img src="/pictures/lantmateriet-manual-table.png" width="600"> <br>
<em>Zzzzzzz…</em></p>
<h3 id="like-everything-this-can-be-solved-with-software">Like Everything, This Can Be Solved With Software!</h3>
<p>I finished my submission, then immediately got to work automating this, because screw doing that again.</p>
<p>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!</p>
<p>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:</p>
<p><code>$ lantmateriet-lookup -i *.jpg -html results.html</code></p>
<p>…then waiting 30 seconds while it does its magic. Lovely! Under the hood, it’s:</p>
<ul>
<li>Extracting a geotag from each image using <code>ImageIO</code>.</li>
<li>Using <code>CoreLocation</code> to do a reverse geocode to get an address.</li>
<li>Converting the WGS84 geotag coordinate into SWEREF.</li>
<li>Doing a lookup on what is <em>definitely</em> a public Lantmäteriet API to get the property allocation.</li>
<li>Writing the results of all that into a table for submission to Lantmäteriet.</li>
</ul>
<p>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: <a href="https://github.com/ikenndac/lantmateriet-lookup">lantmateriet-lookup on GitHub</a>.</p>
<h3 id="i-was-promised-autumn-photos">…I Was Promised Autumn Photos?</h3>
<p>While I was faffing about with all of this, Lantmäteriet approved my original submission. Hooray!</p>
<p class="center"><img src="/pictures/drone-autumn.jpg"> <br>
<em>The slow currents of Mälaren disturb mud around an underwater rock.</em></p>
<p>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: <a href="https://photos.ikennd.ac/autumn-from-the-air">Autumn From The Air</a>. Enjoy!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>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. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
tag:ikennd.ac,2018-04-06:/blog/2018/04/app-store-subscriptions-and-you/App Store Subscriptions And You2018-04-06T17:00:00Z2018-04-06T17:00:00Z<p>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.</p>
<p>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 <em>reasonably</em> 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.</p>
<p>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.</p>
<p><strong>Important:</strong> 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, <em>particularly</em> in the area of subscriptions. You <strong>must</strong> do your own due diligence.</p>
<h3 id="the-paid-applications-contract">The Paid Applications Contract</h3>
<p>A lot of the confusion from this stems from the fact that half of the rules for subscriptions aren’t in the <a href="https://developer.apple.com/app-store/review/guidelines/">App Store Review Guidelines</a>, but are instead located inside your <strong>Paid Applications Contract</strong>. You can find this in the <strong>Agreements, Tax, and Banking</strong> section of iTunes Connect.</p>
<p>Assuming that you have the standard contract, in-app subscription terms can be found in section <strong>3.8</strong>.</p>
<p><strong>Important:</strong> 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 <strong>not</strong> 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.</p>
<h3 id="what-were-building">What We’re Building</h3>
<p>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.</p>
<p class="center"><img class="no-border" src="/pictures/app-subscriptions/iphonex-legalese.png" width="300"></p>
<p>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.</p>
<h3 id="you-must-be-clear-about-pricing-and-billing-frequency">You Must Be Clear About Pricing And Billing Frequency</h3>
<p>You <em>must</em> be very clear about several things:</p>
<ul>
<li>That the user will be paying for a recurring subscription.</li>
<li>How much the user will pay each time.</li>
<li>How often they will pay.</li>
<li>The pricing must be in the user’s App Store currency.</li>
</ul>
<p>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 <code>SKProduct</code> objects you get in the App Store will contain the locale you should use for displaying prices.</p>
<p>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 <em>must</em> list the actual amount that will be charged in your pricing.</p>
<p class="center"><img src="/pictures/app-subscriptions/subscribe-buttons-bad-frequency.png" width="300"> <br>
<em>While this is good at showing the increased value of the longer subscription, exactly how much money is charged when is unclear. This is <strong>not</strong> allowed.</em></p>
<p class="center"><img src="/pictures/app-subscriptions/subscribe-buttons-good-frequency.png" width="300"> <br>
<em>How much money is charged when is much clearer here. This is allowed.</em></p>
<h3 id="you-must-be-clear-about-trials">You Must Be Clear About Trials</h3>
<p>If you offer a free trial, you need to be clear about that as well.</p>
<p><strong>Important:</strong> If the user takes up a subscription with a free trial then later cancels, if they want to re-subscribe they will <strong>not</strong> 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.</p>
<p>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.</p>
<p class="center"><img src="/pictures/app-subscriptions/subscribe-buttons-with-trial.png" width="300"> <br>
<em>The user is eligible for a free trial, so we make it clear that they’ll get a free trial and <strong>then</strong> they’ll be charged.</em></p>
<p class="center"><img src="/pictures/app-subscriptions/subscribe-buttons-no-trial.png" width="300"> <br>
<em>If the user is not eligible for a trial, we don’t mention it at all.</em></p>
<h3 id="you-must-include-the-correct-legalese">You Must Include The Correct Legalese</h3>
<p>This is the one that seems to cause the most problems, since legalese is hard and there’s a lot of it.</p>
<p>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 <strong>not allowed</strong>.</p>
<p>An exception to this is that you <em>are</em> allowed to have the legalese scroll off the bottom of the screen, as long as it is <em>completely</em> clear that there’s more content to read, and that you’re not hiding all of the legalese “below the fold”.</p>
<table>
<tr style="background-color: transparent; border: none;">
<td style="width: 49%;"><p class="center"><img src="/pictures/app-subscriptions/iphonese-legalese-bad.png" width="320"><br><em>Here, the legalese is entirely hidden off the bottom of the screen. Even though the user can scroll to it, this is <strong>not</strong> allowed.</em></p></td>
<td style="width: 49%;"><p class="center"><img src="/pictures/app-subscriptions/iphonese-legalese-good.png" width="320"><br><em>Here, it's very clear that there's legalese to read, and that you can scroll to read more. This is allowed.</em></p></td>
</tr>
</table>
<p>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 <em>is</em> allowed to have these be one page.</p>
<p>You can find the legalese you need to include in section <strong>3.8b</strong> 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.</p>
<p class="center"><img class="no-border" src="/pictures/app-subscriptions/iphonex-legalese.png" width="300"> <br>
<em>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.</em></p>
<p>At the time of writing, the standard contract requires that we state the following information to users.</p>
<p><strong>Important:</strong> This was correct in <em>my</em> contract at the time of writing (early April 2018). You <strong>must</strong> check your own contract!</p>
<blockquote>
<p><strong>Title of publication or service</strong></p>
</blockquote>
<p>The name of your app or subscription. Ours is <strong>Cascable Pro</strong>.</p>
<blockquote>
<p><strong>Length of subscription</strong></p>
</blockquote>
<p>In my examples here, this is in the subtitle of the buy buttons.</p>
<blockquote>
<p><strong>Price of subscription</strong></p>
</blockquote>
<p>Also in the subtitle of the buy buttons.</p>
<blockquote>
<p><strong>Payment will be charged to iTunes Account at confirmation of purchase</strong></p>
</blockquote>
<p>Covered in the first paragraph of my legalese.</p>
<blockquote>
<p><strong>Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period</strong></p>
</blockquote>
<p>Covered in the first paragraph of my legalese.</p>
<blockquote>
<p><strong>Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal</strong></p>
</blockquote>
<p>Covered in the first paragraph of my legalese.</p>
<blockquote>
<p><strong>Subscriptions may be managed by the user and auto-renewal may be turned off by going to the user’s Account Settings after purchase</strong></p>
</blockquote>
<p>Covered in the first paragraph of my legalese.</p>
<blockquote>
<p><strong>Links to Your Privacy Policy and Terms of Use</strong></p>
</blockquote>
<p>Green button at the bottom of my legalese.</p>
<blockquote>
<p><strong>Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable</strong></p>
</blockquote>
<p>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.</p>
<p>Since it <em>is</em> technically possible to buy two Cascable Pro products at once if you really try hard<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, I wrote the second paragraph of my legalese with this in mind.</p>
<h3 id="you-must-also-include-subscription-details-in-your-app-store-description">You Must Also Include Subscription Details In Your App Store Description</h3>
<p>When submitting your app to the App Store, you must also detail your subscriptions in the same manner as in the app, including:</p>
<ul>
<li>A list of the subscriptions, including their durations and prices.</li>
<li>The same legalese as you put on your in-app store page.</li>
<li>A link to your Terms & Conditions and Privacy Policy pages.</li>
</ul>
<p>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.</p>
<hr>
<p>Good luck!</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>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. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
tag:ikennd.ac,2017-01-16:/blog/2017/01/excuse-me-sir-can-i-rattle-your-macbooks/Excuse Me Sir, But Can I Rattle Your MacBooks?2017-01-16T15:00:00Z2017-01-16T15:00:00Z<p>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.</p>
<p>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.</p>
<p>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 <em>far</em> the worst experience I’ve had with Apple hardware in my life.</p>
<h2 id="macbook-pro-1--2-a-normal-doa-experience">MacBook Pro #1 & #2: A Normal DoA Experience</h2>
<p>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 <em>Rattle A</em>, which will be important later.</p>
<p>No, it did not.</p>
<p>A call to Apple later and a new MacBook Pro (<em>MBP #2</em>) 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.</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/eZdzeZUYHBM?rel=0" frameborder="0" allowfullscreen=""></iframe>
</div>
<p><br><em>MBP #1’s rattle.</em></p>
<p>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 <em>Rattle B</em>.</p>
<p>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 (<em>MBP #3</em>) is being assembled and shipped to me, again from China. The agent agreed that it’d be silly to transfer my data to <em>MBP #2</em> when <em>MBP #3</em> is on its way, so a return for <em>MBP #2</em> is arranged. The next day, it leaves my house.</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/jkBdevHHqwc?rel=0" frameborder="0" allowfullscreen=""></iframe>
</div>
<p><br><em>MBP #2’s rattle.</em></p>
<h2 id="macbook-pro-3-excuse-me-sir-but-can-i-rattle-your-macbooks">MacBook Pro #3: Excuse Me Sir, But Can I Rattle Your MacBooks?</h2>
<p>This is where it starts to get a bit abnormal.</p>
<p><em>MBP #3</em> turns up, and immediately out of the box it exhibits <em>Rattle B</em>. I call Apple again, and eventually get to a nice lady in after-sales who’s very sympathetic to my bad luck, and is <em>adamant</em> that they’ll keep sending me MacBook Pros until I get one that doesn’t rattle.</p>
<p>However, I’ve been doing some of my own research and I’m starting to think that <em>Rattle B</em> 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?</p>
<p>So, at opening time on Saturday morning I walk into the Apple Store and try to explain to the employees there that:</p>
<ol>
<li>I want to shake their MacBook Pros.</li>
<li>I’m not crazy.</li>
</ol>
<p>After surprisingly little convincing, they let me go ahead. In the eight MacBook Pros I tried, two of them exhibited <em>Rattle B</em>.</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/h6bcLSE2O08?rel=0" frameborder="0" allowfullscreen=""></iframe>
</div>
<p><br><em>A rattling MacBook Pro at the Apple Store.</em></p>
<p>I return home resigned to having a MacBook Pro with <em>Rattle B</em>. 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.</p>
<p><em>Clank.</em></p>
<p>Praying that I’m hallucinating, I pick it up and set it down again.</p>
<p><em>Clank.</em></p>
<p><em>MBP #3</em> exhibits both <em>Rattle A</em> <strong>and</strong> <em>Rattle B</em>. Superb. Time for a Twitter rant.</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/nhSMMVjO4Qw?rel=0" frameborder="0" allowfullscreen=""></iframe>
</div>
<p><br><em>MBP #3’s rattle.</em></p>
<h2 id="macbook-pro-4-maybe-i-am-crazy">MacBook Pro #4: Maybe I <em>Am</em> Crazy!</h2>
<p>At 10am this morning, the phone rings with the promised callback from the lady I spoke to on Friday.</p>
<p>After explaining my results at the Apple Store and the fact <em>MBP #3</em> 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:</p>
<p><em>MBP #4</em> 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 <strong>hand it straight off for repair</strong> if it continues to show these problems.</p>
<p>I’d like to repeat that last part, for emphasis: <strong>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.</strong></p>
<h2 id="what-next">What Next?</h2>
<p>If this were almost any other company (or if I were new to Apple), I’d have given up at <em>MBP #2</em>. 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.</p>
<p>However, all that goodwill is <em>gone</em> — <em>MBP #4</em> will be their last chance. The Apple Store is a 1hr 30min round trip from my home, something I’ll probably have to do <em>twice</em> — once to find out <em>MBP #4</em> rattles too, and again to collect it after it’s been repaired.</p>
<p>Here’s a timeline, for brevity:</p>
<table>
<tr>
<td><strong>2016-12-17</strong></td>
<td>First call to Apple about <em>MBP #1</em>.</td>
</tr>
<tr>
<td><strong>2017-01-02</strong></td>
<td>
<em>MBP #2</em> arrives.</td>
</tr>
<tr>
<td><strong>2017-01-02</strong></td>
<td>Call to Apple about <em>MBP #2</em>.</td>
</tr>
<tr>
<td><strong>2017-01-04</strong></td>
<td>
<em>MBP #2</em> is collected for return to Apple, <em>MBP #3</em> is ordered. </td>
</tr>
<tr>
<td><strong>2017-01-09</strong></td>
<td>
<em>MBP #3</em> leaves China.</td>
</tr>
<tr>
<td><strong>2017-01-13</strong></td>
<td>
<em>MBP #3</em> arrives.</td>
</tr>
<tr>
<td><strong>2017-01-14</strong></td>
<td>"Excuse me, but can I rattle your MacBooks?" at the Apple Store.</td>
</tr>
<tr>
<td><strong>2017-01-16</strong></td>
<td>Call Apple, <em>MBP #4</em> is ordered for delivery to the Apple Store.</td>
</tr>
</table>
<p>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 <em>ludicrous</em> 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.</p>
<p>The interesting question comes if <em>MBP #4</em> 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 <em>really</em> 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?</p>
<p>In the words of the greats: I’m not angry, I’m just disappointed. Maybe I should develop for Windows Phone instead.</p>
tag:ikennd.ac,2016-10-28:/blog/2016/10/launching-cascable-2/Launching Cascable 2.02016-10-28T18:00:00Z2016-10-28T18:00:00Z<p><em>Cascable</em> 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 <em>Secret Diary of a Side Project</em> series of posts, the first one of which <a href="http://ikennd.ac/blog/2014/12/secret-diary-of-a-side-project-intro/">can be found here</a>.</p>
<hr>
<p>“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.</p>
<p>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.</p>
<h2 id="getting-to-20">Getting to 2.0</h2>
<p>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:</p>
<blockquote class="twitter-tweet" data-lang="en-gb">
<p lang="en" dir="ltr">Autumn is in full swing and it’s exciting times here at Cascable as the road to multi-manufacturer support begins! <a href="https://t.co/NFFAmyyghs">pic.twitter.com/NFFAmyyghs</a></p>— Cascable (@CascableApp) <a href="https://twitter.com/CascableApp/status/657541898995937281">23 October 2015</a>
</blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Nine months is a <em>very</em> 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.</p>
<p>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.</p>
<p>In the weeks before the launch, I didn’t <em>feel</em> 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.</p>
<p><em>Plus</em>, this time I had help in the form of <a href="http://ikennd.ac/blog/2016/04/secret-diary-of-a-side-project-part-7/">Tim</a>, 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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p><em>“You realise that you’ll <strong>immediately</strong> get people downloading it without looking then leaving you one-star reviews because it isn’t Instagram right?”</em>, said one.</p>
<p>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.</p>
<h2 id="launch-day">Launch Day</h2>
<p>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!</p>
<p class="center"><img src="/pictures/releasing-cascable-2/iTCHistory.png" width="350"> <br>
<em>This is not the history of a smooth release process!</em></p>
<p>I clicked the “Release” button, and braced myself for a horrible week.</p>
<blockquote class="twitter-tweet" data-lang="en-gb">
<p lang="en" dir="ltr">Launching an app is crazy - what a week! On Wednesday I was having to forcefully take breaks to keep the stress levels under control. 😰</p>— Daniel Kennett (@iKenndac) <a href="https://twitter.com/iKenndac/status/764185288411414528">12 August 2016</a>
</blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>But, the avalanche never came. Instead, we got great coverage, a big pile of downloads and some <a href="http://www.photographyblog.com/reviews/cascable_review/">really positive reviews</a>.</p>
<p>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.</p>
<p>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!</p>
<blockquote class="twitter-tweet" data-conversation="none" data-lang="en-gb">
<p lang="en" dir="ltr">Two days later, I’m bowing out the week with a great review and coverage on one of the largest photography sites there is. Much better! 😀</p>— Daniel Kennett (@iKenndac) <a href="https://twitter.com/iKenndac/status/764185315942727680">12 August 2016</a>
</blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>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.</p>
<p>So, with all of that self-congratulation out of the way, let’s look at some cold, hard data!</p>
<h2 id="how-did-the-launch-actually-go">How did the launch actually go?</h2>
<p>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 <a href="https://medium.com/swlh/how-our-app-went-from-20-000-day-to-2-day-in-revenue-d6892a2801bf#.6b3r10ffq">can be fatal</a>.</p>
<p>Our spike followed normal trends. Here’s our downloads over the first few days of 2.0:</p>
<p class="center"><img src="/pictures/releasing-cascable-2/Cascable20Downloads.png" width="725"> <br>
<em>Downloads during the launch.</em></p>
<p>However, if we compare that to the number of purchases over the same period, a couple of things stick out:</p>
<p class="center"><img src="/pictures/releasing-cascable-2/Cascable20Sales.png" width="725"> <br>
<em>Purchases during the launch.</em></p>
<p>First, the spike for purchases was a couple of days <em>after</em> 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.</p>
<h2 id="was-switching-to-freemium-the-right-thing-to-do">Was switching to Freemium the right thing to do?</h2>
<p>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 <em>huge</em> 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.</p>
<p>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!</p>
<p>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 <em>four</em> separate In-App Purchases, as follows:</p>
<table class="alt">
<tr>
<th>Product</th>
<th>Cost</th>
<th>Description</th>
</tr>
<tr>
<td>Cascable Pro: Photo Management</td>
<td>$10</td>
<td>Support for RAW images, bulk copying, filtering and searching, image editing.</td>
</tr>
<tr>
<td>Cascable Pro: Remote Control</td>
<td>$10</td>
<td>Powerful camera remote control and shot automation tools.</td>
</tr>
<tr>
<td>Cascable Pro: Photo Management</td>
<td>$10</td>
<td>Support for RAW images, bulk copying, filtering and searching, image editing.</td>
</tr>
<tr>
<td>Cascable Pro: Night Mode</td>
<td>$10</td>
<td>A dark theme for the app.</td>
</tr>
<tr>
<td>Cascable Pro: Full Bundle</td>
<td>$25</td>
<td>All of the above.</td>
</tr>
</table>
<p>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 <a href="https://cascable.se/help/pro-features/">support article with a big-ass table</a> to explain it all was necessary.</p>
<p>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.</p>
<p>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.</p>
<p>Here’s a typical flow. Feel free to <a href="https://itunes.apple.com/us/app/cascable-wifi-camera-remote/id974193500?ls=1&mt=8&at=1010l4JU&ct=homepage">download Cascable</a> and follow along!</p>
<p>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.</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/1-PhotosStart.jpg" width="768"></p>
<p>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:</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/2-PhotosProPrompt.jpg" width="768"></p>
<p>In some places, particularly in lists, we place a “Pro” button in place of the switch or button that would invoke a particular feature:</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/2a-NightModeProPrompt.png" width="438"></p>
<p>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.</p>
<p class="center"><img src="/pictures/releasing-cascable-2/3-StoreIndividual.png" width="571"></p>
<p>If the user attempts to purchase the presented In-App Purchase, they’ll be presented with this dialog:</p>
<p class="center"><img src="/pictures/releasing-cascable-2/4-StoreBundlePrompt.png" width="571"></p>
<p>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.</p>
<p class="center"><img src="/pictures/releasing-cascable-2/5-StoreBundle.jpg" width="571"></p>
<p>Finally, once the user has purchased the unlock for a feature, the original message is replaced with controls for the feature itself.</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/6-PhotosProFeature.jpg" width="768"></p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/6a-NightModeProFeature.png" width="438"></p>
<p>As you can see, even though payment and billing logic is provided by the App Store infrastructure, there’s still a <em>ton</em> of work to do if you want to provide a somewhat rich In-App Purchase experience for your users. Which you <em>do</em> want to do — that little “Give me money!” button is difficult for users to tap!</p>
<p>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):</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/7-Purchases.png" width="438"></p>
<p>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.</p>
<h2 id="so-does-our-store-work">So, does our store work?</h2>
<p>The following data is taken from a five week period during that long tail after the big spike.</p>
<p>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.</p>
<p>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.</p>
<p>Overall, our paid:free ratio is about 20%, which I don’t feel is too bad.</p>
<h2 id="does-our-upsell-work">Does our upsell work?</h2>
<p>This graph shows the <em>Entry Point</em> 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 <em>reasonably</em> 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.</p>
<p class="center"><img src="/pictures/releasing-cascable-2/StoreShown.png" width="680"> <br>
<em>In-App Store entry point by product over five weeks during our long tail.</em></p>
<p>This next graph shows the products purchased over the same period. As you can see, the Full Bundle <em>significantly</em> 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.</p>
<p class="center"><img src="/pictures/releasing-cascable-2/ProductPurchased.png" width="680"> <br>
<em>In-App Store purchases by product over five weeks during our long tail.</em></p>
<p>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:</p>
<p>1) What if we still had four separate In-App Purchases at the same prices, but without the upsell from the $10 ones?</p>
<p>2) What if there was only one $25 In-App Purchase as originally planned?</p>
<p>However, my <em>feeling</em> 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.</p>
<h2 id="what-next">What Next?</h2>
<p>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.</p>
<p>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.</p>
<p>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!! <em>codes harder</em>) and will be putting effort into marketing the iOS app we have.</p>
<p>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.</p>
<p>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.</p>
<h3 id="first-approach-get-more-people-to-use-cascable">First Approach: Get more people to use Cascable</h3>
<p>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.</p>
<p>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.</p>
<h3 id="second-approach-get-more-people-to-convert-to-paid-users">Second Approach: Get more people to convert to paid users</h3>
<p>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.</p>
<p class="center tight-border"><img src="/pictures/releasing-cascable-2/Announcements.png" width="1024"></p>
<p>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.</p>
<p>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!”.</p>
<p>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.</p>
<h3 id="third-approach-dont-put-all-our-eggs-in-the-ios-basket">Third Approach: Don’t put all our eggs in the iOS basket</h3>
<p>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.</p>
<p>In a <a href="http://ikennd.ac/blog/2015/01/secret-diary-of-a-side-project-part-2/">previous <em>Secret Diary of a Side Project</em> post</a>, 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.</p>
<p>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 <em>incredibly</em> 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.</p>
<p class="center no-border"><img src="/pictures/releasing-cascable-2/TransferPrototype.png" width="700"></p>
<h2 id="conclusion">Conclusion</h2>
<p>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 <em>better</em> 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”!</p>
<p>As always, feel free to get in touch with me <a href="http://twitter.com/iKenndac">on Twitter</a>.</p>
tag:ikennd.ac,2016-04-12:/blog/2016/04/secret-diary-of-a-side-project-part-7/Secret Diary of a Side Project: No Longer Alone2016-04-12T14:30:00Z2016-04-12T14:30:00Z<p><em><strong>Secret Diary of a Side Project</strong> 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 <a href="/blog/2014/12/secret-diary-of-a-side-project-intro/">here</a>.</em></p>
<hr>
<p>It’s been nearly ten months since <a href="/blog/2015/06/secret-diary-of-a-side-project-part-6/">my last Secret Diary post</a>, and since then I’ve been doing nothing but keeping my head down and plodding along:</p>
<ul>
<li>First, I shipped a couple of bugfix updates.</li>
<li>In August 2015, I released a feature update that added some powerful new stuff.</li>
<li>In September 2015, I released a feature update that added support for some new platform goodies — WatchOS 2 and iOS 9 split screen.</li>
</ul>
<p>Other than a couple of minor bugfix updates, there’s been nothing new released since then. So, what’s going on?</p>
<h2 id="crossroads">Crossroads</h2>
<p>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.</p>
<p>That said, the people who do buy Cascable seem to love it. I’ve had some <a href="http://dustinabbott.net/2015/11/killer-apps-cascable-wi-fi-camera-remote/">great reviews</a> and many lovely emails from happy users.</p>
<p>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?</p>
<p>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:</p>
<ol>
<li>
<p>Carry on by my lonesome for three years.</p>
</li>
<li>
<p>Hire someone for one year.</p>
</li>
</ol>
<p>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 <em>severely</em> 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.</p>
<h2 id="employee-1">Employee #1</h2>
<p>As of last week, Cascable has employees! <a href="http://twitter.com/timkitchener/">Tim</a> is Cascable’s <em>Head of Stuff That Isn’t Programming</em>, 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 <em>super</em> important for a successful business (marketing, product direction, pricing, etc etc).</p>
<p>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 <em>Secret Diary</em> post I write - thinking about Cascable as a “side project” is completely inappropriate now other people are involved.</p>
<p>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!</p>
tag:ikennd.ac,2016-01-03:/blog/2016/01/garmin-virb-xe-review-updated/Garmin VIRB XE Review Updated2016-01-03T17:10:00Z2016-01-03T17:10:00Z<p>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.</p>
<p>As such, I’ve updated my review to reflect what the camera is like in early 2016. Spoiler: It’s better!</p>
<div class="video-container">
<video class="center" style="overflow:hidden; max-width: 640px;" autoplay="" loop="">
<source src="/pictures/virb-review/gforces-compare.mp4" type="video/mp4"></source>
Your browser does not support the video tag.
</video>
</div>
<p> </p>
<p><a href="/blog/2015/08/garmin-virb-xe-review/">You can find my full and updated review here</a>.</p>
tag:ikennd.ac,2015-12-06:/blog/2015/12/sprucing-up-indoor-training-with-simulated-power-data/Sprucing Up Indoor Training with Simulated Power Data2015-12-06T16:15:00Z2015-12-06T16:15:00Z<p>The clocks have gone back and the nights are closing in. Here in Sweden, it’s already dark by 3:30pm!</p>
<p>The dark, more than the cold, severely dampens my enthusiasm for cycling in the evenings after work — the lovely path along the edge of the lake becomes a harrowing edge over a black nothingness.</p>
<p>So, it’s time to bring the evening rides indoors. I’m not a fan of regular exercise bikes – you have to spend <em>silly</em> money to get a decent one, and then you get some weird geometry. I already have a <em>great</em> bike that’s been perfectly set up over a period of time to provide the correct geometry for my body. Why can’t I use that?</p>
<p>Thankfully, there are <em>stationary trainers</em> that let you do just that. I have a Kurt Kinetic <a href="https://kurtkinetic.com/products/kinetic-rock-and-roll-smart/">Rock and Roll Smart</a> stationary trainer — it has a built-in Bluetooth power meter so I can manage workouts on my phone, and is built to allow side-to-side motion of the bike. Not only does this simulate real riding better, it allows the lateral forces I put through the bike to be absorbed by the spring in the trainer and not my rear wheel’s axle and rear triangle, putting to rest fears of stressing parts of the bike that don’t normally take those sort of forces.</p>
<p class="center"><img src="/pictures/simulated-power/rock-and-roll.jpg" width="800"></p>
<p>Anyway! I’m all set up — this is gonna be just like riding outside!</p>
<p>…Oh.</p>
<p class="center"><img src="/pictures/simulated-power/view-no-video.jpg" width="800"></p>
<p>Well, that’s boring. Why don’t I record a video of my ride to play back while I’m training? And if I’m doing that… it’d be great if I can overlay some data so I can match my pacing to the ride on the video. I use a <a href="/blog/2015/08/garmin-virb-xe-review/">Garmin VIRB XE</a> camera, the software for which can import the data from my Garmin GPS to overlay my heart rate, speed, pedalling cadence and more over the video. This sounds perfect!</p>
<p>Unfortunately, this is where we hit a snag. The trainer I have has a “fluid” resistance unit, which ramps up resistance with speed — when I pedal fast in a high gear it’s difficult, and when I pedal slowly it’s easy. This sounds sensible enough until you realise that the hardest parts of my ride are up steep hills on off-road trails — I’m putting a ton of power down, but I’m travelling really quite slowly. This means that overlaying speed data onto my video is useless since the trainer is basically simulating a perfectly level road. What I need to overlay on my video is a readout of the actual <em>power</em> I’m putting out at any given moment.</p>
<p class="center"><img src="/pictures/simulated-power/horse-hill.jpg" width="800"> <br>
<em>I’m doing 5km/h here, but outputting nearly 300W. 5km/h on my trainer gives an almost negligible power output.</em></p>
<p>After a weekend of mucking around with several <em>horrible</em> looking programs, I finally managed to get a simulated-but-accurate-enough power figure into Garmin’s software, allowing me to overlay power output onto my video:</p>
<p class="center"><img src="/pictures/simulated-power/finished-video-framegrab.jpg"></p>
<p>Now when riding indoors I can put my iPad and iPhone on a music stand (make sure you get a sturdy one!) and reproduce my outdoor ride by matching my live power output on the trainer to the one displayed in the video.</p>
<p>I <em>love</em> this method of training. It gives me something to look at while riding, and because it’s realtime from <em>my</em> ride, I get great pacing — it’s on local trails I know and ride frequently, and when I need rest stops, I’m already stopping to rest on the video.</p>
<h2 id="producing-simulated-power-data">Producing Simulated Power Data</h2>
<p>So, how to we get that live power overlay?</p>
<p>The easiest option would be to buy an actual power meter for my bike. Most of them are designed for road bikes, and <em>all</em> of them are expensive — you’re looking at towards $1,000, which is a bit spendy for a project like this.</p>
<p>So, with that out, we need to simulate our power data. I use the popular site <a href="http://strava.com">Strava</a> to track my rides, and they provide a pretty decent-looking “simulated” power graph for each ride:</p>
<p class="center"><img src="/pictures/simulated-power/strava-estd-power.png" width="820"></p>
<p>Annoyingly, though, there’s absolutely no way to get this data <em>out</em> of Strava in any meaningful way, so that’s out. Garmin’s similar service, Garmin Connect, doesn’t produce this data at <em>all</em>, so that’s out too.</p>
<p>Looks like we’re going to have to do this manually!</p>
<h3 id="ingredients">Ingredients</h3>
<ul>
<li>A video recording of a bike ride.</li>
<li>Some recorded telemetry data from that same ride, such as from a GPS unit.</li>
<li>
<a href="http://www.goldencheetah.org">GoldenCheetah</a>, an open-source data management application.</li>
<li>
<a href="http://ikennd.ac/fitness-converter/">Fitness Converter</a>, a free application by yours truly for converting fitness files between formats.</li>
<li>
<a href="http://www.garmin.com/en-US/shop/downloads/virb-edit">Garmin VIRB Edit</a>, a free video editor that can overlay data onto your video.</li>
</ul>
<h3 id="method">Method</h3>
<p>First, we’re going to load our recorded telemetry data (heart rate, speed, pedalling cadence, etc) from the GPS into <a href="http://www.goldencheetah.org">GoldenCheetah</a>, a piece of software for working this this sort of thing. Once imported, clicking the “Ride” tab should show graphs of your data:</p>
<p class="center no-border"><img src="/pictures/simulated-power/gc-original-data.png" width="976"></p>
<p><strong>Note</strong>: On the first launch, GoldenCheetah will ask you to set up a profile. You need to enter an accurate weight for you and your bike to get accurate power data.</p>
<p>Next, choose “Estimate Power Values…” from the Edit menu. Once you complete the process, you’ll see more graphs added to your data, including a “Power” graph. If you have other data to compare to, such as Strava’s Simulated Power graph, you can compare them, and if GoldenCheetah’s data is significantly wrong you can choose “Adjust Power Values…” from the Edit to move it all up or down.</p>
<p class="center no-border"><img src="/pictures/simulated-power/gc-with-power.png" width="976"></p>
<p>Finally, choose “Export…” from the Activity menu to export the file as a TCX file.</p>
<p>Unfortunately, we’re not quite there — Garmin’s software can’t import TCX files, so we need to convert our new file to the FIT format. The best pre-existing solution I could find for this was really quite terrible, so I ended up writing my own (as you do): <a href="http://ikennd.ac/fitness-converter/">Fitness Converter</a>.</p>
<p class="center no-border"><img src="/pictures/simulated-power/fitness-converter.png" width="773"></p>
<p>Once the data is in the FIT format, we can import it into VIRB Edit. Since the VIRB XE camera has GPS in it, it has the accuracy to automatically sync the data from my GPS unit (now with added power data!) perfectly. If you’re not in this position, you can manually sync your data file to the video.</p>
<p>…aaand, we’re done. You can now add your graphs and overlays as you wish using VIRB Edit. Since speed is completely irrelevant in this instance, I leave all that out and just have a single giant power bar — it’s easy to read when working out over a constantly changing number.</p>
<p>Happy training!</p>
<p class="center"><img src="/pictures/simulated-power/view-with-video.jpg" width="800"> <br>
<em>Next training spend: a bigger screen!</em></p>
tag:ikennd.ac,2015-08-16:/blog/2015/08/garmin-virb-xe-review/Garmin VIRB XE for Automotive and Track Days: A First Impressions Review2015-08-16T17:30:00Z2015-08-16T17:30:00Z<p><strong>Update January 2016:</strong> I’ve updated this review to reflect the camera and its software after a few months and a few software updates. Happily, it’s pretty much all positive. Parts of the review that are now incorrect are still here but <del>are struck through</del> so you can see what’s changed.</p>
<p><strong>Note:</strong> For the first part of this review, I’m going to ramble on a bit about my history with this sort of thing and why I’m <em>so</em> hopeful that the VIRB XE isn’t crappy for use on track days. If you don’t care, you can <a href="#virb-xe-review-start">scroll down a bit</a> to get to the real review.</p>
<h2 id="we-were-totally-ahead-of-the-times-man">We were totally ahead of the times, man!</h2>
<p>I’ve always loved cars and driving. As <em>soon</em> as I had a car more interesting than my Mum’s 1.2L Vauxhall Corsa (SXi!) I started going on track days. As my skills and enjoyment grew I wanted to record videos of my driving to show my friends and catalogue my improvement over time, so I started to record my track driving.</p>
<p>But! Without data, track driving videos are <em>boring</em>. Check out this <a href="https://www.youtube.com/watch?v=JOrR4EX8NuM">recent one of mine</a> — even if you’re a car nut, I bet you won’t make it through more than a lap or two before getting bored.</p>
<p>Back in 2007 I was bored of my dataless videos, and as part of my final year at university, I wrote a prototype Mac application to add graphical overlays to my track day videos. It was just a prototype, but it worked great and I was really proud of what I’d made — enough that it still gets a space in my <a href="/about/">abbreviated life history</a>.</p>
<p>However, while the software was ready, the hardware for <em>gathering</em> the data just wasn’t there. iPhones and iPads were just beginning to arrive, and the other smartphone platforms at the time weren’t quite suitable. In particular, the Windows Mobile devices used at the time didn’t have accurate enough clocks to reliably time the data, warranting a whole section in my dissertation discussing interpolating timestamps.</p>
<p>In 2007, no camera came close to the tiny action cameras of today (particularly in the consumer space) so I ended up using a HDV camcorder strapped into the car.</p>
<p class="center"><img src="/pictures/virb-review/mx5-camcorder.jpg"></p>
<p>For recording data from the car I used a reasonably high-end (in the consumer space) OBD to Serial dongle that was advertised as being “high speed”. It read data from the CAN bus of my car at roughly 5Hz, which meant if you wanted to record multiple properties at once, you rapidly lost nuance in your data.</p>
<p>Since there was nothing like the iPad back then, I ended up using a tablet PC designed for outdoor use - it had a digital pen for input, and a special display that was readable outdoors and terrible everywhere else. This thing ran full-blown Windows XP and cost a <em>fortune</em>.</p>
<p>I had well over £3,000/$4,500 worth of big, heavy equipment. Here’s an example of what all that would get you when combined with my prototype software:</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/GKXdBraWnzI" frameborder="0" allowfullscreen=""></iframe>
</div>
<p> </p>
<p>Perfectly acceptable (despite the hilariously slow data acquisition rate), but I ended up abandoning the project. Strapping all that stuff into your car was just not fun, and the marshals at most track days I went to weren’t desperately happy with the thought of that amount of stuff flying around the car if I crashed. Compare the photos above with my equipment list below and you’ll see just how far we’ve come!</p>
<p><a name="virb-xe-review-start"></a></p>
<h2 id="virb-xe-the-review">VIRB XE: The Review</h2>
<p>This review focuses on the experience the VIRB XE gives when using it to create driving videos, typically on a track day or on a road trip. As well as the camera itself, I’ll be using it with the following equipment:</p>
<ul>
<li>An OBDLink LX — a Bluetooth OBD dongle for interfacing with the car.</li>
<li>A Raceseng Tug View — a tow hook with an integrated GoPro mount.</li>
<li>An Audio-Technica ATR3350 microphone and Zoom H1 audio recorder.</li>
</ul>
<p class="center"><img src="/pictures/virb-review/equipment.jpg" width="800"></p>
<p class="center"><img src="/pictures/virb-review/virb-xe-mounted.jpg" width="800"> <br>
<em>The camera is attached to the front of my car (along with a lot of bugs!) using the Tug View.</em></p>
<h3 id="a-note-on-audio">A Note On Audio</h3>
<p>Garmin claims their microphone “…records clean and clear audio that cameras in cases just can’t pick up”, which is an implied bash at GoPro, I suppose. While that may be true, the interesting noises from a car come from under the bonnet or out the back, neither of which are interesting places for a camera. Therefore, this review won’t deal with sound quality.</p>
<p>That said, my video explaining how to get good sound quality from your car on a track day does use the VIRB XE for the clips at the end, so if you’re an expert on what wind noise should sound like, go nuts!</p>
<div class="iframe-16x9-container">
<iframe class="iframe-16x9" src="https://www.youtube.com/embed/t_9u5CMjZYM" frameborder="0" allowfullscreen=""></iframe>
</div>
<p> </p>
<h3 id="a-note-on-video-quality">A Note On Video Quality</h3>
<p>I’m not going to directly compare video quality to other cameras either — I don’t have the skill set to do a good job of it. The video quality seems great, though, and the camera does an admirable job in difficult autoexposure situations, like driving through a shady forest on a sunny day.</p>
<h3 id="pre-impressions">Pre… Impressions…?</h3>
<p>Garmin, I’m going to level with you: paper launches <em>suck</em>. This camera was announced in April and I was <em>super</em> excited about it, thrusting cash at my computer screen with the enthusiasm of a kid in a candy store. And then you said “summer”, and my enthusiasm waned. I went to a track day in August (<em>firmly</em> in “summer”) and the camera still wasn’t available. “Garmin suck!” I found myself saying to my friend, grumpy that I was still waiting for the camera.</p>
<p>That’s a pretty negative feeling to come back from.</p>
<h3 id="first-impressions">First Impressions</h3>
<p>This review is going to compare to the GoPro a <em>lot</em>. They’re the de-facto standard in this space, and I’ve been using them for years. They have a huge amount of momentum, but I’ve actually been falling out of love with them for a little while. They’ve always been a bit fiddly, but silly design decisions like that stupid port cover and a flimsy USB connector that’s soldered (poorly, in one of mine) to the mainboard make it feel fragile, which is exactly the opposite of what you want in an outdoor action camera.</p>
<p>Within seconds of pulling the VIRB XE out of its box, you realise it’s different. After a couple of minutes, you get the feeling that it’s been designed with care for its intended environment — dropping off my bike into a muddy puddle.</p>
<p>The whole thing is really well put together. A few particular details stand out for me:</p>
<p class="center"><img src="/pictures/virb-review/virb-xe-record-switch.jpg" width="800"> <br>
<em>Easy to push buttons and the big chunky “record” switch and great to use with gloves on.</em></p>
<p class="center"><img src="/pictures/virb-review/virb-xe-screen.jpg" width="800"> <br>
<em>The screen is lovely and clear compared to that of the GoPro.</em></p>
<p class="center"><img src="/pictures/virb-review/virb-xe-moisture-tray.jpg" width="800"> <br>
<em>A little tray holds inserts that absorb moisture to prevent the camera from fogging. The inserts are reusable and four are included in the box (one of which I promptly lost because they’re small and I’m stupid).</em></p>
<p class="center"><img src="/pictures/virb-review/virb-xe-port.jpg" width="800"> <br>
<em>All electronic interfacing is done using this external set of pins. No female ports means no ports have load-bearing flimsy soldering, no holes for water to get in, and no stupid port cover.</em></p>
<p class="center"><img src="/pictures/virb-review/virb-xe-gopro-suction-cup.jpg" width="800"> <br>
<em>Sensibly, they’ve accepted that GoPro currently rule the roost in the market and the camera is directly compatible with the GoPro ecosystem of mounts.</em></p>
<p>However! It’s not all perfect.</p>
<p>A very minor niggle is that the “Menu” button on mine feels a bit weird. You feel it click when you push it, but nothing happens. You need to push a tiny bit harder to get the button to register.</p>
<p>A much less minor niggle is the cable connecting mechanism. The cable snaps on using a very rugged connector (which is great), but when I pick the camera up it disconnects as if I’d unplugged it. I can repeat this with 100% repeatability with my camera and cable, which is quite worrying. Randomly disconnecting is a great way to corrupt the filesystem. Sure, I can work around that by taking the SD card out and using a card reader, but what happens if my dog bumps my desk during a firmware update?</p>
<p><del>Hopefully, this is just a niggle with my particular camera. I’ll contact Garmin about it and update this review with their reply.</del></p>
<p><strong>Update January 2016:</strong> The weird menu button isn’t unique to my camera. There are theories on the Garmin forums that it’s actually a half-full button like the shutter button on a camera, and there’s nothing yet assigned to a half press. Garmin’s response was that the camera was acting as normal. I haven’t actually used the cable again since this review, and I haven’t pursued it further.</p>
<h3 id="recording-a-car-video">Recording a Car Video</h3>
<p>During setup, the camera created a WiFi network and paired with my iPhone perfectly, and the camera allows you to customise its SSID and password on-screen.</p>
<p class="center"><img src="/pictures/virb-review/virb-xe-wifi.jpg" width="800"></p>
<p>Next, I connected it to my OBDLink LX. It took a few clicks of the “Scan” option in the VIRB’s Bluetooth settings before it saw my OBD dongle, but once it found it the two paired instantly. While the camera was adamant it was connected to my car, the VIRB App on my iPhone reported “No connected sensors”. Thankfully the camera was right, and the data from my car was recorded perfectly. Hopefully the glitch in the app will be fixed.</p>
<p class="center"><img src="/pictures/virb-review/virb-app-no-bluetooth.png" width="375"></p>
<p>I attached the camera to the front mount on my car, started my audio recorder then used the VIRB app to start the camera from my iPhone. After a little beep of the horn (for syncing my separate audio recording with the video), I set off for a 25-minute drive around a local lake.</p>
<p>Once home, I was able to connect to the camera using my phone and stop recording. Everything appeared to have worked just fine.</p>
<h3 id="editing-a-car-video">Editing a Car Video</h3>
<p>This is where I’m ready to be let down. I wrote the app I wanted (well, a prototype of it) eight years ago, and nothing has come close since. Like the bride who’s been planning her wedding since she was a small girl, reality can never quite match up to expectation. Nobody will write the app <em>I</em> want.</p>
<h4 id="data-and-gauges">Data and Gauges</h4>
<p>Expectations lowered, I fire up VIRB Edit for the first time and import the recording straight from the camera.</p>
<p class="center no-border"><img src="/pictures/virb-review/virb-edit-first-impression.png" width="731"></p>
<p>Holy crap. With zero effort I have a full set of data <em>and</em> a map synced to my drive. This is wonderful!</p>
<p>The quality of the recorded data by the VIRB seems great — the OBD data came out perfectly despite there being a couple of metres and an engine between the camera and the Bluetooth OBD adapter, and the application managed to handle the device losing a GPS fix for a few seconds with grace, resulting in a slightly funny-looking map (bottom left of the map in the screenshot above — the road isn’t that square) but no other problems.</p>
<p><del>However, the data is a bit <em>too</em> perfect, and the app seems too trusting of it. In particular, G-forces. With the camera directly bolted to my car’s chassis, the camera’s internal accelerometer seems to pick up every tiny little vibration, which VIRB Edit displays without filtering as this example from a perfectly smooth road shows:</del></p>
<p><strong>Update January 2016:</strong> I’m happy to report that this problem has been <em>completely</em> fixed with firmware 3.70, released in early December 2015. I was concerned that the vibrations from being directly bolted to my car with a metal mount would be too much to overcome, but with the firmware update the G-force data from the VIRB is lovely and smooth, and picks up gentle curves and speed changes just fine. You can see a before (left) and after (right) comparison below:</p>
<div class="video-container">
<video class="center" style="overflow:hidden; max-width: 640px;" autoplay="" loop="">
<source src="/pictures/virb-review/gforces-compare.mp4" type="video/mp4"></source>
Your browser does not support the video tag.
</video>
</div>
<p> </p>
<p><del>It’d be nice if there was an option to have the application perform a low-pass filter on the data. This would reduce the responsiveness of the data slightly, but my 1,200kg car isn’t changing direction fast enough in any axis to make that a huge problem.</del></p>
<p>VIRB Edit comes with a number of templates which work great, and a lot of individual gauges that you can customise the colours of to create your own layouts and styles.</p>
<p>If that’s not enough, you can create your own gauges and edit them, which is a superb feature to have for power users — I plan to make gauges in VIRB Edit to match the ones in my car, and I bet others will do that same.</p>
<p class="center no-border"><img src="/pictures/virb-review/gauge-editor.png" width="648"></p>
<h4 id="video-editing">Video Editing</h4>
<p>VIRB Edit is a basic, newbie-friendly video editing application, and the features it does have work well, although I did notice a little audio hiccup during playback when two sequential clips (the camera splits recordings into fifteen-minute chunks) are placed together.</p>
<p>There are a number of features I need to produce my track day videos that VIRB Edit doesn’t have:</p>
<ul>
<li>The ability to import a separate audio track (from my audio recorder) and precisely sync it (and keep it synced) with the audio track of the video.</li>
<li>The ability to rotate the video slightly when I mount the camera slightly off-level.</li>
</ul>
<p>Now, I’m not saying Garmin should implement all these features — that’d be silly given the number of video editors already out there at any price range you can mention. Normally, I’d just import my video into my editor of choice and edit to my heart’s content. However, the addition of data overlays makes that problematic — if I add my data overlays in VIRB Edit then export for further editing, a number of problems occur:</p>
<ul>
<li>An extra layer of encoding has happened, reducing the quality of the video.</li>
<li>The gauges are baked into the video, meaning any rotations, colour corrections, etc will be applied to them as well.</li>
</ul>
<p>I could go the other way — import the raw video into my editor of choice, apply corrections, merge in the better audio, etc, but you still end up with an extra encoding step that reduces quality.</p>
<p><del>Solving this is actually relatively easy, and my prototype application from years ago had this built-in: several video formats and containers support videos with alpha channels. What I’d love to do is add my data overlays in VIRB Edit then export a lossless video containing <em>only</em> the overlays on a transparent canvas. This way, I could import the original video and the overlays into my editor of choice and keep them in separate tracks, allowing me to apply rotations and colour corrections to the video to my heart’s content. Bonus points for being able to export each overlay separately, allowing the sweet animations seen in Garmin’s own VIRB XE promotional video!</del></p>
<p><strong>Update January 2016:</strong> I’m not sure if I was just being dumb when I wrote this review, but I’ve recently found an option in VIRB Edit’s preferences: <em>Export transparent PNG sequence for overlays</em>. This does exactly what it says on the tin, and after exporting a video it’ll separately export a sequence of transparent PNGs containing only the overlays. Apple’s Motion editing software picked this sequence up directly with no further action needed on my part. The only minor downside to this is that you’ll have one PNG sequence containing every single overlay, which is less useful if you want to animate them independently. This can be worked around, though, by exporting multiple times with one overlay at a time. The minor downside to <em>that</em> approach, though, is that there’s no option to export <em>only</em> the overlay PNG sequence, so you have to re-export the video itself as well. This can become a lengthy process!</p>
<h3 id="hail-to-the-power-user">Hail To The Power User</h3>
<p>One thing I’d like to call out about this product that won’t be talked about in most reviews is Garmin’s attitude towards advanced/power users. Many companies lock away the inner workings of their products in what often turns out to be a futile effort as users tend to reverse-engineer the fun stuff anyway. GoPro’s WiFi protocol has been <a href="https://github.com/KonradIT/goprowifihack">mostly reverse-engineered</a>, for instance, and there are a wide number of GoPro “<a href="http://hackaday.com/2014/06/20/pwn-your-gopro-scripting-wifi-and-bus-hacking/">hacks</a>” (which turn out to mostly be undocumented config files) to enable features like long exposures.</p>
<p>Garmin, on the other hand, publishes documentation for controlling their VIRB cameras on their own <a href="http://developer.garmin.com/virb/overview">VIRB Developer site</a>, and VIRB Edit has an “Advanced Editing” button on its already pretty advanced gauge editor which opens up a JSON file in your favourite text editor alongside a PDF documenting the file format.</p>
<p>For most users, this means nothing. However, I <em>love</em> this attitude — I can customise my gauges to my heart’s content and write little apps to control my camera if I want, all using tools provided to me by Garmin.</p>
<h3 id="conclusion">Conclusion</h3>
<h4 id="short-version">Short Version</h4>
<p>I’ve already - and I’m not joking - sold all of my GoPro cameras.</p>
<h4 id="long-version">Long Version</h4>
<p>I bought this camera within its first week of availability in Sweden, and unfortunately these days that means software niggles are to be expected. However, I’ve owned a number of Garmin devices (and still do) and they’ve a long history of continuing to improve their products over time. My four year old GPS unit still gets regular software updates, for instance. I have a very positive opinion of Garmin as a company — they make solid products and solid software, so I’m hopeful they’ll resolve the bugs I found.</p>
<p><strong>Update January 2016:</strong> I’m happy that my faith in Garmin seems to have been well placed - the more problematic software issues have been fixed by updates.</p>
<p>I <em>am</em> rather concerned about the flaky connection between the camera and its USB cable, though. This is certainly a hardware issue — I’ll contact Gamin and see what they say.</p>
<p>Overall, though, I love this camera and have already sold all my GoPros. The combination of its superb build quality and extra data acquisition features are <em>killer</em> for me, and are a joy to have after years of lacklustre GoPro updates.</p>
<h4 id="hardware">Hardware</h4>
<p><strong>Good</strong></p>
<ul>
<li>It feels like it’s built like a tank — I love the record switch in particular.</li>
<li>Lots of thought in the design — the moisture tray and port design stand out.</li>
<li>Lovely screen compared to the GoPro.</li>
<li>Paired with my OBD dongle and phone effortlessly.</li>
<li>Directly compatible with the GoPro ecosystem of mounts.</li>
</ul>
<p><strong>Bad</strong></p>
<ul>
<li>PAPER LAUNCH DAMNIT! Don’t show me a product I want then wait four months to start selling it!</li>
<li>Cable doesn’t fit snugly and disconnects when I move the camera. Hopefully this is a one-off thing.</li>
<li>One of the buttons feels weird. <del>Again, hopefully a one-off niggle.</del> <strong>May actually be as-designed. Garmin considers it ‘normal’.</strong>
</li>
<li>Proprietary cable isn’t super great when you need an emergency charge in a world of micro USB. I see why they did it and, like Apple’s Lightning, the pros outweigh the cons most of the time.</li>
<li>Only one sticker in the box. I’m prepared to go full fanboy with this thing, and I only have one sticker?!</li>
</ul>
<h4 id="software">Software</h4>
<p><strong>Good</strong></p>
<ul>
<li>Great Mac citizen — you’ve no idea how many companies ship crappy “cross-platform” desktop software.</li>
<li>Gauges functionality covers all my uses, from great looking templates through to complete and total customisability.</li>
</ul>
<p><strong>Bad (as of August 2015)</strong></p>
<ul>
<li>
<del>Accelerometer data needs a low-pass filter — it’s unusably noisy when the camera is bolted to my car’s chassis.</del> <strong>Fixed with firmware 3.70.</strong>
</li>
<li>Audio glitch when transitioning between clips that’ve been cut up by the camera.</li>
</ul>
<p><strong>Missing Features</strong></p>
<ul>
<li>
<del>Ability to export a translucent video containing only the gauges so I can edit the source video in my preferred editor and keep the data overlays clean.</del> <strong>Feature exists, but is slightly hidden. My fault!</strong>
</li>
</ul>
tag:ikennd.ac,2015-06-21:/blog/2015/06/secret-diary-of-a-side-project-part-6/Secret Diary of a Side Project: In Reality, I've Only Just Started2015-06-21T14:00:00Z2015-06-21T14:00:00Z<p><em><strong>Secret Diary of a Side Project</strong> 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 <a href="/blog/2014/12/secret-diary-of-a-side-project-intro/">here</a>.</em></p>
<hr>
<p>On March 27th 2013, I started an Xcode project called <em>EOSTalk</em> to start playing around with communicating with my new camera (a <a href="/blog/2013/03/canon-eos-6d-review/">Canon EOS 6D</a>) over its WiFi connection.</p>
<p>Over two years and 670 commits later, on June 5th 2015 (exactly a <a href="/blog/2015/02/secret-diary-of-a-side-project-part-4/">month late</a>), I uploaded Cascable 1.0 to the App Store. Ten agonising days later, it went “In Review”, and seventeen hours after that, “Pending Developer Release”.</p>
<p>Late in the evening the next day, my wife, our dog, a few Twitter friends (thanks to Periscope) and I sat together by my desk and clicked the <em>Release This Version</em> button.</p>
<iframe width="320" height="600" src="https://www.youtube.com/embed/ZQYM9kuNXAU" frameborder="0" allowfullscreen=""></iframe>
<p> </p>
<p>I absolutely meant to blog more in the three months since my last <em>Secret Diary</em> post, and I’m sorry if you’ve been looking forward to those posts. An interesting thing happened — I thought I’d have <em>way</em> more time for stuff like blogging after leaving my job and doing this fulltime, but I’ve ended up with <em>way</em> less. A strict deadline and a long issues list in JIRA made this a fulltime 9am-6pm job. So much for slacking off and playing videogames!</p>
<p>Fortunately, though, I still have a few things I want to write about and now I can slow down a bit, I should start writing here on a more frequent basis again.</p>
<h3 id="statistics">Statistics</h3>
<p>Some stats for Cascable 1.0 for the curious:</p>
<table class="alt">
<tr>
<td>Objective-C Implementation</td>
<td>124 files, 23,000 lines of code</td>
</tr>
<tr>
<td>C/Objective-C Header</td>
<td>133 files, 2,400 lines of declaration</td>
</tr>
<tr>
<td>Swift</td>
<td>None</td>
</tr>
<tr>
<td>Commits</td>
<td>670</td>
</tr>
</table>
<p>Now, lines of code is a pretty terrible metric for comparing projects, but here’s the stats for the Mac version of Music Rescue, the last app of my own creation that brought in the Benjamins:</p>
<table class="alt">
<tr>
<td>Objective-C Implementation</td>
<td>154 files, 24,000 lines of code</td>
</tr>
<tr>
<td>C/Objective-C Header</td>
<td>169 files, 4,100 lines of declaration</td>
</tr>
<tr>
<td>Swift</td>
<td>This was 2008 — I barely had Objective-C 2.0, let alone Swift!</td>
</tr>
</table>
<p>As you can see, the projects are actually of a similar size. It’s a completely meaningless comparison, but it’s interesting to me nonetheless. Back in 2008 I considered Music Rescue a pretty massive project, something I don’t think about Cascable. I guess my experience with the Spotify codebase put things in perspective.</p>
<p>You can check Cascable out <a href="http://cascable.se">here</a>. You should totally buy a copy!</p>
<h3 id="celebrating">Celebrating</h3>
<p>At <a href="/blog/2015/03/nsconference-7/">NSConference 7</a> I gave a short talk which was basically <em>Secret Diary: On Stage</em>, in which I discussed working on this project.</p>
<div class="iframe-16x9-container">
<iframe src="https://player.vimeo.com/video/124337772" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
</div>
<p> </p>
<p>In that talk, I spoke about a bottle of whiskey I have on my desk. It’s a bottle of Johnnie Walker Blue Label, and at £175 it’s by far the most expensive bottle of whiskey I’ve bought. When I bought it, I vowed it’d only be opened when a real human being that wasn’t my friend (sorry Tim!) exchanged money for my app.</p>
<p>Releasing an app is reward in itself, but there’s nothing <em>tangible</em> about it. Having that physical milestone there to urge me on really was helpful when I was on hour four of debugging a really dumb crash, for instance.</p>
<p>This weekend, that bottle was opened. It tasted like <em>glory</em>.</p>
tag:ikennd.ac,2015-05-01:/blog/2015/05/build-time-cfbundleversion-values-in-watchkit-apps/Build-Time CFBundleVersion Values in WatchKit Apps2015-05-01T21:00:00Z2015-05-01T21:00:00Z<p>When building a WatchKit app, you’ll likely encounter this error at some point:</p>
<blockquote>
<p>error: The value of CFBundleVersion in your WatchKit app’s Info.plist (1) does not match the value in your companion app’s Info.plist (2). These values are required to match.</p>
</blockquote>
<p>Easy, right? We just make sure the values match. But… what if we’re using dynamically generated bundle version numbers derived from, say, the number of commits in your git repository? Well, we just go to the WatchKit app’s target in Xcode, click the “Build Phases” tab and… oh. There isn’t one.</p>
<p>So, if we’re required to have our WatchKit app mirror the CFBundleVersion of our source app and we’re generating that CFBundleVersion at build time, what do we do? First, we wonder why this mirroring isn’t automatic. Second, we try to modify the WatchKit app’s Info.plist file from another target before realising that it screws with its code signature. Third, we come up with this horrible workaround!</p>
<h2 id="the-horrible-workaround">The Horrible Workaround</h2>
<p>The workaround is to generate a header containing definitions for your version numbers, then use Info.plist preprocessing to get them into your WatchKit app’s Info.plist file.</p>
<p>This little tutorial assumes you already have an Xcode project with a set up and working WatchKit app.</p>
<h3 id="step-1">Step 1</h3>
<p>Make a new build target, selecting the “Aggregate” target type under “Other”.</p>
<p class="center no-border"><img src="/pictures/watchkit-versions/new-aggregate-target.png" width="764"></p>
<h3 id="step-2">Step 2</h3>
<p>In that new target, create a shell script phase to generate a header file in a sensible place that contains C-style <code>#define</code> statements to define the version(s) as you see fit.</p>
<p>My example here generates two version numbers (a build number based on the number of commits in your git repo, and a “verbose” version that gives a longer description) then places the header into the build directory.</p>
<pre><code class="language-bash"><span class="nv">GIT_RELEASE_VERSION</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>describe<span class="w"> </span>--tags<span class="w"> </span>--always<span class="w"> </span>--dirty<span class="w"> </span>--long<span class="k">)</span>
<span class="nv">COMMITS</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>rev-list<span class="w"> </span>HEAD<span class="w"> </span><span class="p">|</span><span class="w"> </span>wc<span class="w"> </span>-l<span class="k">)</span>
<span class="nv">COMMITS</span><span class="o">=</span><span class="k">$((</span><span class="nv">$COMMITS</span><span class="k">))</span>
mkdir<span class="w"> </span>-p<span class="w"> </span><span class="s2">"</span><span class="nv">$BUILT_PRODUCTS_DIR</span><span class="s2">/include"</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"#define CBL_VERBOSE_VERSION </span><span class="si">${</span><span class="nv">GIT_RELEASE_VERSION</span><span class="p">#*v</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span>><span class="w"> </span><span class="s2">"</span><span class="nv">$BUILT_PRODUCTS_DIR</span><span class="s2">/include/CBLVersions.h"</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"#define CBL_BUNDLE_VERSION </span><span class="si">${</span><span class="nv">COMMITS</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span>>><span class="w"> </span><span class="s2">"</span><span class="nv">$BUILT_PRODUCTS_DIR</span><span class="s2">/include/CBLVersions.h"</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Written to </span><span class="nv">$BUILT_PRODUCTS_DIR</span><span class="s2">/include/CBLVersions.h"</span></code></pre>
<p>The file output by this script looks like this:</p>
<pre><code class="language-c"><span class="cp">#define CBL_VERBOSE_VERSION a6f5bd0-dirty</span>
<span class="cp">#define CBL_BUNDLE_VERSION 1</span></code></pre>
<p class="center"><img src="/pictures/watchkit-versions/aggregate-with-script.png" width="1060"></p>
<h3 id="step-3">Step 3</h3>
<p>Make your other targets depend on your new aggregate target by adding it to the “Target Dependencies” item in the target’s “Build Phases” tab. You can add it to all the targets that you’ll use the version numbers in, but you’ll certainly need to add it to your WatchKit Extension target.</p>
<p class="center"><img src="/pictures/watchkit-versions/dependency-setup.png" width="828"></p>
<h3 id="step-4">Step 4</h3>
<p>Xcode tries to be smart and will build your target’s dependencies in parallel by default. However, this will mean that your WatchKit app will be built at the same time as the header is being generated but aggregate target, which will often result in build failures due to the header not being available in time.</p>
<p>To fix this, edit your target’s scheme and uncheck the “Parallelize Build” box in the “Build” section. This will force Xcode to wait until the header file has been generated before moving on.</p>
<p class="center no-border"><img src="/pictures/watchkit-versions/scheme-build-options.png"></p>
<h3 id="step-5">Step 5</h3>
<p>Edit the build settings in your targets as follows:</p>
<ul>
<li>
<code>Preprocess Info.plist File</code> should be set to <code>Yes</code>.</li>
<li>
<code>Info.plist Other Preprocessor Flags</code> should be set to <code>-traditional</code>.</li>
<li>
<code>Info.plist Preprocessor Prefix File</code> should be set to wherever your generated header file has been placed. In my case, it’s <code>${CONFIGURATION_BUILD_DIR}/include/CBLVersions.h</code>.</li>
</ul>
<p class="center"><img src="/pictures/watchkit-versions/build-settings.png" width="672"></p>
<h3 id="step-6">Step 6</h3>
<p>Finally, change the values in your Info.plist files to match the keys in your generated header file. In my case, I set <code>CFBundleVersion</code> (also known as <code>Bundle Version</code> or <code>Build</code> depending on where you’re looking in Xcode) to <code>CBL_BUNDLE_VERSION</code>.</p>
<p class="center"><img src="/pictures/watchkit-versions/info-plist.png" width="644"></p>
<h3 id="step-7">Step 7</h3>
<p>Go to the Apple Bug Reporter and ask (nicely) they give us build phases back for WatchKit apps. You can dupe mine (<a href="http://www.openradar.me/radar?id=4945965354057728">Radar #20782873</a>) if you like.</p>
<h3 id="step-8">Step 8</h3>
<p class="center"><img src="/pictures/watchkit-versions/cascable.jpg"> <br>
<em>Success!</em></p>
<h2 id="conclusion">Conclusion</h2>
<p>This is horrible. We need to disable parallel builds and generate intermediate headers and all sorts of nastiness. Hopefully we’ll get build phases back for WatchKit apps soon!</p>
<p>You can download a project that implements this tutorial <a href="/pictures/watchkit-versions/Clicker.zip">here</a>.</p>