Ben Dodson

Freelance iOS, macOS, Apple Watch, and Apple TV Developer

Side Project: Stoutness

This is part of a series of blog posts in which I showcase some of the side projects I work on for my own use. As with all of my side projects, I’m not focused on perfect code or UI; it just needs to run!

Over the years I’ve been slowly tinkering with an idea I liked to call “The Morning Briefing”, a simple way to give me the information I want in the mornings such as the news, weather, and a list of my upcoming tasks. Originally this would have been read to me by Siri1 but in mid-2020 I decided instead it would be more fun to have it as a printed page; a personalised newspaper just for me2. It would have consisted of weather icons for the next 7 days, a list of my tasks I could physically check off, and then maybe some personal data from Health such as Apple Watch rings, heart rate data, and sleep averages.

I did make a start on this idea but then late last year my chiropractor told me that I desperately needed to improve my activity level. I also needed to perform the stretches I’d been assigned back in 2017 every day as I’d fallen out of the habit. The video I use for my stretches is by Straighten Up UK and whilst it’s very good there are several bits that can be fast forwarded through once you know the routine. It was also affecting my recommended videos on YouTube as I’d watch it every morning which meant YouTube kept assuming I wanted more and more stretch videos. Eventually I decided that I’d just download the video, remove the extraneous bits, and put it in a basic tvOS app. I then realised that my Morning Briefing idea might work better if it was paired along with the stretching video and thus Stoutness was born.

Tangent: Name and App Icon

As any good developer knows, half the battle is what to name your side project. Originally this was "Morning Briefing" and then became "Hummingbird" after one of the sillier stretches which you'll see shortly. The final name was eventually hit on when my wife remarked (rather cruelly) that I looked like Winnie-the-Pooh doing his stoutness exercise.

One of my favourite things about working on tvOS is the way in which parallax is used to convey focus. Naturally I wanted to have a nice icon if I was going to use it every day and so I decided to take Pooh Bear and let him move around a little. The icon is made of three layers; Pooh on top, the room (with a hole where the mirror is) in the middle, and then the reflection at the back. The result is that both of the Pooh's move opposingly as the icon is highlighted. The whole thing was taken from a very low-res screenshot and then I made judicious use of the "content aware fill" tool in Photoshop to fill in the extra bits of image I'd need such as the space behind Pooh and extending the reflection cut out. You can download a zip of the three individual layers if you're curious to see exactly how it works.

As with all of my side projects, I generally like to use them as an excuse to learn some new code technique. In this case, I mainly focused on how to mix-and-match SwiftUI with UIKit (as that seems like it may prove useful in the coming years) but I also dabbled in some Combine and with using Swift Package Manager over CocoaPods for the limited number of dependencies I use. In the end, I also needed to dabble with AppleScript, Shortcuts, and writing a couple of iOS and macOS apps to help power everything…

Weather

On opening the app the current date is shown along with a loading spinner whilst the five different data sources are called and cached; two of my own APIs, YouTube, Pocket, and OpenWeather. Only once they are all loaded does the “Get Started” button appear.

The weather icons are displayed as soon as the OpenWeather API response is received and show the weather for the next 7 days along with the high and low temperatures. This entire row is powered by SwiftUI with each day rendered like this:

struct DailyWeatherView: View {
    
    var weather: DailyWeather
    
    var body: some View {
        VStack(spacing: 30) {
            Text(weather.day)
                .font(.subheadline)
                .fontWeight(.bold)
                .multilineTextAlignment(.center)
            
            VStack {
                Image(systemName: weather.icon)
                    .font(.system(size: 80))
                Spacer()
            }.frame(maxHeight: 120)
            
            Text("\(weather.low)\(weather.high)↑")
                .font(.subheadline)
                .fontWeight(.bold)
                .multilineTextAlignment(.center)
                .opacity(0.7)
        }
        .frame(width: 220, height: 300, alignment: .center)
    }
}

I love how quick and easy it is to prototype something with SwiftUI and with the new Apple Silicon Macs it’s actually feasible to use the realtime preview in Xcode. The icons for each weather type are pulled from SF Symbols and you may notice I had to lock the VStack they appear in to 120pt high with a Spacer underneath; this is because the icons are all different sizes and so cloud would be misaligned when placed next to cloud.rain.

Newspapers

Stand Tall: To start with we're going to adopt the ideal posture. Try and retain this posture as often as you can. Stand straight and tall with your head high. Make sure your ears, shoulders, hips, knees, and ankles are in a straight line and pull your tummy button in.

In my original idea for “Morning Briefing” I had wanted to get the news headlines along with an excerpt. This is relatively easy to do via something like BBC News with screenscraping but the recent pandemic meant that nearly all of the “top read” stories were pandemic related which I didn’t really care to read about. I have a subscription to The Times so thought about scraping that site somehow but there was no easy way to get the same content as the newspaper so it ended up being whatever was breaking right now which I also didn’t want3. Perhaps I could hook into Apple News now that I’ve got a subscription with Apple One? Nope, Apple News has no APIs or SDK and one look at the network requests with Charles Proxy made me think I didn’t want to go down that rabbit hole. In the end, I opted for a much simpler solution of having a carousel of the front pages of today’s newspapers. This lets me see the main topics of the day without getting bogged down in article excerpts which are often a bit click-baity to get you to the actual article.

I tried a few ways to get front pages (originally from the BBC and Sky News websites) but in the end I wrote a very simple PHP script to scrape tomorrowspapers.co.uk every few hours:

require_once('simplehtmldom/HtmlWeb.php');
use simplehtmldom\HtmlWeb;
$client = new HtmlWeb();
$html = $client->load('https://www.tomorrowspapers.co.uk');

$images = [];
foreach ($html->find('img.size-full') as $imgTag) {
    if (strpos($imgTag->src, 'DESIGNFORM-Logo') !== false) {
        continue;
    }
    $images[] = $imgTag->src;
}

shuffle($images);

$json = json_encode($images);
file_put_contents('newspapers.txt', $json);
echo $json;

The front pages themselves are image views within a UIPageViewController. This has the nice side benefit that they are automatically placed into an endless carousel that can be swiped through with the Siri Remote. I randomise the pages each day so that I see each newspaper in a different order.

Tangent: Page Layout

There are 12 stretches in my chiropractic video which I've trimmed from the original downloaded video and saved as individual files. I then typed up the narration for each exercise and placed it in a JSON file.

The newspaper page and all the subsequent pages are all the same UIViewController which use the data in the JSON file to display the appropriate video, stretch name, and instructions. They then load a child view controller on the right hand side of the page; this was a UIPageViewController for the newspapers but is a UIHostingController with a SwiftUI view in it for all of the following stretches.

Tasks

Tilting Star: From the stand tall position, spread your arms and legs into a star. Facing forwards, place one hand in the air and the other at your side. Breathe in as you slowly stretch one arm overhead while slowly bending to the opposite side and sliding the hand down the thigh. Relax at the end of the stretch and don't forget to breathe. Perform slowly twice each side.

I’ve been a long time user of Things, the task manager based on the Getting Things Done methodology. I have several repeating tasks in intervals along with ad hoc tasks I create throughout the day, all sorted into areas and projects prefixed with an emoji so they don’t look quite so dull. Unfortunately, there is no online component with Things4 and thus no public API as there is with something like Todoist. Whilst there is some support for Siri Shortcuts they are for opening the app rather than extracting data out of it. However, the macOS app does have comprehensive support for AppleScript and so that offered me a way to get at the data:

on zero_pad(value, string_length)
    set string_zeroes to ""
    set digits_to_pad to string_length - (length of (value as string))
    if digits_to_pad > 0 then
        repeat digits_to_pad times
            set string_zeroes to string_zeroes & "0" as string
        end repeat
    end if
    set padded_value to string_zeroes & value as string
    return padded_value
end zero_pad

set now to (current date)
set today to (year of now as integer) as string
set today to result & "-"
set today to result & zero_pad(month of now as integer, 2)
set today to result & "-"
set today to result & zero_pad(day of now as integer, 2)

tell application "Things3"
    
    set todayIdentifiers to {}
    repeat with task in to dos of list "Today"
        set props to properties of task
        copy id of props to the end of todayIdentifiers
    end repeat
    set taggedTasks to {}
    repeat with toDo in to dos of list "Anytime"
        if tag names of toDo is "One A Day" then
            set props to properties of toDo
            if todayIdentifiers does not contain id of props then
                copy toDo to the end of taggedTasks
            end if
        end if
    end repeat
    set listSize to count of taggedTasks
    if listSize > 0 then
        set randomNumber to (random number from 1 to listSize)
        set toDo to item randomNumber of taggedTasks
        move toDo to list "Today"
    end if
    
    
    set listOfProjects to {}
    repeat with proj in projects
        set props to properties of proj
        set projectId to id of props
        set projectName to name of props
        set object to {projectName:projectName, projectId:projectId}
        copy object to the end of listOfProjects
    end repeat
    
    set listOfTasks to {}
    
    set tasks to to dos of list "Inbox"
    repeat with task in tasks
        set props to properties of task
        set projectName to "Inbox"
        set taskName to name of props
        set object to {|name|:taskName, |project|:projectName}
        copy object to the end of listOfTasks
    end repeat
    
    set tasks to to dos of list "Today"
    repeat with task in tasks
        set props to properties of task
        set projectName to ""
        if exists project of props then
            set projectId to id of project of props
            repeat with proj in listOfProjects
                if projectId in proj is projectId then
                    set projectName to projectName in proj
                end if
            end repeat
        end if
        
        set taskName to name of props
        
        set object to {|name|:taskName, |project|:projectName}
        
        copy object to the end of listOfTasks
    end repeat
end tell


set exportPath to "~/Dropbox/Apps/Morning Briefing/things.plist"
tell application "System Events"
    set plistData to {|date|:today, tasks:listOfTasks}
    set rootDictionary to make new property list item with properties {kind:record, value:plistData}
    set export to make new property list file with properties {contents:rootDictionary, name:exportPath}
end tell

There’s quite a lot there but essentially it boils down to going through my Today list and sorting each task by its project. These all then get added to a plist (as easier to deal with in AppleScript than JSON) as an array of dictionaries containing the task name and project name which is then saved to my Dropbox where a PHP script on my server can access it and return it through my own API.

One habit I’ve gotten into over the years is to create a number of short tasks that need to be done at some point and then tagging them “One A Day” with the aim being to do one of them every day. In this way, small jobs around the house or minor tweaks to projects slowly get done over time rather than lingering around. To help with that, the script above will find all of my tasks tagged in such a way, pick one at random, and then move it to Today before the list is exported.

On the app side, this plist is fetched and then parsed in order to make it more palatable to the app by grouping the tasks into their projects and fixing a few minor issues i.e. tasks in Things marked as “This Evening” still show up in the AppleScript as their project name so I rename their project to “🌘 This Evening” mimicking the waning crescent moon icon Things uses in its apps. I could likely do this within the AppleScript file but it’s just easier to do it in Swift!

