Accessing the UI in Xamarin.iOS

Oliver Brown
— This upcoming video may not be available to view yet.

An interesting discussion happened recently, triggered by a code review of (something like) the following C#:

public class ProductViewController
    private UIView _detailsView { get; set; }
    private ProductViewModel _viewModel { get; set; }

    private void Process()
        if (_viewModel.ProcessDetails)
            InvokeOnMainThread(() =>
                if (_detailsView != null)

One of the reviewers suggested moving the second if outside the call to InvokeOnMainThread (and then combining it with the other if).

The theory was that it would more efficient to check if we need to be on the main thread before we do it, instead of doing the check on the main thread and finding out we have nothing to do.

The original author pushed back saying you can’t access _detailsView off the main thread since you would get an exception.

On the surface this sounds reasonable - everyone knows you can only access UIKit objects on the main thread. But it naturally leads to the question: what does “accessing a UIKit object” actually mean?

So I wrote a quick sample that intentionally tries to do lots of UIKit manipulation on background threads in various ways:

The tests

Quick refresher. The View property of a newly constructed UIViewController is not populated until it is first accessed. Some of the tests below refer to accessing this property either before or after it is created. By default properties declared in C# will not be visible to any native iOS code. They can be made visible by adding the [Export] attribute.

  • Create a UIViewController.
  • Create a UIView.
  • Create a UIColor.
  • Check if a view controller’s View property is equal to null.
  • Check if a view controller’s View property is null using pattern matching.
  • Check if a view controller’s IsViewLoaded property is equal to true.
  • Check if a view controller’s View property is equal to null after previously creating View on the main thread.
  • Check if a view controller’s View property is null using pattern matching after previously creating View on the main thread.
  • Check if a view controller’s IsViewLoaded property is equal to true after previously creating View on the main thread.
  • Check if a new UIView property, that is not exported is equal to null.
  • Check if a new UIView property, that is not exported is null using pattern matching.
  • Check if a new UIView property, that is exported is equal to null.
  • Check if a new UIView property, that is exported is null using pattern matching.
  • Check if a view controller’s NavigationController property is equal to null.
  • Check if a view controller’s NavigationController property is null using pattern matching.
  • Set a new UIView property, that is not exported, with a view created on the main thread.
  • Set a new UIView property, that is exported, with a view created on the main thread.


Test Result
Create UIViewController Exception
Create UIView Exception
Create UIColor OK
Check if View is equal to null Exception
Check if View is null pattern Exception
Check if View is loaded Exception
Check if View is equal to null after creating View Exception
Check if View is null pattern after creating View Exception
Check if View is loaded after creating View Exception
Check if non-exported view is equal to null OK
Check if non-exported view is null pattern OK
Check if exported view is equal to null OK
Check if exported view is null pattern OK
Check if NavigationController is equal to null Exception
Check if NavigationController is null pattern Exception
Set non-exported view OK
Set exported view OK


This is by no means exhaustive, but it seems in general, acessing properties declared natively in iOS will throw an exception whereas accessing properties you have declared yourself will be fine. I had a suspicion that an exported view might behave the same as a native property, but apparently not.

The code for this test is available on GitHub.

Charles Dunstone in the news

Oliver Brown
— This upcoming video may not be available to view yet.

Charles Dunstone, a well known entrepreneur and founder of Carphone Warehouse has quit as chair of the Royal Museums Greenwich.

Apparently, the government refused to reappoint a trustee because they advocated for “decolonising” the curriculum. More information is available on the Financial Times.

Normally I wouldn’t be interested in posting about such things, but my blog has a bit of history with Charles Dunstone. He was in charge of UK ISP TalkTalk when it came out. Some people had problems and people complaining about it resulted in my most popular blog post.

Some of the complaints led to some interesting solutions, and I briefly included Charles Dunstone’s email address.

The future of Microsoft MAUI (and Xamarin Forms)

Oliver Brown
— This upcoming video may not be available to view yet.

Since Google seems to like my post about the future of Xamarin Forms so much (and I have a slight history of such posts), I’d figure I’d post an update about interesting things happening in the Xamarin Forms repo specifically related to MAUI.


The change that actually made me write this post - a large PR with 5000+ changed files that changes the Xamarin Forms name to MAUI.

Not much of a thing for actual functionality, but a significant symbolic milestone.

Handlers and the great big architecture shift

.NET MAUI will completely change the way renderers are handled in Xamarin Forms. There are many advantages of doing it the new way, but the mechanics of how it is done are fairly complex. This video by Javier Suárez covers it well.

Interacting with this video is done so under the Terms of Service of YouTube
View this video directly on YouTube

This is all happening right now in the main-handler branch.


While I was writing this, work officially moved to the dotnet/maui repo and it is accepting pull requests directly.

AppHost and Microsoft.Extensions

Originally an ASP.NET concept, that then migrated its way to Windows client development, this provides a common way to provide dependency injection, logging, and lots of other infrastructure stuff. In isolation, the pattern and implementation is good and will make it easier to override certain things in MAUI (such as handlers). It’s also useful in a wider sense since it will make configuring different styles of .NET apps more similar.

Single project

Over the past couple of years there has been a move towards producing Xamarin libraries (and .NET libraries in general) using a single multi-targeted project. The most significant is probably Xamarin Essentials. This PR adds support for creating applications following the same pattern.

Merging in Xamarin Essentials

There is a lot of functionality in Xamarin Essentials that Xamarin Forms would like to use. Likewise there is some functionality in Forms that is useful when not using Forms. This lead to some overlap in functionality (and occasionally overlap in APIs but not a perfect match in functionality).

There was an attempt to add Essentials as a dependency of Forms but it faced some problems, and there was a “change of plans”.

Now the solution is to have Forms and Essentials in the same repo. I hope Essentials remains available as its own Nuget package (and it looks like that will be the case).


Resizertizer.NT, like its predecessor Resizetizer, is a package for generating platform specific images in all the right sizes at build time.

Managing image assets across iOS and Android (and using Visual Studio) has always been an unpleasant process. This tool makes it much easier and will be included in MAUI by default.

When can I reuse my calendar?

Oliver Brown
— This upcoming video may not be available to view yet.

This was a question that came up recently, and the answer is interesting enough I decided to step through the process of working it out.

A good year

There are seven days in the week. If the year had a number of days in it that was divisible by seven (say 364), then the answer would be easy: every year and every date would fall on the same day every year.

This would have some nice properties. Some of the more complicated holidays that need to happen on certain days would get simpler, and the poem about what kind of person you are based on the day of your birth would be more poignant since your birthday would be the same each year.

A normal year

But alas, the number of days in the year is not divisible by seven, and we have to suffer with a different calendar each year. But there is good news, there are only seven possible calendars - one starting on each day of the week.

With 365 days in the year, we would have the following possible calendars:

  • Monday 1st January, Tuesday 2nd January … Monday 31st December
  • Tuesday 1st January, Wednesday 2nd January … Tuesday 31st December
  • Wednesday 1st January, Thursday 2nd January … Wednesday 31st December
  • Thursday 1st January, Friday 2nd January … Thursday 31st December
  • Friday 1st January, Saturday 2nd January … Friday 31st December
  • Saturday 1st January, Sunday 2nd January … Saturday 31st December
  • Sunday 1st January, Monday 2nd January … Sunday 31st December

After we have cycled through those calendars, the pattern starts again. Hooray.

Leap years

Of course it is not that simple. Every four years we have a year with an extra day in it, throwing everything off. And to fix it we will need Maths.

We have a cycle of seven starting days, overlaid with a four year cycle of leap years. To determine when the large cycle repeats we need to calculate the least common multiple of four and seven. Since four and seven are coprime (they have no common divisors), their least common multiple is their product.

4 × 7 = 28

So you will be able to reuse your calendar every 28 years.

You can check this is correct by looking at the calendar for 2020 and the calendar for 2048.

Going one better

Knowing when the whole cycle repeats give you an answer to when you can reuse your calendar, but not necessarily the best answer. Applying a bit of logical reasoning, you can work out that there are only 14 possible calendars: one for each day of the week in a leap year, and one for each day of the week in a common year.

A non-leap year is also called a common year.

Sadly, at this point clever maths is probably less illustrative than a brute force approach. Since we know the cycle is only 28 items long, the force is not that brutish…

I shall name the possible calendars with a number indicating the day of the week they start on (1 for Monday), and a letter L or C, indicating a leap year or common year respectively.

Lets start with a common year:


As you may have noticed from earlier, the year after a common year begins with the next day.

C1 C2

We can continue the pattern up to the first leap year, since leapyness does not affect how a year starts.

C1 C2 C3 L4

But what next? A leap year has an extra day, so instead of going up by one, we go up by two.

C1 C2 C3 L4 C6

We are now ready to complete the pattern (remembering after 7, we go back to 1)

C1 C2 C3 L4 C6 C7 C1 L2 C4 C5 C6 L7 C2 C3 C4 L5 C7 C1 C2 L3 C5 C6 C7 L1 C3 C4 C5 L6

The cycle is 28 years long, and there are 14 possible calendars so you might naïvely think each one appears twice, but that isn’t the case. The leap years appear once each, and the common years appears three times each.

This means for leap years, our guess at waiting 28 years to reuse a calendar is correct. For common years, you can reuse your calendar, on average every 9⅓ years, but in a seemingly irregular pattern.

Making it useful

For any given year, you only need to know when you are relative to a leap year to work out when a calendar can be reused:

Year Years until next reuse
Leap year 28
Year after leap year 6
Two years after leap year 11
Year before leap year 11

So if it is two years after a leap year, for example 2018, you can reuse your calendar 11 years later, in 2029.

If you want to calculate the next reuse after that, then the method is straightforward, but hard to explain clearly. First, I’ll provide a variation of the above table with just common years.

Year Years until next reuse
Year after leap year 6
Two years after leap year 11
Year before leap year 11
Year after leap year 6
Two years after leap year 11
Year before leap year 11
Year after leap year 6
Two years after leap year 11
Year before leap year 11
Year after leap year 6
Two years after leap year 11
Year before leap year 11

To work out the reuse after the next reuse, step backwards through the table (wrapping round when you get to the beginning).

So, if it is 2019 (a year before a leap year), the next reuse is 11 years later in 2030. This is two years after a leap year, so the next reuse is found by reading the row above - 11 years later again in 2041. Now we are a year after a leap - repeating the process and reading the row above we find the next reuse is 6 years later in 2047.

This can be summarised in the following decision tree:

Taking it further

What we have so far will work for the vast majority of people, at least for working out calendars within your own lifetime. But if you want to work out years close to 2100, it will fail.

The reason is that the idea that a leap year is every four years is not the whole story. If the year is also divisible by 100, then it is not a leap year. So 2100 is not a leap year. And nether was 1900.

But… what about 2000? If you check a 2000 calendar you’ll find February 29th. Turns out there is another rule to leap years - if the year is also divisible by 400, it is a leap year. So 2000 was, and 2400 will be.

The reason these are added are to make up for the fact that the length of the year is not nicely divisible by the length of the day. There are currently no further rules but the day will drift again at some point.

Matt Parker made an excellent video explaining this.

Interacting with this video is done so under the Terms of Service of YouTube
View this video directly on YouTube

There is a proposal to make every year divisible by 4000 into a common year but based on historical precedent we will probably completely change the calendar before that happens.

Current year Next reuse Difference
2980 Leap year L6 3020 40
2981 Year after leap year C1 2987 6
2982 Two years after leap year C2 2993 11
2983 Year before a leap year C3 2994 11
2984 Leap year L4 3024 40
2985 Year after leap year C6 2991 6
2986 Two years after leap year C7 2997 11
2987 Year before a leap year C1 2998 11
2988 Leap year L2 3028 40
2989 Year after leap year C4 2995 6
2990 Two years after leap year C5 3002 12
2991 Year before a leap year C6 3003 12
2992 Leap year L7 3004 12
2993 Year after leap year C2 2999 6
2994 Two years after leap year C3 3000 6
2995 Year before a leap year C4 3007 12
2996 Leap year L5 3008 12
2997 Year after leap year C7 3009 12
2998 Two years after leap year C1 3010 12
2999 Three years after a leap year C2 3005 6
3000 Four years after a leap year C3 3006 6
3001 Five years after a leap year C4 3007 6
3002 Six years after a leap year C5 3013 11
3003 Year before a leap year C6 3014 11
3004 Leap year L7 3032 28

At first glance that table looks unhelpful - the numbers in the last column are all over the place. But, with a bit of careful thinking it’s only a small extension to the previous method.

The final algorithm

Any more issues

I have assumed two calendars are the same if they start on the same day and have the same number of days. But there are other differences, like when certain holidays occur.

Some of the holidays will change in the same way and so won’t matter. For example in the UK, Christmas Day is a bank holiday. If it happens to fall on a weekend, then the next weekday is a bank holiday in lieu.

Many holidays however do change, and it is mostly to do with the moon. For instance the definition of when Easter occurs in western Christianity is:

the first Sunday after the first full moon that falls on or after the vernal equinox

To make things a bit weirder, it’s not really the vernal equinox, but March 21 (the vernal equinox is often on March 21, but in reality happens some time between March 19 and March 22 and can vary with timezone). It’s also not the full moon in the astronomical sense, but the Paschal full moon, which is an approximation of the astronomical full moon based on the 19-year Metonic cycle. Full details here.

Wikipedia has a table of dates comparing Gregorian Easter and Astronomical Easter.

I decided not to try and calculate calendar reuse with these details considered.

Orthogonal by Greg Egan

Oliver Brown
— This upcoming video may not be available to view yet.
As an Amazon Associate I earn from qualifying purchases. See here for more information.

I was inspired to write this review after watching Christopher Nolan’s movie Tenet. The central idea of Tenet is explored in the Orthogonal series (amongst many other things) but with more rigor and detail. As such, this review contains spoilers about the premise of Tenet. I won’t cover the details of Tenet’s plot, but if you were already going to watch Tenet, do that first.

Despite ostensibly being a review, this is mostly a piece designed to persuade you to read these books. It contains a fairly detailed explanation of the initial premise for the book (while avoiding spoilers for the plot), because talking about the things that are good is difficult otherwise.


The movie Tenet follows in the wake of Interstellar by taking a confusing scientific concept and creating a movie that can be compelling for a mainstream audience. In Interstellar this was the idea of time dilation, in Tenet it is the idea of the “arrow of time”, and whether, perhaps, it is not as immutable as it seems.

The one line conceptual spoiler for Tenet is the following: there are machines that allow people and objects to travel backwards in time, while in the same space as people traveling forwards. Interactions between normal people and “inverted” people are strange and unintuitive.

In the movie, the details of “how” are basically ignored. And although the plot is clever and demonstrates much of the oddness that would be present, there is little detail.

Riemannian space

The premise for Orthogonal is a simple change to a single formula that defines the geometry of space-time. There is a detailed explanation by the author himself, but a summary is as follows:

  • In two dimensional space, the distance between two points can be found using the Pythagorean theorem: d = √(x² + y²), where x and y are the differences in the points’ positions in the x-axis and y-axis respectively.
  • A very similar formula also works for three (and higher) dimensions: d = √(x² + y² + z²).
  • General relativity combines time and space together to form a single thing called spacetime.
  • It is possible to calculate the “distance” between two points in spacetime. However, in this case the formula features a minus sign: d = √(x² + y² + z² - t²).

This kind of spacetime is described by the author as “Lorentzian”. That minus sign is a consequence (or a cause, depending on your philosophy) of the speed of light being constant, and appears to describe our universe.

In the universe of Orthogonal however, the spacetime is “Riemannian”. That is, the distance between two points in spacetime is just: d = √(x² + y² + z² + t²).

This is summarized by saying the universes have metrics of +, +, +, − and +, +, +, + respectively.

The Arrow of Time

This small change has a lot of consequences. The biggest is that there is no longer a universal speed limit. You can go as fast as you want, even reaching a sort of infinite velocity. Just like in our universe there is time dilation at high velocities. At “infinite velocity”, time stops passing completely for a stationary observer. In the universe of Orthogonal however, if you were to continue accelerating you would find that time has started to progress backwards for you, relative to a stationary observer.

The exact mechanics of this may sound confusing, but are explained well in the book, and it more detail on the author’s site.

An actual review

With all the preliminaries out of the way, it’s time to explain why I think Orthogonal is so good. It basically does three things, and it probably could have stopped at the first one and still have been good.

1. Hard sci-fi with a relevant plot

The story is “hard sci-fi”. It has a universe that has solid rules that are as scientifically rigorous as possible and tries to avoid deviating from them. In order to also be a good sci-fi story though (as opposed to just good science), it also needs a plot that allows the reader to explore that universe.

Orthogonal presents the main characters with a world ending disaster. Not a terribly unique idea in the abstract, but in this case the disaster is one that could only happen in the Riemannian spacetime it presents.

Very quickly, the characters devise a plan to avert disaster. In the best traditions of disaster movies, it initially sounds crazy, but it just might work. And like the disaster itself, the proposed solution would only be possible in Riemannian spacetime.

2. Explore some further detail of the universe

Orthogonal is a trilogy of novels, spanning a significant time period. A lot of less apocalyptic consequences of Riemannian spacetime are explored. One of the more surprising is a running story of social development.

In the world of Orthogonal, women are not treated well. The way this manifests is very similar to our own world (so similar that it reminds me of classic allegorical Star Trek episodes). However the reason for the treatment in the story is, again, something that could only happen the world of Orthogonal (and is in fact a direct result of of the physics possible in Riemannian spacetime).

3. First principles

The final thing Orthogonal does, and the thing that takes the story from being “good” to being absolutely staggering, is how the details of the world are presented.

It starts in a world with a similar level of technology to us at the beginning of the 20th century. The author makes no overt attempt to explain the world narratively. For the reader it appears initially that the characters inhabit a world that behaves very differently to our own for no reason. But, the main characters in the story are striving for knowledge (after all they soon have a world ending disaster to avert), and so they start to learn how the world works.

As the plot develops over several generations, we follow scientists and engineers as they untangle the physics of the universe. We learn with them as the details of how the Riemmannian equivalents of electromagnetism, thermodynamics, relativity and quantum mechanics work.

That is the what makes Orthogonal really special. Not only working out how this strange universe works, but coming with a plausible way that characters in the universe might be able to work it out for themselves.


Stephen Hawking said that he was warned every equation added to a popular science book would halve the number of sales. Despite being a work of fiction, much of Orthogonal feels like a popular science book but the author has not let such an idea concern him. It is full of equations and diagrams. I wouldn’t say it is impossible to enjoy the book if you chose to ignore them (or at least not completely engage with them), but you would be missing out on a lot.

If, on the other hand, that kind of detailed explanation appeals to you, there is a site (all created personally by Greg Egan himself) dedicated to providing way more detail on all the physics and maths explored in the book.

Orthogonal is hard sci-fi. Conceptually it may be the least accessible of Egan’s works. It is, however, spread over three novels to avoid overwhelming the reader and plenty of time is dedicated to exploring how the characters deal with this world, and that can be enjoyed independently of understand why and how it works.


Actually, the least conceptually accessible work from Greg Egan is probably Dichronauts, a story set in a universe with the metric +, +, −, −. That universe is so weird that you can’t turn past ninety degrees, and as you get closer to it, you stretch towards infinity.

Buy it on Amazon


Buy it on Amazon


Buy it on Amazon


Buy it on Amazon


Mobile apps, ads and consent

Oliver Brown
— This upcoming video may not be available to view yet.

I’ve recently spent some time adding the Google User Messaging Platform (UMP) to Tic-tac-toe Collection. The motivating factor was needing to support App Tracking Transparency in iOS 14 when using AdMob, but more generally, due to the GDPR, apps have significant requirements around consent and data management (especially with ads) and my haphazard custom implementation was a pain and probably not entirely compliant.

My effort to do this led me to learning quite a bit about the current state of this in apps and on the web.

Aside - Technical implementation

UMP is provided as a native library for both Android and iOS. There are official Xamarin bindings available for each (they are very new, and for a while I had to bind the iOS implementation myself).

I created a cross-platform wrapper to make using it easier in a Xamarin Forms app.

Ad consent
Ad consent

Google UMP is an implementation of a Consent Management Platform (CMP) as defined by the IAB (I tried to find a link to a neutral definition, but if you search Google for “Consent Management Platform”, you get a whole load of ads for companies wanting to help you).

This will look very familiar to most European users (and probably others) since most websites display a popup with a similar setup. What I did not realize until investigating this is that the information and settings it contains (down to the exact wording in some cases) is very proscribed and there it not much leeway about how the information is presented (possibly explaining why they all feel so universally unpleasant to use).

More surprising, at least initially, is that the way this information is stored is actually really well defined as part of shared API - the Transparency & Consent Framework by the International Advertising Bureau Europe. The popup which you provide consent through makes the information available to the hosting site (as well as advertising components on the page) in a predictable way.

A lot of the information is contained in this GitHub repo, with the API defined here.

The web API is really well specified and provides a JavaScript object available for anything on the page to query to get current consent status. The mobile API is a bit less so. Basically the data is stored in the shared preferences for the app (SharedPreferences on Android and NSUserDefaults on iOS) with well known keys you have to query yourself.

(Mainly just for fun, I created a package for querying this data with Xamarin.)

There a few things about the spec that I found interesting:

  • The “purposes” for which data can be used are very specifically defined, and numbered. Many things refer to them by number.
  • The list of vendors is also centrally determined and numbered.
  • There is what appears to be a typo in the spec: property names include VendorConsents, PurposeConsents and then PublisherConsent. This last one is structured the same as the others, but named in the singular. And since, like HTTP referer, it wasn’t caught before publication, it is now set in stone.

Some problems

There are some problems with this system. Some are general and some are specific to Google’s implementation.

The biggest is the language presented to the user is still very verbose. The whole point of the GDPR was to give users more insight and control about how their data is used. I seriously doubt many people will actually make an effort to understand all the purposes and the consequences of them.

One weird limitation of Google’s implementation is that it is impossible for the user to see their previous consent status and to tweak it slightly. When you first display the form, everything is turned off. The user can select to accept everything or dig through and accept individual permissions. If the form is presented again, everything is turned off again. If the user wanted to revoke or grant a specific permission, they’d have to remember themselves what they chose last time.

Even worse, there is no way to exit the form without making changes. If you launch them form, and select “Manage options”, your only way out saves the “everything off” state.

Purpose one

The first numbered purpose is “Store and/or access information on a device”. This is essentially a technology agnostic way of referring to a cookie. It doesn’t actually allow an app or website to do anything itself, it has to be combined with another purpose. And interesting things happen if the user does not grant it.

Most significantly, Google Mobile Ads (whether using AdMob or Google Ad Manager) will refuse to display any ads at all. In theory any app using Google Ads should be using a system that checks if the user is subject to the GDPR, request consent for purpose one and then respect it in this way.

The new update to Tic-tac-toe Collection does that. Which means European users who refuse consent will disables ads completely, for free. Since my ad revenue is low, and I’m already not a big fan of how mobile advertising tends to work… I’m kind of okay with it.

Interestingly, there is some debate about whether the GDPR actually requires this. If an app requires ads to be financially viable, and storing cookies is required to technically deliver ads (say for basic stats tracking or fraud prevention - not personalisation) that sounds very much like a legitimate interest, which means it can be done without consent. Google does address this here with the following quote

Google uses cookies or mobile ad identifiers to support ads measurement. Existing ePrivacy laws require consent for such uses, for users in countries where local law requires such consent. Accordingly, our policy requires consent for ads personalisation and ads measurement where applicable, even if ads measurement can, for GDPR purposes, be supported under a controller’s legitimate interests.

New site for my music YouTube channel

Oliver Brown
— This upcoming video may not be available to view yet.

I created a new website to go with the music visualization YouTube channel I launched.

There is not much content there yet, besides details of everything that is on the channel. But those details are probably the most interesting part.

I created a tool using the YouTube data API that generates Hugo content based on the channel.

The process started simple and got more sophisticated over a few days. First I generate a page for each video. Then I get a list of all the channel playlists, and add a tag to each video page for each playlist. I also create a page for the tag that links to the actual YouTube playlist. Finally, for each video I parse the description adding YouTube chapter links where appropriate.

It highlights another interesting advantage of static site generators. It is really easy to create content with external tools. You don’t need to access any kind of API, you just generate text files.

Interacting with this video is done so under the Terms of Service of YouTube
View this video directly on YouTube

Blog moved to Hugo

Oliver Brown
— This upcoming video may not be available to view yet.

After spending most of its time on Wordpress (initially a self-hosted version and then, the blog is now a static site generated with Hugo and running as an Azure Static Web App.

Ever since I created the Tic-tac-toe Collection site on Hugo I’ve wanted to move the blog, but I never had the time. The recent events related to COVID-19 have, however, given me random bits of free time, so here we are.

Tic-tac-toe Collection is being run as a static website on Azure Blob Storage, which is a way of connecting various bits of Azure together to host a static site. The new Azure Static Web App Service joins them all for you in a much more straightforward fashion.

Under ideal circumstances, the minimal steps to set up a static web app are:

  • Upload your site to Github.
  • Create a new Static Web App pointing at your Github.

And, if you’re lucky, that’s it. It generates a GitHub action for you that does the actual building, and it’s based on Microsoft Oryx. Oryx is a slightly magical, slightly scary tool designed to auto-detect and then build static web apps using a range of different technologies.

Unfortunately, for me, the auto generated Hugo script did not actually work. I ultimately used the GitHub action from here.