5 min read

Dev Notes #1 – Feature Parity

Inspired by _DavidSmith, I'm going to try posting more-frequent, medium-length technical posts here. Objectives: share my learnings, solidify my knowledge, and open myself up to new ideas and feedback.

Last Friday was my last day at Confluent. I am not seeking another full time job at this time, but instead taking the time to work on FoodNoms and potentially other projects. I'm excited to be able to spend more time on FoodNoms and see if I can get it its growth to accelerate.

Yesterday wasn't much different than a normal working day for me. Same desk, same office, similar laptop. Except instead of Visual Studio Code, I spent all day in Xcode. 🤪

I wish I wasn't running right into dev work, but my #1 priority right now is to wrap up FoodNoms 2 and get it out the door. In case you don't know, FoodNoms 2 is a big update that I've been working on for… checks notes… over two years. It's a major rewrite and includes several of the top feature requests from FoodNoms users over the past few years.

Goal Details Screen

The primary objective yesterday was to achieve feature parity with FoodNoms 1. The last big feature that was missing was this screen (I call this screen "goal details"):

I first started getting the nav and layout in. I copied over the source files from FoodNoms 1 and ended up with dozens of compile errors. Most were easy to fix – just converting to use new APIs. For stuff like the chart and data loading, which had to be completely reimplemented, I just commented-out those lines to get something running.

Next step was to get the data loading. This was the most arduous part of the refactor as FoodNoms 2 has a new layer of abstraction dealing with goals and charts: GoalEvaluation. This is a model struct that is responsible for computing stats related to how a goal should be evaluated (hence the name) for a given day. It provides answers to questions like "was the goal successful on this day?" I also have a Combine publisher that makes it super easy to get a set of goal evaluations for a given time period.

Switching over to use the GoalEvaluation struct and associated publisher was a lot of work, but the end result is much nicer. Way more code shared with other parts of the codebase, and dramatically simplifies the implementation in this view.

Interactive Charts

Next up was the chart. While everyone is busy rewriting their apps in SwiftUI, I actually made the deliberate decision a while ago to rewrite some SwiftUI views in UIKit. The chart component is one of them.

This view is a SwiftUI view though, so I needed to wrap my UIKit-based chart UIView in a UIViewRepresentable. It wasn't hard at all! The key thing I needed to do was override intrinsicContentSize in my UIView subclass – otherwise the chart wasn't visible.

Next up was making the chart interactive. This was fairly straightforward – I took my SwiftUI DragGesture and was able to port most of its logic over to a UILongPressGestureRecognizer. I initially tried implementing this by overriding touchesMoved/touchesEnded, but the touches were getting cancelled by the parent UIScrollView when moving the touch position vertically. Rewrote to use the long press gesture recognizer and it behaved exactly how I wanted, thankfully!

The level of interactivity for these bar charts is pretty simple/rudimentary. You can drag and select a bar, it highlights, and some stats show up above specific to that bar. So once I got the gesture recognizer in place, just needed to add in some additional states and callbacks for when a bar is selected or deselected.

Date Math Problems

Once I got the data loading and chart rendering, I noticed some incorrect behavior when switching to the weekly and monthly time intervals.

From the debugger, I noticed some funky results from some date math.

One of the best technical choices with FoodNoms 2 is the full embrace the concept of a "Day" with a Day struct and various encodings, instead of using Date to represent days.

I have a lot of little unit tests for my Day struct, which provides a lot of conveniences and transformations. From my observation in the debugger, something weird was going on with the startOfWeek function. When calling startOfWeek on a Day, it was sometimes returning the day for the next week.

First thing I did was pull up DayTests.swift, but 😱 it wasn't there! Turns out, this file and a few other test files got deleted a few months ago in another refactor. Once I recovered those, I was relieved that the tests still passed. But something was still wrong – I added some additional tests to track down the edge case that was broken.

It has to do with handling for the "first weekday" setting (configured by the user in iOS Settings.app > General > Language & Region > First Day of Week). I learned yesterday Calendar.firstWeekday is actually one-based, not zero-based. I had a lot of tests for when the first weekday is Sunday and Monday, but those were all incorrect as I was configuring the firstWeekday so Sunday = 0 and Monday = 1, but it should've been Sunday = 1 and Monday = 2. This led me to realize the actual problem with my startOfWeek, which was overcomplicated. By simply removing an additional step, I fixed the bug (best kind of bug fixes!)

Best kind of bug fix!

Top Foods

The top foods section was mostly a copy/paste from the existing implementation, with a few tweaks and fixes. Very thankful I wrote my top foods query implementation to be ambivalent about the data source. It "just worked" despite the underlying model layer being completely different than what exists in FoodNoms 1.

Perf Optimizations

I've spent a ton of time in Instruments lately. Lots of perf work across various parts of the app. I noticed when interacting with the monthly chart, the chart wasn't immediately redrawing.

Found some low-hanging fruit in the time profiler. Instead of dynamically computing the "yyyy-mm-dd" string format for series on the x axis, I precompute that value in the struct's initializer.

Screenshot of time profile results where Day.string is cumulatively taking several hundred milliseconds

The interactivity on the charts is a lot faster on a non-debug build, so not entirely sure how important these optimizations were in the end. Regardless, these optimizations do tend to add up.

After a "heads down" sort of day, I was super happy that FoodNoms 2 has officially achieved feature parity with FoodNoms 1! 🎉 This is a huge milestone as it means that FoodNoms 2 is now much closer to being ready to ship.

That's it for my first dev notes post. Let me know if you learned something, thought this was interesting, or have any other feedback to share! These days I like to hang out over on Mastodon.