#4: New Year, New Tracking App
Rebuilding a personal game tracking app with SwiftUI and Swift Charts
Dear Reader,
One of the things I love most about coding is that it is relatively straightforward to create tools that are useful to you and your own personal situation. You can also use the development of these tools as a way to improve or speed up your professional software development.
For example, I have three apps to discuss this week which are likely of no use to anybody but me. However, they all bring me joy when I use them and I’ve been able to add functionality or employ certain coding practices for the simple reason of learning how to do them. It may be that I add functionality to determine if it’s something I should avoid in future or so I have a bit more knowledge on a particular technology if a client asks about it. There have been several times in the past where I’ve been able to reuse code from a personal project in a client project and save myself days of time as I’d already made an investment for future Ben1.
If you are a software developer, I’d encourage you to work on as many side projects as you comfortably can. They’re a great way to build up a portfolio when you’re first starting out and even for a seasoned developer they can provide fertile ground for testing out new frameworks and new ideas. I have a small list on my website of side projects I’ve worked on; I’ll try to update that list over the next few weeks!
I hope you’ll find something of interest in today’s issue and I look forward to hearing any feedback you may have.
— Ben
Contents
Game Track (rebuilding an app in SwiftUI and Charts)
Back Seat Shuffle (now on GitHub)
Bookmark (adding a predictive feature)
Artwork Finder updates (there are always some)
Music Library Tracker v2.0 (a brief update and a launch date)
My published apps in 2022 (I am terrible at publicity 😂)
Recommended Links
Roadmap
Game Track
As I mentioned last issue, I love tracking what I do. Video games are my largest hobby by time spent so it is probably understandable that I’ve had an app to do this for me over the past 4 years.
It started out as a way to update a website I ran with my good friend John. We wrote game reviews and thought it would be nice to have a page that showed what we’d been playing recently in a chronological list. The entire website was statically generated using Jekyll so we had a database of all the games we had played that was exported as JSON when compiling the site. The app was therefore very simple and consisted of a list of games (from my database), a list of all possible platforms, and a basic input screen to add number of hours played:
If we played a game on Steam (which was 90-95% of our usage back in 2018), then I automatically imported the game details and time spent into the database by running a daily script on my server which checked against the Steam Web API; the total hours of a game would be compared and if there was a difference the excess would be added as a log for the previous day.
Whilst this basic setup served a purpose, there were a number of issues:
I had to manually add every game to the database and create some artwork for it (which was a low res 460x215 to match Steam’s store images - it’s also an awkward aspect ratio). That little + button you see in the first image? That just sends me an iMessage with the name of the game that John wants to add 😂
The platform was independent to the game so you’d always be shown a full list of every platform in the database. My main console is an Xbox Series X so I always had to scroll to the bottom past things I’m very unlikely to play again (looking at you 3DS 👀).
As I was doing a diff between our current playtime and Steam’s playtime, I had to create a new platform called “PC (Not Steam)” which we then merged with the PC platform on the website if we played something from Game Pass, UPlay, Epic, etc. Not ideal!
Logging a game meant going through these screens every time with multiple taps and scrolls.
I resolved the last issue partially by donating an INInteraction using SiriKit whenever a log was created. This would appear in the system search as a deep link so you could jump directly to the final stage of the process if you’d logged against a specific game and platform a couple of times.
Even this was a bit hit and miss though as typically I’d have to search in Spotlight and it would appear under “Show More Results” even if I had interacted with the app every single day2.
Due to the above issues, we weren’t always logging our games, especially if we only played them for a short while as it wasn’t worth the effort of adding a new title and even adding titles we played regularly was arduous.
A new app was needed! Introducing, Game Track built entirely in SwiftUI:
The first major change was to switch to IGDB for the game database. They have a vast array of games and are now owned by Twitch which should keep it running for a long time. It’s easy to add lesser known games3 and whilst their API is a bit weird it does provide some nice 3:4 artwork. It also provides a list of platforms for each game so I can now show a much smaller list; in fact, this page is bypassed entirely if the game is only available on a single platform!
The next change was to alter the way in which logs were being stored, specifically the automatic Steam logs. Previously we had a table named users_to_games which held a running total of hours spent on a conjoined user / game / platform that we could also use to hide a game from the list (i.e. dev tools, early access, etc). Any update to this would use a MySQL trigger to then insert a single record into a logs table. Everything was done in 0.1 hour increments as historically that was how Steam reported time played but that has now changed and it is broken down in minutes for Windows, Mac, and Linux. I have created a steam_cache table which stores these properties and also converts from a Steam ID to an IGDB ID via the External Game endpoint. With this in place I can now split what was previously PC into PC, Mac, and Steam Deck4. I will also be able to create our own logs in minute increments rather than the previously used 6 minute increments.
Finally, rather than starting with the search screen I now show the last 15 games that were logged manually along with their platforms. Tapping one of these instantly brings up the “Add Log” screen and serves as a much quicker replacement for the previous deep linking provided by SiriKit.
These changes alone were a big improvement and formed the basis of v1.0 of the app which John and I started using at the start of the year. However, I wanted to add a bit more and took the logs page we previously had on a website and embedded it natively into the app:
Originally I had server endpoints to fetch our latest 15 games for the Recent tab and the last 3 months of data for the Logs tab. This was a bit slow to load and only became slower when I tweaked the “Add Log” page to show a breakdown of times for each platform (which the eagle eyed of you may have noticed in the previous screenshots). My problem was that I was performing a complex subquery when fetching all of the logs; the solution was to eschew that altogether and instead just return the 5000 or so raw logs (which are fetched very quickly) and have the app join them together.
The result is that I can now show logs from 1st Jan 2018 with no visible slowdown all based on a network request that takes less than a second to resolve. These logs are all stored in a local Realm database which is incredibly fast. I have not added the ability to add logs whilst offline but it would likely be trivial to do so as I have all of the building blocks in place.
One little extra I wanted to add was the ability to “tag” certain logs. Originally this was so we could distinguish games that were played on Steam Deck but I also wanted to use it for tracking what we’d played in VR and on the Game Pass and Apple Arcade services.
Long pressing on a log brings up a context menu which has a list of tags. Interacting with this will let you choose if this is for just this log, this and all future logs, or every log. The tag is then applied on the server (and a note made if it needs to be automatically added for this game / platform combo in future) and the list is reloaded.
The only other thing to note about this page is one of the other context menu options; “Comment”. This goes back to what I was saying in this issue’s introduction about wanting to try new technologies, in this case a new API in iOS 16 called Shared With You. I’d seen that there was a way to link a conversation in Messages back to an item in an app and thought it would be fun to implement that as John and I will often message each other if we see something interesting in the others play history.
The first step was to add Universal Links to the app. The mechanism for this is relatively straightforward but the key thing would be that I’d need a webpage for each log. The logs themselves have unique identifiers but as multiple logs for a single game in the same day are merged together that wasn’t guaranteed to be stable; instead, I came up with a defined hash that looks like this:
1_2023-01-10_151974_381
The hash is broken up by underscores leaving four components; user ID, date, game ID, platform ID. With that, I can fetch the relevant compounded logs for that game for the day and generate a basic website which just spits out a sentence and an image URL into some OpenGraph headers. This URL is then loaded into an MFMessageComposeViewController and will appear as a nice preview once sent:
When tapped, this opens the app but the real magic is that it creates an SWHighlight which can then be rendered in the app to show that this particular log was talked about in Messages:
Tapping that badge (which is a SWAttributionView) opens up Messages directly to that point in the conversation so I can easily go back and see the discussion around that game.
Admittedly this isn’t terribly useful and there were a lot of hoops to jump through to get it to work. Specifically, we had to each enable “Shared With You” for this app within the settings app; for some reason, 3rd party apps are disabled by default and there is no way to provide a system permission prompt in the same way you can for push notifications or calendar access. Instead, you’d need to provide some kind of messaging within the app itself to tell users how to enable this feature like you do for keyboards or Safari extensions. Beyond that, Apple decides when a link is worth suggesting by the Shared With You system so it isn’t guaranteed that one of these links will appear.
That said, I do like the quick link back to a conversation and this was mostly done to satiate my curiosity around the feature rather than something I’ll actively use. [update: whilst proof reading this I clicked on the test link and found that I get the little attribution back to Messages whilst viewing the webpage in Safari on my Mac Studio - that’s pretty cool!]
Speaking of learning new technologies, the final tab – “Stats” – was created primarily so I could learn how to use Swift Charts:
For the past few years I’ve pulled out a number of stats from the MySQL database and shared them with John so we could compare our most played games and total hours. As the app has all of our log files stored in a local Realm database, it’s easy to pull out a ton of stats which can then be displayed in the app instead!
For each month I have the total hours and number of games, then the top 5 most played along with a link to view every game. Beneath that there are two sets of graphs: a line graph to show hours & games played for the whole year and a bar chart to show hours & games played broken down by platform. I’ll likely extend this in future to show how much time has been spent on subscription services such as Game Pass thanks to the tagging system I mentioned previously.
Whilst monthly breakdowns are nice, I also added some UI to change this to a yearly breakdown:
This was a really fun interface to build in SwiftUI as it came together so quickly. I started by building the panel at the top that let you go backwards and forwards through months and it was done within 5 minutes (including bounding so you couldn’t go beyond the current month or earlier than 2018). I then added the tap to switch between Month / Year and to choose a specific date, which again only took a few minutes to implement.
With that in place, I could then create a date range which would either be something like “2020” or “2023-01”. This could then be used as a filter for the logs by checking if the date string was prefixed with that value5; if it was, then it was a log that should be used in the stats page.
The Swift Charts API was also very nice to work with and super fast to implement. Consider a struct like this:
struct PlatformStats: Identifiable {
var id: Int {
return platform.id
}
let platform: Platform
let totalMinutes: Int
let totalGames: Int
var totalHours: Int {
return Int((Double(totalMinutes) / 60).rounded(.up))
}
}
If we have an array of those as a variable named ’platforms’
then the only code required to generate the bar chart is:
Chart(platforms) {
switch mode {
case .games:
BarMark(
x: .value("Platforms", $0.platform.name),
y: .value("Games", $0.totalGames)
)
case .hours:
BarMark(
x: .value("Platforms", $0.platform.name),
y: .value("Hours", $0.totalHours)
)
}
}
.colorScheme(.dark)
Crazy!
However, great simplicity comes without great customisation. The most annoying facet in this first version of Swift Charts is that you can’t change the colour of the axis or their labels. They’ll always use the secondary label colour of the current theme so when I placed it in a black box the axis all but disappeared. To fix this, I had to use a deprecated API to tell the chart to render as if the user was always using dark mode 🤦🏻♂️
It is still a good set of APIs though and works out of the box with very simple data structures. It’s unlikely I’ll be able to use it in any upcoming client projects due to it’s iOS 16 minimum requirement but it’s definitely something I’ll be keeping an eye on for the future.
So, that’s Game Track. I’ve spent far too long working on it bearing in mind it’s just for tracking some basic stats that likely don’t really need tracking but it has been fun to make the previous app more functional and a lot prettier. I not only upped my SwiftUI game but also got to try out some new iOS 16 technologies as well, something that will hopefully pay dividends in the future.
Back Seat Shuffle
Last issue I mentioned an idea I’d had for an app that would let you choose a folder on an external drive and have it randomly play all the videos in that directory. The primary use case for this was for an iPad mounted on the back of my car seat in order to entertain the children on long drives.
The app, Back Seat Shuffle, is now complete! It works by presenting a file selection panel on launch and letting you navigate to the directory you want; in my case it is always an external USB drive but this will work with local files or even cloud-based services if you’re connected to the internet. Once you’ve chosen a folder, I strip out any files that aren’t videos and then play them in a random order. Once all the videos have played the window is dismissed and you’re back to the file picker.
It’s very basic, but it solves a genuine need I have as most apps either don’t have randomised autoplaying or don’t have the ability to read external drives without copying the contents locally.
As promised last time, the app code is available on GitHub so you can compile a copy for yourself. Whilst I could potentially put it on the App Store, this is one of those weird edge cases that likely wouldn’t get through App Review as they’ll look at it and say “this is just the Files app” and completely miss the point. As this would be a free app, it’s not worth the hassle of doing screenshots, App Store description, etc, only to have it be rejected by App Review.
Of course, if enough of you want to try it out then that would likely change my mind…
Bookmark
Another one from the last issue, Bookmark is my app for tracking what I read. One of the issues when it comes to tracking things is that you can easily forget to start or stop a timer. This happened to me last week when I fell asleep whilst reading in bed…
This led me to an idea 💡. If I know how much I’ve read previously and how long it took, and I know where I am now, then I should be able to predict how long the last reading session took…
((current position / previous position) * duration so far) - duration so far
I’ve updated the app so now when I add a position that was later than the previous position, this formula is used and the duration is automatically predicted. This works great as it can help me if I forget to start a reading timer but it can also save me time when entering my logs (as long as I haven’t been slower or faster than usual).
Apple Music Artwork Finder updates
A few minor updates to the Apple Music Artwork Finder:
You can now search for curator artwork. These are typically logos for Apple Music radio series.
Example: Rocket HourPlaylist artwork now has an option for a high resolution PNG in addition to the previous high resolution JPEG. It looks like this is the original image that is used so isn’t compressed by the server in any way.
Example: AntidoteIt turns out it was possible to get music video artwork in the artwork finder but it wasn’t rendering at the correct sizes. I’ve fixed this so music videos are now officially supported.
Example: Cake by the Ocean
Music Library Tracker v2.0
I’m still working away on the big v2.0 update for Music Library Tracker. My plan is to have it ready for release on the 31st January so I’m expecting to send out another beta update next week.
I won’t say anything else at this point as I expect I will go into everything in a lot more detail in the next issue.
If you have not yet tried out the Spatial Audio update, you can use the button below to download the beta for free.
* Music Library Tracker v2.0 will run on any iOS device running iOS 13 or greater. If you already have Music Library Tracker installed then this will update that version; you won’t be able to downgrade. This beta will expire when the app update is publicly released later this month.
I would greatly appreciate any feedback you may have, especially if you run into any problems with the Spatial Audio matching. You can always comment on this post, provide feedback directly through TestFlight, or email me via ben@bendodson.com.
Also, feel free to publicly share any screenshots, videos, or thoughts on the app update (although I’d obviously prefer you share any issues you encounter with me first!)
My published apps in 2022
When I did my wrap up of 2022 in the last issue I completely forgot to promote my own apps 🤦🏻♂️. So, let’s do that now!
I released 3 new apps last year:
Browser Note
A simple way to add a pause to your browsing with dismissible reminders or notes blocking you from certain sites (i.e. social media).Chaise Longue to 5K: Because couches are so common
Use other apps whilst monitoring your Couch-to-5K run with exclusive Picture-in-Picture support. Perfect for a treadmill in front of an Apple TV.Return to Dark Tower Assistant
Keep track of your phases, actions, cards, and Advantages in Return to Dark Tower. Optimised for iPad Slide Over.
I was particularly pleased with both Browser Note and Chaise Longue to 5K as they both run on iPhone, iPad, and Mac with my first foray into Catalyst development! Chaise Longue to 5K goes a ‘step’ further (ha!) and runs on Apple TV as well.
This year I want to release at least 3 new apps which is convenient as I have 3 apps that are close to the end of development: Playlist Precis, Board Game Lists, and Arru. You’ll be hearing about those soon! I also have an Ink framework and some games I’d like to get released so lots to look forward to.
Recommended Links
Video Games
Wreckfest - I saw this whilst browsing through the Game Pass listings and thought it might be a good game to play with my 4 year old son. It turns out I was right! It’s essentially a demolition derby / banger racing sim with a good variety of challenges and some silliness. I thought my son was going to die laughing when we had a demolition derby with combine harvesters. It’s actually an Xbox One game but still looks great on the Series X.
Tunic - This featured heavily in “Game of the Year” lists last month and deservedly so. It looks like a cross between Zelda and Death’s Door but the unique mechanic is that you uncover parts of the instruction manual as you play. It’s all written in a strange language so you have to try and work out what is going on yourself, similar to importing Japanese games back in the ‘90s. One clever twist is that most of the things in the manual are available immediately, you just don’t know how to trigger them. A lot of fun but I’d definitely suggest turning down the combat difficulty.
Board Games
Paperback Adventures - I have no idea how I found this game as I’ve never played it’s predecessors, Paperback and Hardback. In any case, I ordered it and then upon realising you had to buy a character box for it as well ordered that and finally got up and running this week. It’s a solo-player roguelike in which you battle enemies by spelling words. There is a very interesting “splaying” mechanic using the two edges of your letter cards to determine hits and blocks. I like it very much! Just be sure that you buy one of the three character boxes along with the core box as you do need both parts!
TV Shows
Slow Horses - This has been on my “to watch” list for a while but the number of people that mentioned it in “best of 2022” lists caused me to binge both seasons in 3 nights. It’s excellent. The premise is that there are a bunch of useless MI5 agents who get given the bad jobs rather than being fired but they somehow manage to get tangled up in a real case. An excellent thriller with some hilarious writing for Gary Oldman’s character. I also got a big belly laugh out of River Cartwright saying “not again” in Season 2 (keeping that as spoiler-free as possible).
The White Lotus - Another one that scored highly in “best of 2022” lists, this follows a number of wealthy guests in a Hawaiian resort as somebody gets murdered. I don’t know much more than that as I’ve only watched a couple of episodes but it is excellent so far! I’m particularly looking forward to season 2 which stars F. Murray Abraham and Aubrey Plaza.
Roadmap
The roadmap is my way of committing to what I’m going to do over the next 2 weeks.
5th - 18th January
Keep working on Music Library Tracker v2.0 including some UI changes on iPad ✅
Added a Stats page to Game Track with Swift Charts. Also used a new iOS 16 API (“Shared With You”) ✅
Finished working on Back Seat Shuffle and made the code public on GitHub ✅
Another fortnight with everything done! Next issue there is only one thing I want to achieve, but it’s a big one:
19th January - 1st February (Issue #5)
Release Music Library Tracker v2.0
That wraps it up for this issue. I hope you found something of interest and that you’ll be able to recommend the newsletter to your friends, family, and colleagues. You can always comment on this issue or by emailing me directly via ben@bendodson.com
One such piece of code was a Tinder-style swiping mechanic I built for an as-yet-unreleased app called Playlist Precis (you’ll hear about it soon!) which I was then able to use in it’s entirety in a job hunting app for a client.
iOS 16 did fix this by bubbling them up to showing under the search bar when it was empty but the link had stopped working for some reason; no idea why as if you searched and tapped on one it would work so some kind of Spotlight bug.
A good example is Illustrated which I added to IGDB this morning! The only other games I’ve had to add are some side-loaded Playdate titles.
Neither John or myself play games on Linux so if there are any minutes linked to Linux from the Steam API then that means it was a Steam Deck (which is Linux based).
If you’re not a programmer then you’re likely not aware that most dates are stored in reverse order i.e. 2023-01-16. This gets around localisation issues (*cough* America *cough*) but also means you can sort in ascending or descending order incredibly easily. It was also very nice for me filtering by month or year as you can just grab the prefix, a fast action in a filter.