In an ideal world this script would run automatically first thing in the morning so everything is synced up when I open the Apple TV app. Unfortunately this isn’t possible for me at the moment as I’m exclusively using an Apple Silicon MacBook Pro and I don’t leave it running continuously as I used to with my Mac Pro. For now, I need to run the script manually each morning (which is as simple as double clicking an exported app from Script Editor) but this will change once the Apple Silicon desktop machines launch!

Schedule

Twirling Star: In the star position with your tummy button drawn inward, gently turn your head to look at one hand. Slowly turn to watch your hand as it goes behind you, relaxing in this position. Breathe normally. Perform slowly twice each side.

Another habit I’ve gotten into recently is scheduling my week in advance. I do this for my client work but also for things like what exercises I’m going to do or what meals I’m going to eat5. Whilst I could have just typed it into a database to get it out of the API easily, it isn’t much fun manually entering data into an SQL client. Instead, I decided to use the calendar app on my mac and sync the data using Shortcuts on iOS. This is far easier as I can quickly type up my schedule on a Sunday and then sync it each day prior to starting the app.

Tangent: Shortcuts

Shortcuts née Workflow is a powerful system app that allows you to join data from various apps together. I use it extensively for this app, mostly to get otherwise inaccessible data out of my iPhone and into my database so my API can send it to the app. For example:

  • Fetching my calendar entries for the day and formatting them into a JSON array
  • Retrieving my exercise minutes and step count from a custom HealthKit app I wrote
  • Getting the amount of water I drank yesterday from Health
  • Fetching my sleep stats from the AutoSleep app

All of this data can then be packaged up in JSON and posted to my server using the poorly named "Get contents of" network action.

The only downside is that I do have to trigger this manually as no app, not even Shortcuts, can access your Health database when your iPhone is locked so a shortcut set to run at a specific time won't work unless I'm using my phone. This hasn't proved too annoying though as I'm now in the habit of running the shortcut directly from a widget on my home screen before I launch the Apple TV app.

I divide the day up into five sections; Anytime (all-day tasks), Morning (before midday), Lunchtime (midday - 2pm), Afternoon (2pm - 5pm), and Evening (after 5pm). If there are items at a specific time then that time is displayed but mostly items will be from my Schedule calendar which is comprised of tasks tagged in a specific way. For example, for dinner I will create an all-day task named “[food:dinner] Penne Bolognese” and the app will know to use the correct prefix and colour. Once again, SwiftUI becomes a joy to use for short interfaces like this:

struct ScheduleView: View {
    
    var schedule: MorningBriefingResponse.Schedule
    
    var body: some View {

        VStack() {
        
            Text("Schedule")
                .font(.title)
                .padding(.bottom, 50.0)
                        
            VStack(alignment: .leading, spacing: 10) {
                
                ForEach(schedule.formatted, id: \.self) { section in
                    
                    VStack(alignment: .leading, spacing: 2) {
                        Text(section.heading)
                            .font(.headline)
                        Divider()
                            .background(Color.primary)
                    }
                        .padding(.bottom, 10)
                        .padding(.leading, 210)
                    
                    ForEach(section.entries, id: \.self) { entry in
                        HStack(spacing: 10) {
                            Text(entry.label.uppercased())
                                .font(.caption2)
                                .bold()
                                .multilineTextAlignment(.leading)
                                .foregroundColor(Color(entry.color ?? UIColor.label))
                                .frame(width: 200, alignment: .trailing)
                            
                            Text(entry.title)
                                .font(.body)
                                .multilineTextAlignment(.leading)
                            Spacer()
                        }.opacity(0.7)
                    }
                    Spacer()
                        .frame(height: 40)
                }
            }
            Spacer()
        }
    }
}

The entire interface took less than 5 minutes to create and avoided the usual need to create either a reusable cell in a table view or a custom view to be added to a stack view dynamically.

Sleep

Twisting Star: From the star position, raise your arms and put your hands up. Bring your left elbow across your body to your raised right knee. Repeat the movement using your right elbow and left knee. Remain upright as you continue to alternate sides for 15 seconds. Breathe freely and enjoy.

Getting more sleep is usually the simplest way you can improve your life. If you’re getting less than 8 hours a day then sleeping more will improve your mood, cognition, memory, and help with weight loss and food cravings. I used to be terrible for sleep, typically getting 5 hours or less a day, but a concerted effort this year had me at an 8 hour average for most of January and February.

I use the excellent AutoSleep app on Apple Watch to track my sleep. The counterpart iOS app can reply to a Siri Shortcut by pasting a dictionary of your sleep data to the clipboard making it trivial to export and use as part of a workflow (although it also saves to HealthKit so you could just export it from there as well depending on what stats you are after).

The interface above is about to become fairly familiar as I use it for a number of metrics within the app. It comprises of 3 rings in an Apple Activity style giving me the sleep duration for last night, the nightly average over the last week, and the nightly average for the current year. Each ring is aiming to hit the 8 hour target and I write the duration as both time and percentage within each ring.

Tangent: Rings

The rings themselves are provided by MKRingProgressView which are Swift views for use within UIKit apps. To get them to work in SwiftUI was relatively straightforward requiring only that I write a small class conform to UIViewRepresentable:

import SwiftUI
import MKRingProgressView

struct ProgressRingView: UIViewRepresentable {
    var progress: Double
    var ringWidth: CGFloat = 30
    var startColor: UIColor
    var endColor: UIColor

    func makeUIView(context: Context) -> RingProgressView {
        let view = RingProgressView()
        view.ringWidth = ringWidth
        return view
    }

    func updateUIView(_ uiView: RingProgressView, context: Context) {
        uiView.progress = progress
        uiView.startColor = startColor
        uiView.endColor = endColor
    }
}

Each ring can then be created in SwiftUI as simply as:

ProgressRingView(progress: progress, ringWidth: size.ringWidth, startColor: startColor, endColor: endColor)

The only issue with this is that it's slow, especially on the Apple TV hardware which is several chip cycles behind modern iPhones. It may be the MKRingProgressView class itself is quite heavy going but I get the feeling translating it to work with SwiftUI and then pushing the whole SwiftUI view back to UIKit via UIHostingController might also be adding some lag. Overall it isn't a problem but it does mean that the view takes a half second to load.

Alcohol

Trap Openers: Breathe deeply, calmly, and relax your tummy. Let your head hang slightly forward and gently turn your head from one side to the other. Then, using your fingers, gently massage the area just below the back of your head. Move down to the base of your neck, then relax your shoulders and slowly roll them backwards and forwards. Repeat for 15 seconds.

As part of a concerted effort to improve my health, the amount of alcohol I was drinking had to come down fairly significantly. I’ve tried many habit tracking apps in the past mostly in the form of maintaining a chain; the problem I have with them is that once the chain is broken I tend to go all out (i.e. if I’m going to get a black mark to say “drank alcohol” for having a glass of wine I may as well have a bottle of wine). To remedy that I take the idea of chaining but also pair it with the government guidelines for alcohol consumption which is 14 units a week for men.

The data for this is manually input into my SQL database for now but I have other apps I’m working on that interact with those tables. For now, I add what I’ve drank and how many units along with a date and the API then returns the data you see above; how many units I drank yesterday, how many days since I last had something alcoholic (if yesterday was 0 units), the number of units I’ve drank in the last 7 days, and a weekly average for the past 3 months. This differs slightly from the other ring-based metrics in the app as rather than a daily average over 7 days I needed to fetch a cumulative sum to ensure I wasn’t going over 14 units in a 7 day period. The weekly average is also interesting as I needed to go through each Monday-Sunday period of the past 3 months and get a cumulative total before averaging that.

This system has worked very well for me with the result being an average of 1 glass of wine and 1 beer a week which is down significantly from what was likely around 4 bottles of wine and several beers a week. I also had a dry spell of over 2 weeks which is likely the longest duration in around 10 years. I have a lot more thoughts around alcohol tracking and logging which I’ll come back to in a future side project.

The one update I might make to this page is to show how much water I’m drinking a day. I track my water intake via WaterMinder and already sync the data to my server using the Shortcut I’ve mentioned previously. Originally I had a whole page for showing this data but I dropped it in favour of some more important metrics as I found my water levels were pretty consistent once I’d gotten into the habit of drinking more. It looks like it’s not something I need to keep an eye on quite so much but if I do bring it back I think it will be on this page.

Steps

The Eagle: Take your arms out to the side and slowly raise them above your head breathing in and keeping your shoulder blades together. Touch your hands together above your head. Slowly lower your hands to your sides while breathing out. Perform 3 times. This will help the movement of air in and out of your lungs.

Along with doing my morning stretches, my chiropractor was also very adamant that I needed to increase my physical exercise as my step count was a rather woeful average of 6000 steps per day in 20206. To that end I’ve started running again on my treadmill and going for multiple walks each day.

Whilst the data above is rendered in much the same way as the sleep data, getting it is far tricker than you might think due to the complication of having an Apple Watch and an iPhone. If you use Shortcuts to fetch your data from Health, then it isn’t possible to deduplicate properly and so you’ll end up with a total number of steps from both your Apple Watch and iPhone which is bad if you ever walk around with both devices at the same time which I obviously do. You can limit the fetch to a single device but then you run into problems as you won’t be recording any steps whilst your watch is charging for instance. I don’t know how this error has been allowed to continue for so long in Shortcuts but the solution is rather simple for iOS apps: you use a HKStatisticsQuery along with the .cumulativeSum and .separateBySource options. Unfortunately this meant I needed to write an iOS app to fetch my steps data along with an Intent Extension to allow that to then be exported via Siri Shortcuts.

class StepsService: NSObject {
    
    let store = HKHealthStore()
    
    func authorize() {
        guard let quantityType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount) else {
            return
        }

        store.requestAuthorization(toShare: [], read: [quantityType]) { (success, error) in
            DispatchQueue.main.async {
                print("Authorized")
            }
        }
    }
    
    func fetch(for date: Date, onCompletion completionHandler: @escaping (Int) -> Void) {
        guard let quantityType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount) else {
            fatalError("StepCount quantity doesn't exist")
        }
        
        let start = Calendar.current.startOfDay(for: date)
        
        var dayComponent = DateComponents()
        dayComponent.day = 1
        guard let tomorrow = Calendar.current.date(byAdding: dayComponent, to: date) else { return }
        let end = Date(timeIntervalSince1970: Calendar.current.startOfDay(for: tomorrow).timeIntervalSince1970 - 1)

        let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [.strictStartDate, .strictEndDate])
        let query = HKStatisticsQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: [.cumulativeSum, .separateBySource]) { _, result, _ in
            DispatchQueue.main.async {
                guard let result = result, let sum = result.sumQuantity() else {
                    completionHandler(0)
                    return
                }
                
                let count = Int(sum.doubleValue(for: HKUnit.count()))
                completionHandler(count)
            }
        }
        store.execute(query)
    }
}
class StepsIntentHandler: NSObject, StepsIntentHandling {
    func handle(intent: StepsIntent, completion: @escaping (StepsIntentResponse) -> Void) {
        
        guard let components = intent.date, let date = Calendar.current.date(from: components) else {
            let response = StepsIntentResponse(code: .failure, userActivity: nil)
            completion(response)
            return
        }
        
        let stepsService = StepsService()
        let exerciseService = ExerciseService()
        stepsService.fetch(for: date) { (steps) in
            exerciseService.fetch(for: date) { (minutes) in
                let response = StepsIntentResponse(code: .success, userActivity: nil)
                
                var activity = [String: Int]()
                activity["steps"] = steps
                activity["exercise"] = minutes
                guard let data = try? JSONEncoder().encode(activity), let output = String(data: data, encoding: .utf8) else {
                    completion(StepsIntentResponse(code: .failure, userActivity: nil))
                    return
                }
                
                response.output = output
                completion(response)
            }
        }
    }
}

