Build notes / Sunfall / 07 September 2026
24 hours from an empty folder to a finished iPhone app.
The plan was one day: idea to App Store submission, no extensions. The build hit that mark. Then I decided to miss it on purpose. Here is what the day actually contained, including the four bugs and the one step nobody can automate.
- Time to a shippable build
- 24 h
- Bugs found before release
- 4
- Found by a compiler
- 0
- Steps that resist automation
- 1
01 / The day
What twenty four hours held.
Roughly in order. The parts that took longest are not the parts I expected to take longest.
- FirstDecide what it refuses to doNo account, no server, no analytics, no subscription. Every one of those is a day of work I did not have, and each one would have been the reason the app never shipped.
- ThenName itLonger than expected, and the closest thing to a real failure in the whole day. Details below.
- ThenWrite the maths in Python firstThe NOAA solar position algorithm, validated against an independent library across ten cities, before a line of Swift existed.
- ThenPort it to Swift and prove the port588 values, compared bit for bit against the Python. Two sign bugs fell out here.
- ThenBuild the interfaceThe fastest part of the day, and the part everyone assumes is the whole job.
- ThenRun it on a real phoneWhich immediately produced a fourth bug, a redesign, and three features I had not planned.
- ThenWrite the store listing, privacy policy and support pagesBoring, unskippable, and the source of the one embarrassing mistake.
- LastArchive and uploadThe step that cannot be done from a terminal, for a reason worth knowing.
02 / Naming
The name was the first real research problem.
The obvious name for a golden hour app is Golden Hour. It is taken outright, plus five or more prefixed variants. Sunward has a direct competitor already shipping a sun tracker under almost exactly that name. First Light is thoroughly colonized by devotional apps, which is a category you cannot out rank and would not want to confuse yourself with.
So I searched the App Store programmatically, and the search told me a handful of names were clear. It was wrong. Sampling the iTunes Search API shallowly gives you a confident answer built on the first page of results, and shipping an app name on that basis is how you find out about the competitor after launch.
Two changes fixed it. Match the whole word rather than a substring, across the full two hundred result depth the API will return. Then check the finalists by hand in the App Store app itself. That manual pass caught three competitors the API pass had missed, which is a humbling ratio.
The general lesson is not about app names. It is that a script which returns a clean result is not evidence of a clean result until you have checked what it actually looked at.
03 / Method
Write the reference implementation first, in the wrong language.
The temptation on a one day build is to write the solar maths straight into Swift and eyeball the output against a website. That feels faster and it is a trap, because you have no way to tell a subtle error from a correct answer. Sunrise being twenty minutes off does not look wrong. It looks like sunrise.
So the NOAA solar position algorithm went into Python first, where a mature reference library exists to disagree with it. Ten cities, both hemispheres, from the equator to Anchorage, agreeing within 2.2 minutes. Only then did it become Swift, and the Swift was checked against the Python across 588 values rather than against intuition.
The same discipline caught more on the lunar side, which is much harder maths. The moon engine follows the standard Meeus chapters, and its 120 periodic terms are generated from the Python source rather than typed in by hand, because a hand typed coefficient table is a bug waiting for a quiet moment. Checked against Skyfield and the JPL DE421 ephemeris, it agrees to 0.014 degrees in altitude and 0.054 degrees in azimuth, then reproduces byte identically in Swift across 8,190 values.
04 / The bugs
Three sign errors, none of which the compiler could see.
Every bug found in the maths was a sign, and every one compiled perfectly.
- Swift's remainder is not Python's.
truncatingRemainderkeeps the sign of the dividend where Python's%does not. True solar time needs a floored modulo, so for any instant before UTC midnight the sun's azimuth came out wildly wrong while the sunrise time looked fine. - An inverted timezone offset. Local midnight is at minus the offset from GMT, not plus. Getting it backwards shifted the entire day's arc by twice the offset, which is eight hours on the west coast in summer.
- A parallax correction applied backwards. The moon is close enough that where you stand on Earth moves it in the sky. The hour angle is local sidereal time minus right ascension, so a positive shift in right ascension lowers the hour angle. I had it the other way round, which made the correction worse than not correcting at all.
A fourth was subtler and more interesting: refraction was being counted twice in rise and set times, once in the position and once in the threshold. Rise and set are now solved on the geometric altitude with refraction folded into the threshold, the same way the solar engine does it, because refraction models near the horizon disagree with each other most severely at exactly the altitude where rise and set happen.
There was also a phantom. My lunar right ascension differed from the reference by 0.39 degrees, consistently, which looked like a real bug for a while. It was precession: I was computing in the equinox of date and comparing against J2000. The fix was to ask the reference the same question I was asking, not to change my maths. Worth remembering that a stable, suspicious offset is often a definitions mismatch rather than an error.
05 / The bug that only runs at night
Green build, green tests, blank screen.
The status block at the top of the app, the one that says what happens next, went blank every night between astronomical dusk and midnight. The code compiled cleanly and every test passed, because the tests asked about times of day and the failing state was a time of day nobody had thought to ask about: the window when today's named moments are all spent and tomorrow's have not started.
It is the ordinary shape of the bug in a time dependent app. There is a branch nothing in the test suite reaches, and the only thing that reaches it is the clock. The fix was to roll into tomorrow's day when today runs out, which is also the right behaviour: the person opening a sun app at eleven at night is planning the dawn.
06 / Design
The icon looked better than the app.
That was the note I wrote after the first run on a real phone, and it was correct. The first version of the hero view drew golden hour and blue hour as flat blocks of color behind the sun's arc. On a screen it did not read as sky. It read as a bar chart, and a bar chart is a thing you decode rather than a thing you glance at.
The redesign runs the sky color horizontally, keyed to the sun's altitude at each moment, so the warm band is golden hour rather than labelling it. Two other decisions followed from putting it on a device. The window became a rolling 24 hours centred on now rather than a chart of today, because the moon's arc crosses midnight and a day view splits a moonlit night into two disconnected fragments. And the sun became warm cream against a cool blue moon, because when both were pale they were genuinely hard to tell apart.
Running it on real hardware also added a settings sheet, a place picker and an in-app feedback path, none of which were in the plan. That is the argument for building the thing rather than specifying it.
07 / The unglamorous part
The support address bounced, after it was published.
The privacy policy went live with a contact address that did not exist. It bounced within seconds of the first real test. The address had already been published, which is the part that stings: a privacy policy is the one document where a dead contact address is not a small mistake.
Two things made it easy to get wrong. I had convinced myself the alias existed without ever sending to it, and when I did test, I tested from the same account, and Gmail quietly deduplicates mail you send to yourself. A send that appears to work proves nothing. Send from a different mailbox, and confirm the message arrives in the inbox rather than assuming.
A second one in the same family: the site's content security policy blocks inline scripts, and the CDN in front of it was helpfully rewriting every email address into an obfuscated placeholder that needs a script to decode. The address was on the page, in the source, and invisible to every reader. Two protections, each sensible, that cancel each other out.
08 / The last mile
The one step that cannot be automated.
Almost all of this was done over SSH. Build, test, run in the simulator, capture screenshots, all fine from a terminal on another machine. Archiving and uploading to App Store Connect is not, and the reason is worth knowing before you plan a pipeline around it.
macOS gives an SSH session its own security session, and the login keychain belongs to the graphical one. From SSH you cannot reach it, which means you cannot reach the code signing private key. It is not a permissions problem you can grant your way out of. An App Store Connect API key does not fix it either, because that solves Apple authentication, and the missing piece is keychain access. Signing into Xcode alone does not create a certificate; Xcode only mints one when a build demands it.
So the final step is a human at the machine: Any iOS Device, Product, Archive, Distribute App. Everything upstream of that can be scripted, and knowing exactly where the automation stops is more useful than pretending it does not.
09 / Afterwards
The deadline was the feature.
A widget and cloud cover forecasting were both obvious v1 features and both were cut before a line of either was written, specifically to protect the day. That was the right call. Then, once the app was on a phone and working, I chose to hold the release anyway to add the moon, which was not the plan and took the total past twenty four hours.
Both decisions were the same decision made twice with different information, and I would make both again. The deadline was what stopped a sun app from becoming a weather app. Seeing it run was what showed the moon was not a feature, it was half the point: light does not stop at sunset.
The honest status, at the time of writing, is that Sunfall is built, tested and finished, and the archive is the next thing to happen. The purpose was never market share. It was to push something real through the whole pipeline once, so that the next one is a known quantity rather than a research project. That part already worked.