After opening the app and authorising it for read access on my HealthKit store, it now sits hidden in the App Library and is woken each morning as my sync shortcut requests the data and sends it to my server. It’s a long winded solution but it works!

Exercise

The Hummingbird: Put your arms to the side with your hands up. Pull your shoulders together. Next, make small backward circles with your hands and arms drawing your shoulder blades together. Sway gently from side to side in The Hummingbird. Keep it going while you count to 10.

The exercise view is pretty much identical to the steps view previously only with different data input, colour, and a goal of 30 minutes. Originally I used the native Health actions within Shortcuts to extract the exercise minutes but then I ran into similar duplication issues if I wore my Apple Watch whilst doing a work out on my Peleton bike as that too would add an exercise. To make things simple I added exercise minutes to the same exporter app I mentioned above.

YouTube

The Butterfly: Place your hands behind your head and gently draw your elbows backwards. Slowly and gently press your head backwards against your hands for a count of two. Release, again slowly and gently. Keeping your hands in this position, perform 3 times. This will make you more relaxed and improve your sense of wellbeing.

Whilst I’m on my treadmill I’ll typically watch a variety of videos I’ve saved on YouTube. This screen shows me the number of videos left in my “To Watch” playlist along with the total runtime7. I then show the details of five videos; the most recently added, the two oldest, and then two random ones from those that are left. In this way I get to know which videos have been sat a while and that I should really watch whilst also seeing something that is likely more exciting to me right now as I added it most recently. I’m not forced to watch these particular videos but I find I usually do choose them as it’s one less thing to think about when I get onto the treadmill.

Fetching the videos is relatively easy using the YouTube API as a single request can give you all the video details for a single playlist. Unfortunately, and for reasons I don’t understand, YouTube doesn’t provide access to the default “Watch Later” playlist even with an authenticated request so I had to create a public playlist named “To Watch” and then remember to add all of my videos to that. It’s a bit of a pain as I nearly always want to just click the “Watch Later” button on YouTube but I’m slowly getting used to adding to the playlist instead.

Pocket

Tight Rope: In the stand tall position pull your tummy in. Take a step forward as if on a tightrope. Make sure your knee is over your ankle and not over your toes. Allow the heel of your back foot to lift. Balance in this position for 20 seconds. Don't worry if you wobble a bit, it's normal. Repeat on the opposite side.

In a similar vein to the YouTube page, I do something very similar to show me the articles I currently have to read in Pocket8. I connect directly to the Pocket API and return the number of articles remaining, the estimated reading time of them all, and then 5 articles as a mixture of newest, oldest, and random entries.

Coding

The Rocker: Stand tall with your feet wider than shoulders. Gently rotate your upper body from side to side. Let your arms flop loosely as you shift your weight from knee to knee. Breathe calmly and deeply. Continue for 15 seconds.

I mentioned previously that I like to have a couple of “One A Day” tasks so I can make slow and steady progress on items over time. This is also true of my own personal projects such as this app which will often remain nothing but ideas unless time is carved out to work on them. To help with that, I maintain a list of the projects I’m working on and then have the API pick one at random each day to show me; the idea then is that I will work on that app for at least 18 minutes9 during the day. It’s a system that has worked well for me and gets around the issue of the current favourite project getting all of my attention.

Tangent: Choices Choices

There is a theory that "Overchoice" or "Choice Overload" occurs when we are presented with too many options and that it causes us stress and slows down our process of choosing whilst also sapping our energy reserves. The original studies of this from the '70s have since been called into question but I personally find that oftentimes if I'm given the freedom to do anything I'll end up doing nothing as I can't choose between the numerous options available to me. This is true of what I should watch, read, play, and work on in my spare time. My solution, as briefly alluded to above with YouTube videos and Pocket articles, is to take a subsection and return them randomly so that I don't have to pick.

This is something I also use with my personal projects, books, and games; rather than have all the options available to me, I take the items that are currently active and then the app will return a single result each day. This avoids any decision making on my part as if I want to do some coding I already know what project it will be on or if I'm going to read I know which of my current books it will be. The only rules I set myself are that if I really want to do something else, I must meet the minimum requirement for the day on the original choice first (i.e. lets say I want to work on my Lord of the Rings LCG app but I've been assigned Pocket Rocket, then I work on Pocket Rocket for the 18 minutes to fulfill my daily goal and then I'm free to work on whatever I like).

The one thing I'm particularly mindful of with this approach is that I may need to tweak how random it actually is. With true randomness, I could get the same result every day for a week and generally I'll want to mix things up a bit. It's also the case that I don't necessarily have time for my hobbies every day so I might have 2 days reading the same book and then go 3 days without reading anything; when I get to the 6th day the same book is randomly returned and I've missed out on the other books on the previous days. The only way around this is likely to remove results if my database sees I've spent concurrent days on the same item. I've also started restricting these choices to just 3 active items at a time as otherwise there are a few too many for the randomness to work nicely.

For logging time on projects, I have written a small app named “Time Well Spent” which I’ll be covering in a future side projects article. This data is then stored in my database where my API returns it for rendering the three activity rings in a similar way to the steps or exercise rings covered earlier.

Reading

The Triangle: Stand tall with your feet wider than your shoulders and your tummy in. Turn your foot outwards as you lean to that side. Feel the gentle stretch on the inside of your leg. Your knee should be above your ankle. Lean over so your elbow can rest on your bent knee as you stretch your arm over your upper body. Do this slowly so you have more control. Stretch for 10 seconds on each side.

I’m a big believer in the power of Idea Sex, the notion that by juggling several different thoughts together you’ll trigger more creative thinking. One way to do this is to read multiple books at the same time rather than reading in a linear fashion. As you may be able to guess from the tangent box above, I do this with books by having the app tell me what book I’m going to read today from the pool of books I’m currently reading. Beneath that I render the familiar three activity rings based on data I’ve logged via my “Time Well Spent” app and a 30 minute daily goal. I might also add a tally of how many books I’ve completed this year at some point but for now this is working well.

I listen to an audiobook on most of my evening walks and have recently begun tracking that data as well. It’s likely that I may try juggling multiple audiobooks and use a similar system to the above to pick one for me each day.

Games

Shaking Loose: Now it's time to let go. Shake your limbs loosely for 15 seconds.

The final screen was originally intended to be a bit of a sweetener to force me to do my stoutness routine every day; it tells me what video game I can play today. However, the amount of time I’m spending on video games has dropped fairly dramatically this year taken up by an increase in exercise and board games and (soon) a newborn baby. It’s no longer quite the pull it once was but I’ve managed to go through this routine every day so far this year so I’m not too concerned!

I’ve long tracked my gaming hobby as I used to run a website where I’d output my currently logged time and write reviews. I have a dedicated app for this purpose which I’m currently migrating into “Time Well Spent” but I also have some automated scripts to fetch my play time from services such as Steam and Xbox Live. In addition, I have a number of game collections which I can query such as “Currently Playing” and “To Play”. Using these, it was very easy to render the above page showing which game I’m allowed to play today but also giving me a secondary choice should I complete that game10.

I don’t use the activity rings to render the average data as I don’t have a goal for how much time I want to play for whereas steps, exercise, projects, and reading are all things I want to do a certain minimum of (or maximum in the case of alcohol).

Conclusion

As of the time of writing, I’ve spent 24.4 hours working on the Stoutness app from its origins as a printed daily report to the Apple TV app it is now complete with its suite of data providers from AppleScript apps to Shortcut workflows. Since using it daily from the start of the year I’ve had huge improvements in nearly all of the metrics I track; I’m sleeping longer, drinking less, exercising more, and reading more. I’ve also noticed big improvements in my health with my daily stretching leading to more flexibility11 and less aches and my average resting heart rate falling from ~70bpm to ~50bpm. Finally, I’ve lost over 22lbs since using the app which is just over 10% of my starting weight; that isn’t all attributable to this app but it has definitely helped by encouraging me to keep working on the various rings that I see every morning.

With a lot of side project apps I often think about how they can be adapted so others can make use of them but in this case what I have is a highly specialised app bespoke to me… and that’s OK! One of the great joys of being a developer is that you can take seemingly disparate data and merge it together into something just for you. Hopefully this article will inspire you to do likewise and learn some new coding skills whilst building something to help you in your own life.

  1. Originally I would have done this using text-to-voice within an app but obviously it’s far easier to do it with Siri Shortcuts nowadays such as this Morning Routine by MacStories. Also, with HomePod update 14.2 it is now possible to say “What’s my update” and get something similar. ↩︎

  2. I’d envisioned it being a little like the Personal Navigator that used to get printed on Disney Cruises showing you the schedule for the next day along with weather and other pertinent information. ↩︎

  3. I despise rolling news and the culture of “information now” which is why I subscribe to a newspaper. I like to get a clear picture of the days news after the dust has settled rather than the incessant guessing at what is happening, about to happen, and the reaction of what just happened. Don’t get me started on “[public figure] is expected to say” with announcement previews. ↩︎

  4. Well that’s not strictly true. There is a private API as data is synced to Things Cloud but I don’t fancy reverse engineering that! ↩︎

  5. I used a meal planning app for a while but it was too much hassle to peck everything in on an iPad; now I just create an all-day event in a dedicated calendar. Why plan meals? Mostly because I’m terrible with letting food expire especially at the moment where recent food shortages at supermarkets have led to shorter shelf lives on several products. ↩︎

  6. The real average was likely lower as there were 2 weeks in Walt Disney World where I was over 20k steps every day which would skew the averages. ↩︎

  7. Rather than returning something normal like seconds, YouTube returns video durations in a format like PT24M49S which required an extra bit of working around. ↩︎

  8. Obligatory “go check out my Pocket app” comment. ↩︎

  9. 18 minutes is a bit specific you might very well think but there’s a (sort of) good reason. My database first started logging games and these were done in a decimal format of hours to one decimal place as that’s how they came from Steam thus all of my gaming time is broken down into 6 minute chunks. When I added other items to the tracking system I kept the same format and this persisted when I started tracking how much time I was spending on personal projects; whilst I’d probably prefer it to be a goal of 15 or 20 minutes a day, it’s 18 so it can be represented cleanly as 0.3 hours in the database. I could make the ring show 20 minutes but then it would always be not quite full or over full and that seems messy. ↩︎

  10. This is something I’ve also thought about adding for books but it’s slightly different as I know how long it will take me to finish a book — you can see how many pages are left — but it isn’t always obvious how much you have left of a game hence the need for a backup if I sit down and finish the current one in short order. ↩︎

  11. At the start of the year I used to struggle to touch my knees with my elbows for the “Twisting Star” exercise as I just couldn’t bend that well; now I can do it with no problems. Hooray for basic levels of fitness! ↩︎

Exorbitantcy and the fight against the App Store

Last month, outspoken CEO of Epic Games Tim Sweeney ranted about the App Store in an interview with CNBC:

Apple has locked down and crippled the ecosystem by inventing an absolute monopoly on the distribution of software, on the monetization of software. They are preventing an entire category of businesses and applications from being engulfed in their ecosystem by virtue of excluding competitors from each aspect of their business that they’re protecting.

If every developer could accept their own payments and avoid the 30% tax by Apple and Google we could pass the savings along to all our consumers and players would get a better deal on items. And you’d have economic competition.

Today, the rhetoric turned into action as Epic announced “direct payment on mobile”:

Today, we’re also introducing a new way to pay on iOS and Android: Epic direct payment. When you choose to use Epic direct payments, you save up to 20% as Epic passes along payment processing savings to you.

Currently, when using Apple and Google payment options, Apple and Google collect a 30% fee, and the up to 20% price drop does not apply. If Apple or Google lower their fees on payments in the future, Epic will pass along the savings to you.

Here’s how it looks in the UK version of the game1.

This is obviously in clear contravention of the App Store Review Guidelines2, specifically 3.1.1 In-App Purchase:

If you want to unlock features or functionality within your app, (by way of example: subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use in-app purchase. Apps may not use their own mechanisms to unlock content or functionality, such as license keys, augmented reality markers, QR codes, etc. Apps and their metadata may not include buttons, external links, or other calls to action that direct customers to purchasing mechanisms other than in-app purchase.

There isn’t a scenario where Epic doesn’t know this and it’s also highly unlikely that Apple accidentally approved an app update which made these changes. It’s probably the case that the code for this was included in the app binary but was disabled via a server switch until today. This means that Apple reviewed the app update, approved it, and then Epic changed the functionality which is also against the rules. This is more likely when you consider that the “what’s new” text for the update which is meant to outline what changed in the version doesn’t mention anything about these new payment options.

Regardless of where you stand on the issue of Apple’s payment cut, the action taken by Epic here is in clear violation of the platform guidelines leaving Apple with a difficult choice; remove Fortnite from the App Store or try and resolve this directly with Epic which will likely lead to every other aggrieved developer trying something similar.

Of course, within a few hours Apple had removed Fortnite from the App Store. This isn’t that much of a big deal as it is likely installed on millions of devices already and I doubt there are many more users flocking to the game at this stage. The app isn’t being forcibly removed from user’s devices and if you’ve ever downloaded it in the past you can still redownload it right now and get the current version. That said, it’s quite a striking image to see Apple remove what is undoubtedly one of the biggest, if not the biggest, games from their store especially when they were likely making millions of dollars from it thanks to their 30% cut of in-app purchases.

Now clearly Epic knew this was going to happen as minutes after the app was removed they were advertising a short film called “Nineteen Eighty-Fortnite3” (a clear callback to Apple’s famous Ridley Scott directed 1984 advert) and had filed a complaint for injunctive relief. In short, this is a well executed publicity stunt in which Epic knew they could goad Apple into removing them from the App Store.

From here on out it’s likely to be an escalating war between the two companies as Apple tries to explain that their rules have been broken and Epic tries to get them to completely change their rules. I don’t see a situation in which either company comes out looking particularly good nor do I see Apple suddenly doing an about turn on a business practice they’ve maintained since the App Store was launched back in 2008. And why should they? It’s their platform and the rules are clear and unambiguous; you can’t circumvent App Store review and you can’t offer your own payment system for these types of transactions. If Epic doesn’t like that, then they don’t have to sell their products on Apple devices. Nobody is entitled to run on any platform they want at the price that they want.

The thing that irks me most about this is that Epic are choosing to call Apple’s take an “exhorbitant [sic] 30% fee on all payments”. It can’t really be described as exorbitant when it’s been the industry standard for over a decade on multiple platforms including Google Play on Android and Steam on PC.

What I find exorbitant is the selling of costumes in a game for upwards of £20. I find it exorbitant that to purchase anything within Fortnite you first have to purchase an intermediary currency whereby you can’t ever get the exact amount you need. For example, here are the current prices on Fortnite today including the new 20% price drop they’ve added if you purchase direct from Epic:

And here are some of the items available for purchase:

Note that there is no equality between the two. If you want to buy a 1200 V-Buck “The Brat” outfit you’ll need to either pay £12.98 to buy two packs of 1000 V-Bucks (leaving you 800 spare) or pay £3.01 more to buy 2800 V-Bucks (leaving you 1600 spare). This is nothing new of course4 and is a well trodden psychological trick designed to give you a form of credit so purchases have less friction; I can thoroughly recommend Jamie Madigan’s excellent website “The psychology of video games” and his article on “The perils of in-game currency” if you want to find out more.

These kind of spending tricks are the worst aspects of the App Store along with the £99 gem bundles that are frequently seen in Match-3 timer games. Worse yet are the loot boxes that are there merely to get children hooked on gambling and which governments around the world are now starting to investigate. Fortnite isn’t as bad as these, but the fact that Epic was able to reduce it’s 30% cut to 12% on it’s PC storefront due to the huge profit they’d made on selling kids overpriced cosmetics in Fortnite isn’t exactly laudable.

Apple are by no means blameless in any of this either. They’ve created the store that allowed these high-price consumables to be commonplace and whilst they could easily improve developers lives by reducing the 30% fee they’ve chosen not to. I’ve long thought a good compromise would be some form of sliding scale along the lines of £0-10k is free, £10k-100k is 30%, then £100k+ is 15%. Of course, doing that means taking a huge financial hit to a multi-billion dollar business5.

For my part, I’d be glad to see a reduction in the 30% fee but I don’t want to see multiple store fronts being allowed on iOS; that there is only once place to get apps is a huge benefit to most users and developers. Whilst I doubt it will happen, I would dearly love for Apple to fight back by banning the selling of intermediary currencies in apps and instead only allowing direct purchases (i.e. that “The Brat” outfit would be clearly labelled as a £7.99 purchase). That sort of change would make genuine shock-waves through the industry and actually let something good come of this entire situation. It’s far more likely however that this is just going to end in endless courtroom battles and some form of App Store regulation.

  1. I don’t know whether it’s A/B testing or a change since the update was released but in all of the promo shots of this on Epic’s blog the Apple payment system comes first but in the app Epic’s payment system is on top. ↩︎

  2. They’re called guidelines but they’re rules. They really should be named as such. ↩︎

  3. It even has today’s date on the screen within the video so this was clearly very well planned. ↩︎

  4. And it’s something I’m guilty of myself. I ran a popular freemium game called WallaBee back in 2011 which had it’s own intermediate currency known as honeycombs. Of course, we provided plenty of ways for players to get these for free (usually by doing more exercise by travelling outside long before Pokémon Go came along) and the cheapest pack at £0.99 would be more than enough to buy any of the items in the game. If somebody spent over a certain amount within a certain time period, we’d reach out to them to make sure there weren’t any issues (in one case somebody was frequently gifted App Store cards from their work so they spent them all on our game). I had a blueprint for a more ethical in-app purchasing system for the long worked on v2 of the game but I ended up selling the company before I could put that in action. ↩︎

  5. Whilst the App Store is small fry compared to the money Apple rakes in from hardware it is still a significant business. It’s very well to say “they’ve got so much money, they can reduce their fees” but, again, why should they? Part of being a successful business is not giving your money away! ↩︎

Side Project: (Feed Me) Seymour

This is part of a series of blog posts in which I showcase some of the side projects I work on for my own use. As with all of my side projects, I’m not focused on perfect code or UI; it just needs to run!

I’ve cheated slightly this month by working on two side projects; a simple iOS app and a SwiftUI watchOS app. The Apple Watch app isn’t quite ready yet so instead I’m going to show you “Seymour”, an app that sends me push notifications when relevant articles are posted online.

The Problem

I’m travelling to The Happiest Place On Earth™ next week and as I’m staying in a Disney hotel I was able to book fastpasses for rides 60 days in advance. The issue is that a new ride, Mickey & Minnie’s Runaway Railway, is due to open whilst I’m there but fastpasses were not yet available to be booked as the opening date hadn’t been formally announced. When the announcement finally came, I didn’t see it for 5 hours as I was busy and hadn’t been checking my numerous theme park RSS feeds1. Fortunately I was still able to grab one of the remaining fastpasses for later in my holiday but I was determined that this shouldn’t happen again…

I use the excellent Feedbin service2 to keep up with my feeds and it turns out they have an app called Feedbin Notifier, but it doesn’t seem to work. It only supports one search term and the examples all use single words which didn’t give me total faith that the phrase “rise of the resistence OR space 220 OR fastpass” would parse correctly. There was also no detail on how regularly the checking was done and whilst I managed to get a notification for the term “apple” I couldn’t get it to work for much else. As I couldn’t trust it would do what I needed it to, I decided it was quicker and easier to just build it myself.

“Couldn’t you just use Google News notifications?”. I already do for some things3 but it’s far too slow for my use case. The types of articles I’m interested in require me to act within around 15 minutes so too slow for Google News which typically takes 12-24 hours to send a notification.

The Name

Like most other developers, I start a side project with the most important decision; choosing a name. Sometimes this can be a days long process but for this app it was relatively quick. I wanted to do some kind of play on words with Feedbin and was looking at things like “Feed Trash Can” or “Feed Trough” but I didn’t really like any of them. I was saying them out loud in bed whilst working on the app and my wife instinctively said “Feed Me Seymour”. I shortened it down to just “Seymour” and decided this was a perfect name as it was letting me “See More” of the things I wanted to see.

As the app isn’t going to be released publicly I did a Google Image Search for “Feed Me Seymour” in the hope of finding some sort of silhouetted version of the singing plant. Instead I found this Mario crossover at Snorgtees which was perfect.

Feed Me

The iOS app is ridiculous simple so I’m not going to spend too much time describing that. It consists of a table view listing my search terms and each row can be swiped to delete the term. The + button in the top right presents a UIAlertController with a text field for adding a search term. The fetch, add, and delete commands are all sent to an incredibly basic PHP API I wrote which syncs to my MySQL database.

The real work is to fetch the latest feeds from my Feedbin account and then search each article for any matching text strings:

$pdo = new PDO("mysql:charset=utf8mb4;host=" . $db_host . ";dbname=" . $db_name, $db_user, $db_pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$headers = [];
$url = 'https://api.feedbin.com/v2/entries.json';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_USERPWD, 'ben@bendodson.com:keepitsecretkeepitsafe');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['If-None-Match: \''.file_get_contents('etag.txt').'\'']);
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
  function($curl, $header) use (&$headers)
  {
    $len = strlen($header);
    $header = explode(':', $header, 2);
    if (count($header) < 2) // ignore invalid headers
      return $len;

    $headers[strtolower(trim($header[0]))][] = trim($header[1]);

    return $len;
  }
);
$json = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);

file_put_contents('etag.txt', $headers['etag'][0]);

$stmt = $pdo->prepare('select * from keywords');
$stmt->execute();
$keywords = array_column($stmt->fetchAll(), 'phrase');

$ids = [];
$entries = [];
$array = json_decode($json);
$countStmt = $pdo->prepare('select count(id) as total from entries where id = :id');
foreach ($array as $entry) {
    $countStmt->execute(['id' => $entry->id]);
    $count = (int)$countStmt->fetch()['total'];
    if ($count) {
        continue;
    }

    foreach ($keywords as $keyword) {
        if (stripos($entry->title, $keyword) !== false || stripos($entry->content, $keyword) !== false) {
            $entries[] = $entry;
            break;
        }
    }

    $ids[] = $entry->id;
}

$notificationStmt = $pdo->prepare('insert into notifications (title, message, url, created_at) values (:title, :message, :url, :created)');
$idStmt = $pdo->prepare('insert into entries (id) values (:id)');

$pdo->beginTransaction();
foreach ($ids as $id) {
    $idStmt->execute(['id' => $id]);
}
$pdo->commit();

foreach ($entries as $entry) {
    $notificationStmt->execute(['title' => $entry->title, 'message' => $entry->summary, 'url' => $entry->url, 'created' => date('Y-m-d H:i:s')]);
    
    $data = ["app_id" => "my-onesignal-app-id", "contents" => ["en" => $entry->summary], "headings" => ["en" => $entry->title], "included_segments" => ["All"], "data" => ["url" => $entry->url]];
    $ch = curl_init('https://onesignal.com/api/v1/notifications');
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Authorization: Basic [OBFUSCATED]']);
    $response = curl_exec($ch);
    curl_close($ch);

}

Feedbin provide an API for fetching the latest articles from all of your feeds. It has support for ETag caching so I store the latest tag to avoid unnecessary loading on their end as this script will run every 60 seconds.

Once the articles are fetched, I ignore any that I’ve already seen before (worked out by storing the ids in my database after each run) and then proceed to do a case insensitive check for my keywords against both the title and content of the article. If there are any matches, these get added to an array which is later looped over in order to send a push notification via OneSignal containing the title, article summary, and the URL.

This runs on a 1 minute CRON job so in theory I should get alerted of any matching articles within 60 seconds of them being published. I don’t know how often Feedbin polls the various RSS feeds I track but in practice I have found that I’m getting notified almost immediately so evidently they’re doing some form of dark magic.

Viewing the content

Once the notification is delivered, I thought it would be a nice bonus to be able to tap to open up the article in a browser. It turns out that the OneSignal SDK can do this automatically if you send a “url” parameter as part of your Push Notification request but it has a few issues; there’s a short lag between the app opening and the browser being shown, the controller is within a modal dialogue that can only be dismissed by swiping at the top of the page (tricky on a big phone), and it’s using either a UIWebView or a WKWebView which means there is no support for content blockers, no Reader View, and no easy way to open the page in Safari or other apps. The solution is to send the URL as part of the data payload and parse it myself within the app to open an SFSafariViewController:

Can you see the difference in the image above? With the webview provided by OneSignal we can just see the start of the title of the article4 obscured by the home indicator, and this is on the Max-sized iPhone! The SFSafariViewController in Reader View is far superior showing the full title, the first paragraph, and an image. It also loaded faster thanks to the support for content blockers and I have easy access to open this in Safari or share it along with controls for changing fonts, etc.

Conclusion

This whole thing took about 2 hours of which the majority was fiddling around with ETags. At the moment I have only two search terms set up:

  • Rise of the Resistance: Quite possibly the best theme park attraction ever made, Rise of the Resistance opened in December 2019 and since then has been using a virtual queueing system which typically gets booked out for the day within an hour. Whilst I don’t expect this is going to be moved to FastPass any time soon, I keep this notification just in case!

  • Space 220: This is a new restaurant in Epcot that was supposed to be open last year and was then rumoured to be opening in February. That still hasn’t happened but it’s feasible the opening will happen whilst I’m there and I definitely want to be one of the first to book a reservation.

Whilst I could have set up a screen scraper of some sort to monitor these pages for changes5, I much prefer operating on top of RSS as it tends to be that the feeds I follow will post updates ridiculously quickly and are likely more reliable than tracking HTML changes. It’s also more flexible as I’m sure there will be things in future that I’ll want to monitor in this way. For example, I tested the app a few weeks ago to get notified when Pokémon HOME was released on the App Store.

As ever, I hope this will inspire you to work on your own side projects! Next month I’ll be talking about the watch app that has ended up taking a lot longer than I’d originally anticipated…

  1. I’ve blogged about this before but RSS is how I consume nearly everything I read online. Twitter doesn’t even come close. With RSS you can subscribe to nearly any website that publishes articles and get them all in a nice app with no ads, no Javascript, and no other superfluous crap. At the time of writing, I subscribe to around 500 different feeds encompassing everything from theme park news and video game updates to Swift developer blogs and philosophy articles. ↩︎

  2. In conjunction with the Reeder apps on iOS and macOS. ↩︎

  3. I use Google News Notifier to see if my name comes up anywhere and also to keep tabs on a court case I sat on as part of Jury Duty. ↩︎

  4. Disney Food Blog is one of the best theme park sites I follow but their website is very hard to view on mobile hence the need for Reader View. Whilst it’s more of a necessity for that page, I find myself using Reader View for most websites as it is nearly always far better. ↩︎

  5. I did do this when the AirPods were first announced; fetch the HTML of the store page every five minutes and then send a notification when it changed so I could jump in and buy a pair. ↩︎

Side Project: Sealed

This is part of a series of blog posts in which I showcase some of the side projects I work on for my own use. As with all of my side projects, I’m not focused on perfect code or UI; it just needs to run!

I am a huge advocate of side projects, small apps that let you test an idea in isolation usually for your own personal use. Over the course of my 14 year career as a software developer I’ve always tried to encourage new developers to work on side projects as a way of honing their craft. There are three reasons for this: firstly, building something for yourself is far more rewarding than building something for a client; secondly, it gives you an excuse to try out new technologies or methodologies that can then improve future client work without running the risk of derailing a major project1; and thirdly, it’s a great way of building up a portfolio if you’re starting out. I’ve always built bizarre little side projects and apps ranging from an iPad app to manage my wine collection to various PHP scripts that extract my time playing video games on Steam. Sometimes these side projects turn into full apps such as Music Library Tracker and Pocket Rocket but usually they are highly bespoke utilities for me that nobody else gets to see. Until now…

This year I’ve decided to start a new series of articles where I’ll show a side project I’ve built over the past month. Today’s article is all about “Sealed”, an iPad app I build in January 2020 to simulate the opening of Magic the Gathering booster packs.

I’m assuming that most people reading this article have little to no interest in Magic The Gathering and so I’m not going to explain that side of it in much detail. Suffice to say that the game consists of you opening blind packs containing 15 cards that you can play with. In sealed play, you open 6 of these blind packs (named “boosters”) and then build a 40 card deck out of the cards you opened. The idea for this app is that it will simulate this process allowing me and my good friend John (who lives in Sweden) to open 6 packs each and build a deck with the random contents within. We can then export them to the game Tabletop Simulator so we can play with them in a realistic 3D physics-based environment…

Tabletop Simulator even supports VR so we can simulate playing a few rounds in the same room even though we’re around 870 miles apart.

As with all of the side projects I’m going to be working on I’m not focused on perfect code or UI; it just needs to work. That said, I did spend a bit more time prettying this one up as I wasn’t the sole user.

A brief tour

The iPad app first does a brief download of data before opening on a selection screen that allows you to pick which expansion you want to play2. You can also choose to load one of your previously created decks.

Each physical booster pack has a specific breakdown of cards based on rarity usually comprising of 10 commons, 3 uncommons, 1 rare or mythic rare, and 1 basic land. The mythic rare is the tricky piece as there is a 1:8 chance it will replace the rare card. As I wanted things to be more “fair” in this app, I’ve fudged the numbers such that you’ll always get 5 rare cards and 1 mythic rare card; this avoids the issue (which could happen in a completely random system) of somebody ending up with far more mythic rares.

As these rare cards are usually the best of the bunch, these are shown immediately after the contents of the packs have been decided by the app:

Once you press continue, you are taken into the deck building interface with those 6 cards automatically added to your deck:

The bulk of the interface is dedicated to showing the cards you’ve opened with a number of diamonds above to show how many copies you have available; these fill in when the cards are added to the deck on the right hand side and will fade to 50% opacity if you’ve used every copy available. The top right hand section shows a “mana curve” (which is just a graph for showing the various costs of the cards in the game) along with a break down of the various types of card you’ve selected (as typically you want more creatures than anything else). Underneath is your deck in a scrollable list with a design mimicking the top of each card showing both the name and mana cost.

If you tap on an item in the deck list or long press on a card in the card picker then you’ll get a blown up version of the card which is easier to read. You’ll also be able to use the plus and minus buttons to add or remove the card from your deck (although if you only have one copy you can just tap to add them and remove them directly from the card picker as this is nearly always quicker).

The final two items are the sample hand and export; the former shows you a random draw of 7 cards from your deck to simulate the first action you take in the game3 whilst the latter generates the tiled image you’ll use to import the deck into Tabletop Simulator.

Reusing code

In total this app took around 6 hours to build mostly thanks to a huge amount of reusable code I could make use of from previous side projects.

I’m a big fan of Magic The Gathering and so last year I built myself a private app called “Gatherer” that gives me access to lots of information about each card thanks to the Scryfall API. For that app I wanted everything to work offline so I duplicated the data into my own hosted database and then made a single request when online to download all of the data and store it on the device in a Realm database. I used the exact same system here with the only new functionality being a new database table to list which expansions I wanted to be available within the app4. Within a few minutes of starting I had a local database and access to all of the card information I needed.

The next major piece of reused code was the design of the card cells in the deck creator. I wanted to mimic the headings of the cards in a similar way to Magic The Gathering Arena, an online version of the card game. The heading should show the title of the card (which will shrink as necessary to avoid word wrapping), the mana cost, and the border should be the colour of the card using a gradient if necessary:

Fortunately I had already built this exact design for another side project of mine, an iPad app to control the overlay for my Twitch stream:

Nearly everything in the above screenshot aside from the game and webcam is powered by an iPad app plugged into my PC capture card via HDMI. It was a fun experience to play with the external window APIs but it also allowed me to do animations to show off the deck list I’m currently playing with on MTG Arena; the sidebar scrolls every minute or so to reveal the full list and I can trigger certain animations from my iPad to show off a particular card in more detail. In any case, you’ll notice the deck list table cells on the right hand side are identical to the ones in this Sealed app. They are fairly straightforward with the most complex piece being some string replacement to convert a mana cost such as {1}{U}{U} into the icons you see.

The lesson here is that huge chunks of UI or functionality can be reused from side projects into your client projects saving you development time and speeding up your learning. As a practical example, I’m currently working on an app which requires a Tinder-style card swiping system; whilst I could embed an unknown 3rd party component which may have bugs and not be updated in future, I can instead use a card swiping system I built for a music-based side project a year ago. This saved me a significant amount of time and resulted in me being able to give a better price to my client than I might otherwise have been able to.

Random Aside

One of the areas I had a lot of fun with in this project was putting together the randomisation for opening packs. There are very specific rules when it comes to Magic packs with a set number of cards in specific rarity slots, no duplication unless there is a foil card5, and some weird edge cases for certain expansions. Here is the full code for this particular feature:

func sealTheDeal(set: String) -> Deck {
    guard let expansion = realm.object(ofType: Expansion.self, forPrimaryKey: set) else { abort() }
    let locations = expansion.locations.sorted(by: { Int($0.key) ?? 0 < Int($1.key) ?? 0})
    let max = Int(locations.first?.key ?? "0") ?? 0
    let cardsInSet = realm.objects(Card.self).filter("set = %@ and number <= %d", set, max)

    var mythics = cardsInSet.filter("rarity = %@", "mythic")
    var rares = cardsInSet.filter("rarity = %@", "rare")
    var uncommons = cardsInSet.filter("rarity = %@", "uncommon")
    var commons = cardsInSet.filter("rarity = %@ AND NOT (typeLine CONTAINS[c] %@)", "common", "basic land")
    
    switch set {
    case "dom":
        uncommons = uncommons.filter("NOT (typeLine CONTAINS[c] %@)", "legendary")
    case "grn", "rna":
        commons = commons.filter("NOT (name CONTAINS[c] %@)", "guildgate")
    case "war":
        mythics = mythics.filter("NOT (typeLine CONTAINS[c] %@)", "planeswalker")
        rares = rares.filter("NOT (typeLine CONTAINS[c] %@)", "planeswalker")
        uncommons = uncommons.filter("NOT (typeLine CONTAINS[c] %@)", "planeswalker")
    default:
        break
    }
    
    
    let mythicIndex = Int.random(in: 0..<6)
    
    var boosters = [[Card]]()
    for boosterIndex in 0..<6 {
        var cards = [Card]()
        var commonPool = Array(commons.map { $0 })
        for _ in 0..<10 {
            guard let card = commonPool.randomElement(), let index = commonPool.firstIndex(of: card) else { continue }
            cards.append(card)
            commonPool.remove(at: index)
        }
        
        var uncommonPool = Array(uncommons.map { $0 })
        for _ in 0..<3 {
            guard let card = uncommonPool.randomElement(), let index = uncommonPool.firstIndex(of: card) else { continue }
            cards.append(card)
            uncommonPool.remove(at: index)
        }
        
        let rareAndMythicRarePool = boosterIndex == mythicIndex ? mythics : rares
        if let card = rareAndMythicRarePool.randomElement() {
            cards.append(card)
        }
        
        switch set {
        case "dom":
            if let card = cardsInSet.filter("rarity = %@ AND (typeLine CONTAINS[c] %@)", "uncommon", "legendary").randomElement() {
                cards[cards.count - 2] = card
            }
        case "grn", "rna":
            if let card = cardsInSet.filter("name CONTAINS[c] %@", "guildgate").randomElement() {
                cards.append(card)
            }
        case "war":
            let uncommonChance = boosterIndex == mythicIndex ? 92 : 78
            if Int.random(in: 1...100) <= uncommonChance {
                if let card = cardsInSet.filter("rarity = %@ AND (typeLine CONTAINS[c] %@)", "uncommon", "planeswalker").randomElement() {
                    cards[cards.count - 2] = card
                }
            } else {
                if let card = cardsInSet.filter("rarity = %@ AND (typeLine CONTAINS[c] %@)", boosterIndex == mythicIndex ? "mythic" : "rare", "planeswalker").randomElement() {
                    cards[cards.count - 1] = card
                }
            }
        default:
            break
        }
        
        boosters.append(cards)
    }
    
    let boosterCards = boosters.flatMap({$0})
    let topCards = boosterCards.filter { return $0.rarity == "rare" || $0.rarity == "mythic" }
    
    let deck = Deck()
    deck.id = NSUUID().uuidString
    deck.expansion = expansion
    deck.topCards.append(objectsIn: topCards)
    
    let cards = boosterCards.sorted(by:{ $0.number < $1.number })
    var currentCard: Card?
    var quantity = 0
    for card in cards {
        if card != currentCard && currentCard != nil {
            let deckCard = DeckCard()
            deckCard.card = currentCard
            deckCard.quantityAvailable = quantity
            deck.allCards.append(deckCard)
            quantity = 0
        }
        
        currentCard = card
        quantity += 1
    }
    
    let deckCard = DeckCard()
    deckCard.card = currentCard
    deckCard.quantityAvailable = quantity
    deck.allCards.append(deckCard)
    
    deck.allCards.append(objectsIn: expansion.lands())
    
    let topIdentifiers = Array(deck.topCards.map({$0.id}))
    for index in 0..<deck.allCards.count {
        let card = deck.allCards[index]
        if topIdentifiers.contains(card.card?.id ?? "") {
            for _ in 0..<card.quantityAvailable {
                deck.add(card)
            }
        }
    }
    
    return deck
}

Essentially I take the following steps:

  • Build an array containing the cards at each rarity level
  • Remove any edge cases for specific expansions (i.e. removing Guildgates from Guilds of Ravnica)
  • For each booster pack, loop through each array randomly selecting a card and removing it from the pool to avoid duplication
  • For Dominaria (“dom”), change one of the uncommon cards to be a Legendary card
  • For Guilds of Ravnica (“grn”) and Ravnica Allegiance (“rna”) add a random Guildgate card.
  • For War of the Spark (“war”), replace one of the uncommon, rare, or mythic rare cards with a random Planeswalker card of the same rarity
  • Once the cards for each booster are known, group the 6 rare / mythic rare cards into their own array for UI simplicity and then bundle everything together in a nice object that groups any duplicates found across the packs

There is nothing particularly difficult about the above but it was still fun the first time I got it working to see my console filling up with cards as if I’d opened a physical pack!

Exporting

The “killer feature” of the app is the ability to export cards to Tabletop Simulator, a task that is surprisingly easy. To import custom cards, all you need to do is supply a 4096x3994px image that comprises of 10 columns and 7 rows. Here’s an example image of a 40 card deck that was exported from Sealed which uses the top 4 rows and leaves the remaining 3 blank (although it will use them if you build a deck larger than 40 cards although this isn’t usually recommended for sealed play).

In order to generate the large image I simply render UIImageViews onto a UIView that is the correct size, loop through each image and download it, and then use the snapshotting APIs to capture the view as a UIImage ready for exporting as a JPEG that usually weighs in at around 3MB. Here’s the full code:

import UIKit
import SDWebImage

class TabletopSimulatorDeck: UIView {
    
    static let cardWidth = 410
    static let cardHeight = 571
    static let maxColumns = 10
    
    var cards = [Card]()
    private var imageViews = [UIImageView]()
    private var downloadedImageCount = 0

    class func instanceFromNib() -> TabletopSimulatorDeck {
        return Bundle.main.loadNibNamed("TabletopSimulatorDeck", owner: nil, options: nil)?.first as! TabletopSimulatorDeck
    }
    
    static var fileURL: URL {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("deck.jpg")
    }
    
    func export(onCompletion completionHandler: @escaping (Data?) -> Void) {
        downloadedImageCount = 0
        var row = 0
        var column = 0
        for _ in cards {
            let rect = CGRect(x: column * TabletopSimulatorDeck.cardWidth, y: row * TabletopSimulatorDeck.cardHeight, width: TabletopSimulatorDeck.cardWidth, height: TabletopSimulatorDeck.cardHeight)
            let imageView = UIImageView(frame: rect)
            imageViews.append(imageView)
            addSubview(imageView)
            
            column += 1
            if column == TabletopSimulatorDeck.maxColumns {
                column = 0
                row += 1
            }
        }
        
        for index in 0..<cards.count {
            let card = cards[index]
            let imageView = imageViews[index]
            imageView.sd_setImage(with: card.url(for: .card), placeholderImage: nil, options: .retryFailed) { (_, _, _, _) in
                self.downloadedImageCount += 1
                self.render(onCompletion: completionHandler)
            }
        }
    }
    
    func render(onCompletion completionHandler: @escaping (Data?) -> Void) {
        if downloadedImageCount != cards.count {
            return
        }
        
        DispatchQueue.main.async {
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 1.0)
            self.layer.render(in: UIGraphicsGetCurrentContext()!)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            
            if let image = image, let data = image.jpegData(compressionQuality: 0.75) {
                try? data.write(to: TabletopSimulatorDeck.fileURL, options: .atomicWrite)
                completionHandler(data)
                return
            }
            
            completionHandler(nil)
        }
    }
}

It’s a dirty solution and it could possibly cause some memory issues on a really old iPad, but I don’t need to worry about that for this project where both devices that will use the app are more than capable of rendering all of this in milliseconds.

Once the image is generated, I use a standard UIActivityViewController to allow for simple sharing. One annoying gotcha that catches me every time is that the controller will provide a “save image” button that you can use to save to your photo library but the app will crash when this is pressed unless you’ve added a NSPhotoLibraryAddUsageDescription key to your info.plist. I’m not sure why Xcode can’t flag this in advance or why this requirement can’t be removed bearing in mind the user is making an informed action.

Conclusion

John and I have played three games of sealed using this app so far and I’m really pleased with how it’s turned out. I can build a deck in around 10 minutes whereas usually it would take 30-45 minutes using real packs. The exports work great in Tabletop Simulator and I can see us using this for a long time to come. I’ll likely add some extra functionality over time such as the ability to duplicate decks or updating the artwork to use the new showcase variants that are super rare but for now this app is definitely a success.

Whilst on the face of it this would be easy to publish to the App Store, the legal and moral implications prevent me from doing so. I’ve spent literally thousands of pounds on this game both in physical cards and digital ones on MTG Arena so I don’t have any qualms about using the artwork to play with a friend I otherwise wouldn’t be able to play with. That said, it’s very different doing something like this for your own private use than it is to publish it and enable it for others who may not have made the same investment in the real world product. For that reason it’s unlikely this will ever be available for wider consumption.

For February’s side project I’m working on something a bit different that will work as a form of learning for me; a standalone watchOS app built with SwiftUI! Be sure to check back next month to learn more about that project and to see how it ended up…

  1. “Oh that new framework looks good, I’ll try that in my next project” - Nope! I’ve learned the hard way that you do not want to use your clients as a guinea pig for the latest thing. By way of example, look at SwiftUI announced at WWDC 2019. You should not be using that in a client project, but it would be perfect in a side project. ↩︎

  2. There are around 3 expansions each year and you typically play sealed within one expansion i.e. you’ll get 6 packs of Guilds of Ravnica or 6 packs of Throne of Eldraine but you wouldn’t build a deck with 3 packs from each. This is all due to the careful balancing the games creators do to ensure that things stay relatively fair within these sealed games. ↩︎

  3. This is an important tool as it allows you to very quickly perform a few draws to see if the cards you are getting are balanced, especially when it comes to mana costs, lands, and colours. ↩︎

  4. My database is updated every morning in order to get the latest pricing information but that means I often get partial expansions if a new one is in the middle of being unveiled. This can last a few weeks so I needed the ability to hide certain expansions until they were ready for playing. ↩︎

  5. Although I made things easier by ignoring foil cards. Usually they replace a common card to give you a random card from the set with a shiny foil treatment but again this can lead to an imbalance as my opponent might end up with 3 mythic rare cards if they get lucky with the randomiser. ↩︎

Revival

I’m pleased to announce the release of a new client app I’ve been working on over the past few months: Revival, truly uncomplicated task planning for everyone:

I was originally contacted by Wonderboy Media with the intention of working on updating their existing reminders app which was looking a little dated and had some issues with broken functionality. After a detailed examination, it was determined that a complete rebuild was needed in order to make a sustainable v2.0 which would last as a solid foundation for many years to come. I worked as the sole iOS developer on the project over many months1 as I got to grips with what was a deceptively complex project including such things as local notifications (with snoozing), timezones, locations, subtasks, priorities, lists, tags, notes, files, contacts, subscriptions, integration with the iOS Reminders app, and Siri support! In addition, I completely redesigned the app giving it a far more modern look and feel complete with fluid gestures, sounds, and haptics. I even redesigned the app icon!2 It should also go without saying that the app was built entirely in Swift 5 using AutoLayout to ensure a flexible design from the 4.7” iPhone SE right up to the 12.9” iPad Pro including the various adaptations that can occur when multitasking on iPadOS.

One of the most complicated pieces was a desire for syncing not only between devices but also via sharing and sending tasks between users. I’m particularly proud of the seamless and accountless syncing system within the app which uses your CloudKit identifier along with Firebase in order to provide real time syncing between devices – you can literally complete a task on your iPhone and see it update in milliseconds on your iPad! In addition, everything is stored locally on device using Realm in order to keep things incredibly fast and to provide advanced searching capabilities. When it came to sharing a task with another user, I removed the old system which required creating user accounts and sending codes back and forth and instead created a system which provides a simple URL; when opened, the app pulls all of the information you need quickly and securely to either create a copy or grant access to the shared task.

Another complex piece revolved around theming in that users can change both the overall colour of the app and also switch between a light and dark mode. This required a custom solution for being able to change all of the UI on a whim whilst also recognising that iOS 13 was around the corner and would likely include a dark mode. The work I put in paid off and I was able to easily add an “automatic” mode that would change the theme between light and dark based on the iOS preferences3 when iOS 13 was launched.

Whilst there are many aspects to this project that provided additional complications4, one of the most satisfying to work on was the localisation of the app to eight additional languages. We worked with Babble-on in order to get the required translation files which were easily loaded into the app but I also wanted to improve the experience on the App Store. The previous versions of the app had custom artwork for each language with translated text above but showed the same English interface. I wanted to automate this and improve it, especially as Apple required screenshots for four devices across nine languages. The solution was to use the XCTest framework (along with a custom data loader) to open the app at various pages and take snapshots; these were then used by the deliver and snapshot parts of Fastlane to wrap them in a device frame and add the localised text. The result is 180 screenshots each with localised text, the correct frame for the targeted device, and a localised screenshot.

I really enjoyed working with Wonderboy Media on this project and having the opportunity to work on such a complex project. I’m incredibly pleased with both the design and development work that I’ve put into this project and I’m excited to see how it grows in future.

You can download Revival on the App Store for free and subscriptions are available to gain access to advanced features.

  1. Another developer has now been added to the team to help with a number of exciting new updates which will be rolling out over the next few months. ↩︎

  2. As a reminders app it’s almost law that you have to use a tick but I wanted to have a homage to the skeuomorphic clock-based interface of the old app as well. My solution was to make a clock face with the tick rendered from the hands. ↩︎

  3. This is far more complex than it seems as if you choose to have iOS change between light and dark automatically based on the time of day then it is possible for the theme to change whilst you are in the middle of using the app; every view needed to be able to handle the possibility that the underlying theme could change at any second rather than just occurring from the settings panel. ↩︎

  4. Auto renewable subscriptions and supporting promoted in-app purchases, getting around the 64-notification limits of the UNUserNotificationCentre when you have 300 notifications to load in, how to efficiently deal with syncing contact information when it could be different on each device, etc. Don’t even talk to me about recreating the entirety of the custom repeat options from the iOS Reminders app! ↩︎

Gyfted

I’m pleased to announce the release of a new client app I’ve been working on recently: Gyfted, the free universal wishlist.

I worked as the only iOS developer on the project working remotely from the UK. The app provides a free and easy way for users to create their own wishlists and populate them with items from a data driven explore page or by manually entering details. It is also possible to add an item via a web link (including via a system wide share sheet) which is then used to automatically add metadata including images, descriptions, and more.

Whilst I originally started building the app using Cloud Firestore from Firebase, this quickly proved to be insufficient for the ways in which we wanted to generate the various social feeds and explore pages. To remedy this, I built a custom server backend and API including such features as feed generation, friend requests, likes, shares, and profile data collection.

The beautiful design was provided by Gyfted but I did need to make a number of adjustments to ensure it would scale correctly on all devices from the iPhone 4 up to the iPhone 11 Pro Max. The app is written entirely in Swift 5 and there are several pieces of swiping interactivity and subtle animated bounces to make the app feel right at home in the modern iOS ecosystem. Other Apple technologies used include push notifications, accessing the address book1, and a full sharing suite powered by Universal Links allowing the app to be opened directly to specific sections of the app or redirecting a user to the App Store if they don’t have the app installed.

I really enjoyed working with Gyfted on this project and having a chance to build both an interesting wishlist app and the server infrastructure to support it. You can download Gyfted on the App Store for free and learn more about it at gyfted.it.

  1. Done in a secure and privacy-focussed way. The user doesn’t need to give full access to their contacts or provide access permissions. ↩︎

Introducing the Apple TV Shows & Movies Artwork Finder

With iOS 12.3, Apple unveiled a new design for the TV app featuring an industry unstandard 16:9 aspect ratio for cover artwork. This new design was used for both TV shows and movies which had been 1:1 squares and 3:2 portraits respectively. Apple doubled down on this design with the preview of macOS Catalina over the summer and the imminent removal of iTunes.

Apple TV and Movie artwork: before and after iOS 12.3 redesign

This new art style is notable for a few reasons. Firstly, it is almost the exact opposite to every other platform that uses portrait style artwork. Secondly, there must have been an insane amount of work done by the graphics department at Apple to get this ready. These aren’t just automated crops but brand new artwork treatments across tens of thousands of films and TV shows (which get this new treatment for each season).

The Big Bang Theory artwork before and after the iOS 12.3 TV update

This update doesn’t extend to every single property on the store but the vast majority of popular titles seem to have been updated. For those that haven’t, Apple typically places the old rectangular artwork into the 16:9 frame with an aspect fit and then uses a blurred version of the artwork in aspect fill to produce a passable thumbnail.

Since this new style debuted, I’ve received a lot of email asking when my iTunes Artwork Finder would be updated to support it. Unfortunately the old iTunes Search API does not provide this new artwork as it relates to the now defunct iTunes and a new API has not been forthcoming. Instead, I had to do some digging around and a bit of reverse engineering in order to bring you the Apple TV Shows & Movies Artwork Finder, a brand new tool designed specifically to fetch these new artwork styles.

The Walking Dead in Ben Dodson's Apple TV Shows Artwork Finder

Jurassic World in Ben Dodson's Apple Movies Artwork Finder

When you perform a search, you’ll receive results for TV shows and movies in the same way as searching within the TV app. For each show or film, you’ll get access to a huge array of artwork including such things as the new 16:9 cover art, the old iTunes style cover art, preview frames, full screen imagery and previews, transparent PNG logos, and even parallax files as used by the Apple TV. Clicking on a TV show will give you similar options for each season of the show.

I’m not going to be open sourcing or detailing exactly how this works at present as the lack of a public API makes it far more likely that Apple would take issue with this tool. However, in broad terms your search is sent to my server1 to generate the necessary URLs and then your own browser makes the requests directly to Apple in order that IP blocking or rate limiting won’t affect the tool for everybody.

As always, this artwork finder is completely free and I do not accept financial donations. If you want to thank me, you can drop me an email, follow me on Twitch, check out some of my iOS apps, or share a link to the finder on your own blog.

Apple TV Shows & Movies Artwork Finder »

  1. I don’t log search terms in any way. I don’t even use basic analytics on my website as it is information I neither need nor want. I only know how many people use these tools due to the overwhelming number of emails I get about them every day! ↩︎

Customising a website for iOS 13 / macOS Mojave Dark Mode

On our The Checked Shirt podcast yesterday, Jason and I were discussing the announcements at WWDC and in particular the new “Dark Mode” in iOS 131. One question Jason asked (as I’m running the iOS 13 beta) is how Safari treats websites; are the colours suddenly inverted?

No. It turns out that just before the release of macOS Mojave last year, the W3C added a draft spec for prefers-color-scheme which is supported by Safari (from v12.1), Chrome (from v76), and Firefox (from v67). Since iOS 13 also includes a dark mode, Mobile Safari now supports this selector as well.

There are three possible values:

  • no-preference (evaluates as false): the default value if the device doesn’t support a mode or if the user hasn’t made a choice
  • light: the user has chosen a light theme
  • dark: the user has chosen a dark theme

In practice, usage is insanely simple. For my own website, my CSS is entirely for the light theme and then I use @media (prefers-color-scheme: dark) to override the relevant pieces for my dark mode like such:

@media (prefers-color-scheme: dark) {
    body {
        color: #fff;
        background: #000;
    }

    a {
        color: #fff;
        border-bottom: 1px solid #fff;
    }

    footer p {
        color: #aaa;
    }

    header h1,
    header h2 {
        color: #fff;
    }

    header h1 a {
        color: #fff;
    }

    nav ul li {
        background: #000;
    }

    .divider {
        border-bottom: 1px solid #ddd;
    }
}

The result is a website that seamlessly matches the theme that the user has selected for their device:

Enabling Dark Mode on a website for iOS 13

A nice touch with this is that the update is instantaneous, at least on iOS 13 and macOS Mojave with Safari; simply change the theme and the CSS will update without the need for a refresh!

I haven’t seen many websites provide an automatic dark mode switcher but I have a feeling it will become far more popular once iOS 13 is released later this year.

  1. Of which I am rightly a hypocrite having complained for years about the never-ending demand for such a mode only to find that I quite like using it… ↩︎

Detecting text with VNRecognizeTextRequest in iOS 13

At WWDC 2017, Apple introduced the Vision framework alongside iOS 11. Vision was designed to help developers classify and identify things such as objects, horizontal planes, barcodes, facial expressions, and text. However, the text detection only recognized where text was displayed, not the actual content of the text1. With the introduction of iOS 13 at WWDC last week, this has thankfully been solved with some updates to the Vision framework adding genuine text recognition.

To test this out, I’ve built a very basic app that can recognise a Magic The Gathering card and retrieve some pertinent information from it, namely the title, set code, and collector number. Here’s an example card and the highlighted text I would like to retrieve.

The components of a Magic card to extract with Vision

You may be looking at this and thinking “that text is pretty small” or that there is a lot of other text around that could get in the way. This is not a problem for Vision.

To get started, we need to create a VNRecognizeTextRequest. This is essentially a declaration of what we are hoping to find along with the set up for what language and accuracy we are looking for:

let request = VNRecognizeTextRequest(completionHandler: self.handleDetectedText)
request.recognitionLevel = .accurate
request.recognitionLanguages = ["en_GB"]

We give our request a completion handler (in this case a function that looks like handleDetectedText(request: VNRequest?, error: Error?)) and then set some properties. You can choose between a .fast or .accurate recognition level which should be fairly self-explanatory; as I’m looking at quite small text along the bottom of the card, I’ve opted for higher accuracy although the faster option does seem to be good enough for larger pieces of text. I’ve also locked the request to British English as I know all of my cards match that locale; you can specify multiple languages but be aware that scanning may take slightly longer for each additional language.

There are two other properties which bear mentioning:

  • customWords: you can provide an array of strings that will be used over the built-in lexicon. This is useful if you know you have some unusual words or if you are seeing misreadings. I’m not using it for this project but if I were to build a commercial scanner I would likely include some of the more difficult cards such as Fblthp, the Lost to avoid issues.
  • minimumTextHeight: this is a float that denotes a size, relative to the image height, at which text should no longer be recognized. If I was building this scanner to just get the card name then this would be useful for removing all of the other text that isn’t necessary but I need the smallest pieces so for now I’ve ignored this property. Obviously the speed would increase if you are ignoring smaller text.

Now that we have our request, we need to use it with an image and a request handler like so:

let requests = [textDetectionRequest]
let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage, orientation: .right, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
    do {
        try imageRequestHandler.perform(requests)
    } catch let error {
        print("Error: \(error)")
    }
}

I’m using an image direct from the camera or camera roll which I’ve converted from a UIImage to a CGImage. This is used in the VNImageRequestHandler along with an orientation flag to help the request handler understand what text it should be recognizing. For the purposes of this demo, I’m always using my phone in portrait with cards that are in portrait so naturally I’ve chosen the orientation of .right. Wait, what? It turns out camera orientation on your device is completely separate to the device rotation and is always deemed to be on the left (as it was determined the default for taking photos back in 2009 was to hold your phone in landscape). Of course, times have changed and we mostly shoot photos and video in portrait but the camera is still aligned to the left so we have to counteract this. I could write an entire article about this subject but for now just go with the fact that we are orienting to the right in this scenario!

Once our handler is set up, we open up a user initiated thread and try to perform our requests. You may notice that this is an array of requests and that is because you could try to pull out multiple pieces of data in the same pass (i.e. identifying faces and text from the same image). As long as there aren’t any errors, the callback we created with our request will be called once text is detected:

func handleDetectedText(request: VNRequest?, error: Error?) {
    if let error = error {
        print("ERROR: \(error)")
        return
    }
    guard let results = request?.results, results.count > 0 else {
        print("No text found")
        return
    }

    for result in results {
        if let observation = result as? VNRecognizedTextObservation {
            for text in observation.topCandidates(1) {
                print(text.string)
                print(text.confidence)
                print(observation.boundingBox)
                print("\n")
            }
        }
    }
}

Our handler is given back our request which now has a results property. Each result is a VNRecognizedTextObservation which itself has a number of candidates for us to investigate. You can choose to receive up to 10 candidates for each piece of recognized text and they are sorted in decreasing confidence order. This can be useful if you have some specific terminology that maybe the parser is getting incorrect on the first try but determines correctly later even if it is less confident. For this example, we only want the first result so we loop through observation.topCandidates(1) and extract both the text and a confidence value. Whilst the candidate itself has different text and confidence, the bounding box is the same regardless and is provided by the observation. The bounding box uses a normalized coordinate system with the origin in the bottom-left so you’ll need to convert it if you want it to play nicely with UIKit.

That’s pretty much all there is to it. If I run a photo of a card through this, I’ll get the following result in just under 0.5s on an iPhone XS Max:

Carnage Tyrant
1.0
(0.2654155572255453, 0.6955686092376709, 0.18710780143737793, 0.019915008544921786)


Creature
1.0
(0.26317582130432127, 0.423814058303833, 0.09479101498921716, 0.013565015792846635)


Dinosaur
1.0
(0.3883238156636556, 0.42648010253906254, 0.10021591186523438, 0.014479541778564364)


Carnage Tyrant can't be countered.
1.0
(0.26538230578104655, 0.3742666244506836, 0.4300231456756592, 0.024643898010253906)


Trample, hexproof
0.5
(0.2610074838002523, 0.34864263534545903, 0.23053167661031088, 0.022259855270385653)


Sun Empire commanders are well versed
1.0
(0.2619712670644124, 0.31746063232421873, 0.45549616813659666, 0.022649812698364302)


in advanced martial strategy. Still, the
1.0
(0.2623249689737956, 0.29798884391784664, 0.4314465204874674, 0.021180248260498136)


correct maneuver is usually to deploy the
1.0
(0.2620727062225342, 0.2772137641906738, 0.4592740217844645, 0.02083740234375009)


giant, implacable death lizard.
1.0
(0.2610833962758382, 0.252408218383789, 0.3502468903859457, 0.023736238479614258)


7/6
0.5
(0.6693102518717448, 0.23347826004028316, 0.04697717030843107, 0.018937730789184593)


179/279 M
1.0
(0.24829587936401368, 0.21893787384033203, 0.08339192072550453, 0.011646795272827193)


XLN: EN N YEONG-HAO HAN
0.5
(0.246867307027181, 0.20903720855712893, 0.19095951716105145, 0.012227916717529319)


TN & 0 2017 Wizards of the Coast
1.0
(0.5428387324015299, 0.21133480072021482, 0.19361832936604817, 0.011657810211181618)

That is incredibly good! Every piece of text that has been recognized has been separated into it’s own bounding box and returned as a result with most garnering a 1.0 confidence rating. Even the very small copyright text is mostly correct2. This was all done on a 3024x4032 image weighing in at 3.1MB and it would be even faster if I resized the image first. It is also worth noting that this process is far quicker on the new A12 Bionic chips that have a dedicated Neural Engine; it runs just fine on older hardware but will take seconds rather than milliseconds.

With the text recognized, the last thing to do is to pull out the pieces of information I want. I won’t put all the code here but the key logic is to iterate through each bounding box and determine the location so I can pick out the text in the lower left hand corner and that in the top left hand corner whilst ignoring anything further along to the right. The end result is a scanning app that can pull out exactly the information I need in under a second3.

iOS app to detect Magic The Gathering cards with iOS 13 Vision Framework

This example app is available on GitHub.

  1. This seemed odd to be me at the time and still does now. Sure it was nice to be able to see a bounding box around individual bits of text but then having to pull them out and OCR them yourself was a pain. ↩︎

  2. Although, ironically, the confidence is 1.0 but it put TN instead of ™ and 0 instead of ©. A high confidence does not mean the parser is correct! ↩︎

  3. In reality I only need the set number and set code; these can then be used with an API call to Scryfall to fetch all of the other possible information about this card including game rulings and monetary value. ↩︎

UKTV Play for Apple TV

In January 2019 I started working with a large brand on an exciting new project; bringing UKTV to the Apple TV.

UKTV is a large media company that is most well known for the Dave channel along with Really, Yesterday, Drama, and Home. Whilst they have had apps on iOS, the web, and other TV set top boxes for some time, they were missing a presence on the Apple TV and contracted me as the sole developer to create their tvOS app.

Whilst several apps of this nature have been built with TVML templates, I built the app natively in Swift 5 in order that I could match the provided designs as close as possible and have full control over the trackpad on the Siri Remote. This necessitated building a custom navigation bar1 and several complex focus guides to ensure that logical items are selected as the user scrolls around2. There are also custom components to ensure text can be scrolled perfectly within the settings pages, a code-based login system for easy user authentication, and realtime background blurring of the highlighted series as you scroll around the app.

Aside from the design, there were also complex integrations required in order to get video playback up and running due to the requirements for traditional TV style adverts and the use of FairPlay DRM on all videos as well as a wide-ranging and technical analytics setup. A comprehensive API was provided for fetching data but several calls are required to render each page due to the rich personalisation of recommended shows; this meant I needed to build a robust caching layer and also an intricate network library to ensure that items were loaded in such a way that duplicate recommendations could be cleanly removed. I also added all of the quality of life touches you expect for an Apple TV app such as Top Shelf integration to display personalised content recommendations on the home screen.

The most exciting aspect for me though was the ability to work on the holy grail of app development; an invitation-only Apple technology. I had always been intrigued as to how some apps (such as BBC iPlayer or ITV Hub) were able to integrate into the TV app and it turns out it is done on an invitation basis much like the first wave of CarPlay compatible apps3. I’m not permitted to go into the details of how it works, but I can say that a lot of effort was required from UKTV to provide their content in a way that could be used by Apple and that the integration I build had to be tested rigorously by Apple prior to submission to the App Store. One of the best moments in the project was when our contact at Apple said “please share my congrats to your tvOS developer; I don’t remember the last time a dev completed TV App integration in just 2 passes”.

UKTV on the TV app

All of this hard work seems to have paid off as the app has reached #1 in the App Store in just over 12 hours4.

I’ve really enjoyed working on this project and I’m looking forward to working with UKTV again in the future. You can download UKTV Play for Apple TV via the App Store and read the official launch press release.

Please note: I did not work on the iOS version of UKTV Play. Whilst iTunes links both apps together, they are entirely separate codebases built by different teams. I was the sole developer on the tvOS version for Apple TV.

  1. Replete with a gentle glimmer as each option is focussed on. ↩︎

  2. For example, the default behaviour you get with tvOS is that it will focus on the next item in the direction you are scrolling. If you scroll up and there is nothing above (as maybe the row above has less content) then it may skip a row, or worse, not scroll at all. This means there is a need for invisible guidelines throughout the app which refocus the remote to the destination that is needed. It seems a small thing, but it is the area in which tvOS most differs from other Apple platforms and is a particular pain point for iOS developers not familiar with the remote interaction of the Apple TV platform. ↩︎

  3. CarPlay is now open to all developers building a specific subsection of apps as of iOS 13. ↩︎

  4. Which I believe makes it my fourth app to reach #1. ↩︎

« Older Entries