{
    "version": "https://jsonfeed.org/version/1",
    "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL -- https://tom.lokhorst.eu/feed/json -- and add it your reader.",
    "home_page_url": "https://tom.lokhorst.eu",
    "feed_url": "https://tom.lokhorst.eu/feed/json",
    "title": "Tom Lokhorst&#039;s blog",
    "description": "Writings from a happy Swift coder.",
    "items": [
        {
            "id": "https://tom.lokhorst.eu/2022/12/swift-package-manager-plugins-talk",
            "url": "https://tom.lokhorst.eu/2022/12/swift-package-manager-plugins-talk",
            "title": "Swift Package Manager Plugins [talk]",
            "content_html": "\n<p>Last week, I gave an impromptu talk at the <a href=\"https://cocoaheads.nl\">CocoaHeadsNL</a> November meetup.</p>\n\n\n\n<p>I&#8217;ve recently been working on <a href=\"https://github.com/mac-cain13/R.swift\">R.swift</a> version 7, which adds Swift Package Manager Plugin support.</p>\n\n\n\n<p>After a brief overview of R.swift itself, I demo&#8217;ed the way SPM plugins can be added to an Xcode project, as well as an SPM project. I then showed the two types of Plugins; BuildTool plugins and Command plugins.</p>\n\n\n\n<p>In the Q&amp;A afterwards, people asked some great questions about SPM plugins and R.swift.</p>\n\n\n\n<p><iframe src=\"https://www.youtube-nocookie.com/embed/OzZ9w3QM08U\" width=\"640\" height=\"360\" frameborder=\"0\" allowfullscreen=\"allowfullscreen\"></iframe></p>\n\n\n\n<p>See the <a href=\"https://github.com/mac-cain13/R.swift\">R.swift project on Github</a> for a full example of how to use SPM Plugins.</p>\n",
            "date_published": "2022-12-02T17:00:00+02:00",
            "date_modified": "2022-12-31T15:19:09+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2020/07/server-driven-ui",
            "url": "https://tom.lokhorst.eu/2020/07/server-driven-ui",
            "title": "Server Driven UI",
            "content_html": "<p><em><strong data-rich-text-format-boundary=\"true\">Update 2023-03-29:</strong> In August 2021 Apple acquired Primephonic the company. On March 28, 2023 Apple <a href=\"https://www.apple.com/newsroom/2023/03/apple-music-classical-is-here/\">launched Apple Music Classical</a> as a new app. This new app is built on the foundations of the Primephonic app, the fundamentals of the Server Driven UI architecture as described in this post remain the same.</em></p>\n<p>Note: This post is based on a talk I gave at CocoaHeadsNL in July 2020. Warning: because this is based on a transcription of a talk, sentences and wording may be weird or incoherent. The original talk, including a live Q&amp;A afterwards, can be seen here:</p>\n\n\n<iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https://www.youtube-nocookie.com/embed/ERPmUsLkwEE\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"></iframe>\n\n\n\n<p>Over the years, I\u2019ve build many different types of apps. However in one aspect they\u2019ve all been very similar; In the client/server architecture, the server sends domain objects to the client (encoded in JSON), and the client renders these domain objects to some pretty UI.</p>\n\n\n\n<p>Server Driven UI is different. The server does not send domain objects, with the client having to decide how to render those. Instead, the server decides what and how to render, and just sends instructions to the client. You know, kinda like HTML\u2026</p>\n\n\n\n<p>(Honestly, it\u2019s not HTML&#8230; But it sort of is. But really, it isn&#8217;t)</p>\n\n\n\n<span id=\"more-768\"></span>\n\n\n\n<h2>Outline</h2>\n\n\n\n<ul>\n<li><a href=\"#introduction\">Introduction</a></li>\n\n\n\n<li><a href=\"#components\">Components</a></li>\n\n\n\n<li><a href=\"#navigation\">Navigation</a></li>\n\n\n\n<li><a href=\"#actions\">Actions</a></li>\n\n\n\n<li><a href=\"#api-design\">API design</a></li>\n\n\n\n<li><a href=\"#when-not-appropriate\">When not appropriate?</a></li>\n\n\n\n<li><a href=\"#summary\">Summary</a></li>\n</ul>\n\n\n\n<h2 id=\"introduction\">Introduction</h2>\n\n\n\n<p>This post is about my experiences with building and using a Server Driven UI in the Primephonic app.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.003-1024x576.jpeg\" alt=\"\" class=\"wp-image-772\" srcset=\"/media/server-driven-ui.003-1024x576.jpeg 1024w, /media/server-driven-ui.003-300x169.jpeg 300w, /media/server-driven-ui.003-768x432.jpeg 768w, /media/server-driven-ui.003-1536x864.jpeg 1536w, /media/server-driven-ui.003-1200x675.jpeg 1200w, /media/server-driven-ui.003.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Screenshots of Primephonic app running on Android, iPhone, iPad, and the Web on a laptop</figcaption></figure>\n\n\n\n<p>Primephonic is a classical music streaming service. It has apps for Android, iPhone, iPad, a web player and Sonos integration. Similar to Apple Music, or Spotify, but additionally there are lot of features that are specific to classical music.</p>\n\n\n\n<p>We build Primephonic over the course of a couple of years and we build it all using Server Driven UI from the very beginning.</p>\n\n\n\n<h3>The basics</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.005-1024x576.jpeg\" alt=\"\" class=\"wp-image-774\" srcset=\"/media/server-driven-ui.005-1024x576.jpeg 1024w, /media/server-driven-ui.005-300x169.jpeg 300w, /media/server-driven-ui.005-768x432.jpeg 768w, /media/server-driven-ui.005-1536x864.jpeg 1536w, /media/server-driven-ui.005-1200x675.jpeg 1200w, /media/server-driven-ui.005.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">The Primephonic Composer page, separated into multiple components</figcaption></figure>\n\n\n\n<p>This screen from Primephonic shows a Composer page. It is build up out of several components.<br>At the top is an Artist header, there are two section headers, three detail buttons that link to other pages and a couple of paragraphs of text.</p>\n\n\n\n<p>The order of these components, as well their content, are all decided by the server. So if we want to make a change, and show some albums, instead of biography. We can make that change on the server, without needing to release new versions of the client apps.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.006-1024x576.jpeg\" alt=\"\" class=\"wp-image-775\" srcset=\"/media/server-driven-ui.006-1024x576.jpeg 1024w, /media/server-driven-ui.006-300x169.jpeg 300w, /media/server-driven-ui.006-768x432.jpeg 768w, /media/server-driven-ui.006-1536x864.jpeg 1536w, /media/server-driven-ui.006-1200x675.jpeg 1200w, /media/server-driven-ui.006.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">The same Composer page, but with some components swapped out</figcaption></figure>\n\n\n\n<h3>History</h3>\n\n\n\n<p>This is not a new idea. Here are just a few examples of other applications of the same concept:</p>\n\n\n\n<ul>\n<li><strong>Lona by AriBnb (2018)</strong><br>Open Source project: <a href=\"https://github.com/airbnb/Lona\">Lona</a><br>Presentation: <a href=\"https://www.youtube.com/watch?v=Ir8lq4rSyyc\">Scaling Server-driven UI by Laura Kelly &amp; Nathanael Silverman</a></li>\n\n\n\n<li><strong>Hub Framework at Spotify (2016)</strong><br>A bit of a tumultuous history, see deprecated <a href=\"https://github.com/spotify/HubFramework\">open source project</a><br>Presentation: <a href=\"https://www.youtube.com/watch?v=vuCfKjOwZdU\">John Sundell &#8211; Building component-driven UIs at Spotify (2016)</a><br>Presentation: <a href=\"https://www.droidcon.com/media-detail?video=352688824\">Elin Nilsson, Spotify &#8211; The Silver Bullet That Wasn\u2019t &#8211; the story of Hubs (2019)</a></li>\n\n\n\n<li><strong>WWDC talk at Apple (2010)</strong><br>A decade ago, Apple had an interesting WWDC talk, about using server side Plists to configure a native iOS app.<br>Transcript: <a href=\"https://asciiwwdc.com/2010/sessions/117\">Building a Server-driven User Experience</a></li>\n\n\n\n<li><strong>HTML of the Web (1991)</strong><br>Of course, the World Wide Web has been server driven from the very beginning. Although, in \u201891 all HTML was probably static. With the introduction of CGI, the HTML send to clients became dynamic.</li>\n</ul>\n\n\n\n<h2 id=\"components\">Components</h2>\n\n\n\n<p>Let\u2019s explore Server Driven UI by building some components. We start small, with a UITableViewCell.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.013-1024x576.jpeg\" alt=\"\" class=\"wp-image-780\" srcset=\"/media/server-driven-ui.013-1024x576.jpeg 1024w, /media/server-driven-ui.013-300x169.jpeg 300w, /media/server-driven-ui.013-768x432.jpeg 768w, /media/server-driven-ui.013-1536x864.jpeg 1536w, /media/server-driven-ui.013-1200x675.jpeg 1200w, /media/server-driven-ui.013.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">A UITableView cell with two lines, showing a single work</figcaption></figure>\n\n\n\n<p>This cell shows a single work by Beethoven. This looks simple, but of course there\u2019s more to it than that.</p>\n\n\n\n<p>When looking at the domain model, we see that there\u2019s a ton of details. For each work there\u2019s a separate title, number, key, alternative title, opus number, genre, period and some translations.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.014-1024x576.jpeg\" alt=\"\" class=\"wp-image-781\" srcset=\"/media/server-driven-ui.014-1024x576.jpeg 1024w, /media/server-driven-ui.014-300x169.jpeg 300w, /media/server-driven-ui.014-768x432.jpeg 768w, /media/server-driven-ui.014-1536x864.jpeg 1536w, /media/server-driven-ui.014-1200x675.jpeg 1200w, /media/server-driven-ui.014.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">A comparison of the domain model of a work and the visual representation in UITableViewCell</figcaption></figure>\n\n\n\n<p>This domain model lives on the server. In some database somewhere, there are a bunch of fields for this record that contains all this rich information.<br>What part of all this data do we send to the client? Do we want to send the whole object? Including the fields that aren\u2019t needed? Who does the logic, who decides that the key should be in the top line, and the opus number in the second line?</p>\n\n\n\n<p>In a Server Driven UI this is pretty straight forward. Everything is done on the server. The server does all the formatting and simply sends the results to the client.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n  \"title\": \"Piano Sonata No.14 in C# minor\",\n  \"subtitle\": \"Op. 27/2 \u2022 \u201cMoonlight\u201d\"\n}</code></pre>\n\n\n\n<p>This is all the JSON that\u2019s send to the app. A title and a subtitle. Because that\u2019s all that\u2019s needed to implement the design as shown.</p>\n\n\n\n<h3>Multiple variants</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.017-1024x576.jpeg\" alt=\"\" class=\"wp-image-782\" srcset=\"/media/server-driven-ui.017-1024x576.jpeg 1024w, /media/server-driven-ui.017-300x169.jpeg 300w, /media/server-driven-ui.017-768x432.jpeg 768w, /media/server-driven-ui.017-1536x864.jpeg 1536w, /media/server-driven-ui.017-1200x675.jpeg 1200w, /media/server-driven-ui.017.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Three variations of previous cell, all driven by optional fields in JSON</figcaption></figure>\n\n\n\n<p>Obviously, in a real world app, there\u2019s more variations. In certain contexts we may want to include the composer name. Or show the number of recordings that we have for this particular work.<br>Again, we can do all this in a server driven way. The JSON has a bunch of extra fields, all of which are optional. If the field isn\u2019t included in the JSON, it isn\u2019t shown in the table view cell. It is the server that decides which field is filled when and with what content.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"type\": \"detailRow\",\n&nbsp; \"addition\": \"Ludwig van Beethoven\",\n&nbsp; \"title\": \"Piano Sonata No.14 in C# minor\",\n&nbsp; \"subtitle\": \"Op. 27/2 \u2022 \u201cMoonlight\u201d\",\n&nbsp; \"disclosureText\": \"873\"\n}</code></pre>\n\n\n\n<h3>Cell with Artwork</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.019-1024x576.jpeg\" alt=\"\" class=\"wp-image-783\" srcset=\"/media/server-driven-ui.019-1024x576.jpeg 1024w, /media/server-driven-ui.019-300x169.jpeg 300w, /media/server-driven-ui.019-768x432.jpeg 768w, /media/server-driven-ui.019-1536x864.jpeg 1536w, /media/server-driven-ui.019-1200x675.jpeg 1200w, /media/server-driven-ui.019.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Three variations of cells showing artworks, all driven by JSON</figcaption></figure>\n\n\n\n<p>Let\u2019s look at a different example, a table view cell that shows an artwork. Again, all variations of this cell are modeled in JSON. There\u2019s a bunch of different fields that are all optional.</p>\n\n\n\n<p>What\u2019s interesting here is that the format of the JSON was very much influenced by the interaction designs for the app. When developing this app, a trio of a designer, a backend engineer, and an app developer together talked through all variations that could occur and designed the visuals and API simultaneously.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"type\": \"artworkRow\",\n&nbsp; \"image\": { \"url\": \"...\" },\n&nbsp; \"addition\": null,\n&nbsp; \"title\": \"Berliner Philharmonic, H...\",\n&nbsp; \"subtitle\": null,\n&nbsp; \"description\": \"1980 \u2022 30 tracks \u2022 2h 8m\"\n}</code></pre>\n\n\n\n<h3>Carousel</h3>\n\n\n\n<p>For a final component, let\u2019s look at something more complex. This is a carousel, it lists the top composers in order of popularity.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.021-1024x576.jpeg\" alt=\"\" class=\"wp-image-784\" srcset=\"/media/server-driven-ui.021-1024x576.jpeg 1024w, /media/server-driven-ui.021-300x169.jpeg 300w, /media/server-driven-ui.021-768x432.jpeg 768w, /media/server-driven-ui.021-1536x864.jpeg 1536w, /media/server-driven-ui.021-1200x675.jpeg 1200w, /media/server-driven-ui.021.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">A single UITableViewCell which contains a horizontally scrolling UICollectionView</figcaption></figure>\n\n\n\n<p>We implemented this as another table view cell, but this one contains a UICollectionView.<br>Looking at the JSON for this, we see this component is of type carousel. It includes the item type, so we know for the collection view which collection view cells have to be dequeued.<br>The items field holds an array of items for each composer.</p>\n\n\n\n<p>Again, the JSON model is pretty straightforward, an image and a title. That\u2019s all there is to display this cell.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"type\": \"carousel\",\n&nbsp; \"itemType\": \"artist\",\n&nbsp; \"items\": [\n&nbsp; &nbsp; {\n&nbsp; &nbsp; &nbsp; \"title\": \"Beethoven\",\n&nbsp; &nbsp; &nbsp; \"image\": { \"url\": \"...\" }\n&nbsp; &nbsp; },\n&nbsp; &nbsp; ...\n&nbsp; ]\n}</code></pre>\n\n\n\n<h3>A complete screen: Composer</h3>\n\n\n\n<p>Taking it all together, let\u2019s look again at this Composer page.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.023-1024x576.jpeg\" alt=\"\" class=\"wp-image-785\" srcset=\"/media/server-driven-ui.023-1024x576.jpeg 1024w, /media/server-driven-ui.023-300x169.jpeg 300w, /media/server-driven-ui.023-768x432.jpeg 768w, /media/server-driven-ui.023-1536x864.jpeg 1536w, /media/server-driven-ui.023-1200x675.jpeg 1200w, /media/server-driven-ui.023.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Screenshot of the composer page, along with the JSON</figcaption></figure>\n\n\n\n<p>To render this page, the app needs to do 1 single API call to the backend. That API call returns all data for this single screen at once, as a list of components.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"type\": \"componentScreen\",\n&nbsp; \"title\": \"Leonard Bernstein\",\n&nbsp; \"header\": {\n&nbsp; &nbsp; \"image\": { \"url\": \"...\" },\n&nbsp; &nbsp; \"title\": \"Leonard Bernstein\",\n&nbsp; },\n&nbsp; \"sections\": [\n&nbsp; &nbsp; {\n&nbsp; &nbsp; &nbsp; \"heading\": {\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; \"title\": \"Popular Works\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; \"button\": { \"title\": \"Show all\", ...}\n&nbsp; &nbsp; &nbsp; },\n&nbsp; &nbsp; &nbsp; \"components\": [\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; {\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"type\": \"detailRow\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"title\": \"West Side Story\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"disclosureText\": \"258\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; },\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; {\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"type\": \"detailRow\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"title\": \"Candide\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"disclosureText\": \"116\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; },\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; {\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"type\": \"detailRow\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"title\": \"Symphonic Dances\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"disclosureText\": \"70\",\n&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }\n&nbsp;&nbsp; &nbsp; &nbsp; ]\n&nbsp;&nbsp; &nbsp; },\n&nbsp;&nbsp; &nbsp; {\n&nbsp;&nbsp; &nbsp; &nbsp; \"heading\": {\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"title\": \"Biography\",\n&nbsp;&nbsp; &nbsp; &nbsp; },\n&nbsp;&nbsp; &nbsp; &nbsp; \"components\": [\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"type\": \"text\",\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \"paragraphs\": \"Leonard Bernstein was an American pianist...\",\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }\n&nbsp; &nbsp; &nbsp; &nbsp; ]\n&nbsp; &nbsp; &nbsp; }\n&nbsp; &nbsp; }\n&nbsp; ]\n}</code></pre>\n\n\n\n<p>So going from the top, we see the type of the page, a title, and a header. And then a list of sections, each section consisting of a list components.</p>\n\n\n\n<p>This whole page is implemented as UITableViewController. And each component implemented as a table view cell. Every row in the table renders itself based purely on the data contained in the JSON.</p>\n\n\n\n<p>In fact there is is no special logic in the app for the composer page. This could just as well be a work or an album page. All logic to distinguish between those domain objects lives on the server. The app knowns nothing about that.</p>\n\n\n\n<h2 id=\"navigation\">Navigation</h2>\n\n\n\n<p>A topic that comes up quite a lot when talking about an app whose data comes from a server is \u201cNavigation\u201d.<br>When do we load the data that is needed?</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.026-1024x576.jpeg\" alt=\"\" class=\"wp-image-787\" srcset=\"/media/server-driven-ui.026-1024x576.jpeg 1024w, /media/server-driven-ui.026-300x169.jpeg 300w, /media/server-driven-ui.026-768x432.jpeg 768w, /media/server-driven-ui.026-1536x864.jpeg 1536w, /media/server-driven-ui.026-1200x675.jpeg 1200w, /media/server-driven-ui.026.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Two screens, an overview page and a detail page</figcaption></figure>\n\n\n\n<p>These are two screen from the app, the first shows a list of composers, the second a single composer. But how do we get from the first screen to the second screen?</p>\n\n\n\n<p>Given that each screen is one API call, you might think of two approaches:</p>\n\n\n\n<ul>\n<li>Tap the item, navigate to an empty white screen and start doing the API call</li>\n\n\n\n<li>Tap the item, do the API call, wait for that to return and then push the new view controller, that is then completely filled in</li>\n</ul>\n\n\n\n<p>We didn\u2019t like either of these approaches; We don\u2019t want to have the \u201cwebbrowser\u201d effect, where each link sends you to a white screen that slowly loads. But we also didn\u2019t want taps to be slow, where you have to wait for an API call to return before navigation starts.</p>\n\n\n\n<p>So instead, we include the header of the second screen, as part of the link on the first screen.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.029-1024x576.jpeg\" alt=\"\" class=\"wp-image-788\" srcset=\"/media/server-driven-ui.029-1024x576.jpeg 1024w, /media/server-driven-ui.029-300x169.jpeg 300w, /media/server-driven-ui.029-768x432.jpeg 768w, /media/server-driven-ui.029-1536x864.jpeg 1536w, /media/server-driven-ui.029-1200x675.jpeg 1200w, /media/server-driven-ui.029.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Three states, an overview page, a detail page in a loading state, and a detail page with all content loaded</figcaption></figure>\n\n\n\n<p>So there are in fact three states that we have to deal with.:</p>\n\n\n\n<p>The first screen that contains a link, which includes the header for the second screen.<br>When we tap that item, we immediately push the second viewcontroller.<br>That second view controller is in a loading state, that already shows the big header, with a loader animation below.<br>And the finally, when the API call returns, we fade in all data.</p>\n\n\n\n<p>Let\u2019s take a deeper look at the JSON for the Mozart item in the Composers section.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"title\": \"Mozart\",\n&nbsp; \"image\": { \"url\": \"...\" },\n&nbsp; \"action\": {\n&nbsp; &nbsp; \"type\": \"componentScreen\",\n&nbsp; &nbsp; \"url\": \"/query/view/artist/wolfgang-amadeus-mozart-1756\",\n&nbsp; &nbsp; \"title\": \"Wolfgang Amadeus Mozart\",\n&nbsp; &nbsp; \"header\": {\n&nbsp; &nbsp; &nbsp; \"type\": \"artist\",\n&nbsp; &nbsp; &nbsp; \"image\": {\n&nbsp; &nbsp; &nbsp; &nbsp; \"url\": \"...\",\n&nbsp; &nbsp; &nbsp; &nbsp; \"lowresUrl\": \"...\"\n&nbsp; &nbsp; &nbsp; },\n&nbsp; &nbsp; &nbsp; \"title\": \"Wolfgang Amadeus Mozart\",\n&nbsp; &nbsp; &nbsp; \"subtitle\": \"Composer 1756 \u2013 1791\"\n&nbsp; &nbsp; }\n&nbsp; }\n}</code></pre>\n\n\n\n<p>There is the title and the image fields we saw before, but this item also has a tap action.<br>That action, already contains the type of the target page. So that we know what view controller to load. It also contains the URL for the API call and a title and a header.<br>The header is the exact same header that is part of the components on the next API call.<br>It also contains a title, subtitle and an image.</p>\n\n\n\n<p>One interesting aspect of this image, it doesn\u2019t just contain an url field. There\u2019s also a <code>lowresUrl</code> field. That low res image can be used as a placeholder, while the larger image is being loaded. If that image happens to be in the HTTP cache, it is used.<br>And in fact, that image is very often in the cache, because the low res url is the same url as was used for the small image in the carousel on the previous page.</p>\n\n\n\n<aside>If you have seen <a href=\"https://tom.lokhorst.eu/2018/03/rijksmuseum-artviewer-talk\">my CocoaHeads talk</a> from two years ago, where I talked about the Rijksmuseum Artviewer, you may be familiar with this approach. There we also used a low res version of cached tiles, as a placeholder while downloading higher resolution versions of those tiles.</aside>\n\n\n\n<p>So in practice, when tapping items in a carousel, we see a nice push animation to a new viewcontroller. Where the top half of the screen is already filled with a header including the background image, while the bottom halve is being loaded.</p>\n\n\n\n<p>In general, we are quite happy with this form of navigation. It takes some work on the server, to be able to generate the exact type of header that is also generated when requesting the target URL. But once that\u2019s implemented, the result looks quite nice.<br>Obviously, it\u2019s not as nice as it would have been had we not have to do the API call. But this is a good middle ground, that works well in practice.</p>\n\n\n\n<h2 id=\"actions\">Actions</h2>\n\n\n\n<h3>Local actions</h3>\n\n\n\n<p>The most obvious action you might expect in an app like this is playing music. So when we tap the first track in this list, the miniplayer pops up, and music starts playing.</p>\n\n\n\n<p>How does this work with our Server Driven UI?</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.043-1024x576.jpeg\" alt=\"\" class=\"wp-image-821\" srcset=\"/media/server-driven-ui.043-1024x576.jpeg 1024w, /media/server-driven-ui.043-300x169.jpeg 300w, /media/server-driven-ui.043-768x432.jpeg 768w, /media/server-driven-ui.043-1536x864.jpeg 1536w, /media/server-driven-ui.043-1200x675.jpeg 1200w, /media/server-driven-ui.043.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">A tracklist, each track contains an \u201caction\u201d in the JSON</figcaption></figure>\n\n\n\n<p>So a track is a type of component which includes a title, subtitle and a duration. And similar to the navigation example, this also includes an action.<br>However this action is of type \u201cplay\u201d and it contains all data needed to start playing music.</p>\n\n\n\n<p>The <code>tracksMetadataUrl</code> will be loaded asynchronously, to get all data needed to play all the items in the queue. But in the mean time, similar to the header for navigation, this action also includes the <code>startTrack</code> object. So that the player can start playing music as fast as possible, without having to wait for the metadata to be loaded.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"type\": \"track\",\n&nbsp; \"title\": \"Beginning and Ending\",\n&nbsp; \"subtitle\": \"Max Richter\",\n&nbsp; \"duration\": \"4:54\",\n&nbsp; \"action\": {\n&nbsp; &nbsp; \"type\": \"play\",\n&nbsp; &nbsp; \"tracksMetadataUrl\": \"/query/view/album/886448177210/tracks\",\n&nbsp; &nbsp; \"startIndex\": 0,\n&nbsp; &nbsp; \"startTrack\": {\n&nbsp; &nbsp; &nbsp; \"id\": \"886448177210-0\",\n&nbsp; &nbsp; &nbsp; \"md5\": \"073629779deddb06a14cdb559da7a62d\"\n&nbsp; &nbsp; }\n&nbsp; }\n}</code></pre>\n\n\n\n<h3>Remote actions</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.046-1-1024x576.jpeg\" alt=\"\" class=\"wp-image-793\" srcset=\"/media/server-driven-ui.046-1-1024x576.jpeg 1024w, /media/server-driven-ui.046-1-300x169.jpeg 300w, /media/server-driven-ui.046-1-768x432.jpeg 768w, /media/server-driven-ui.046-1-1536x864.jpeg 1536w, /media/server-driven-ui.046-1-1200x675.jpeg 1200w, /media/server-driven-ui.046-1.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Action sheet with three buttons, opened by tapping \u201cmore\u201d button in navigation bar</figcaption></figure>\n\n\n\n<p>A different type of actions are what we call \u201cremote command\u201d actions. These we can see in the menu in top right. The JSON for this menu shows a list of actions, each of these actions has a URL to call, and a payload to send back as the body of a HTTP POST.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; \"secondaryButtons\": [\n&nbsp; &nbsp; {\n&nbsp; &nbsp; &nbsp; \"action\": {\n&nbsp; &nbsp; &nbsp; &nbsp; \"type\": \"command\",\n&nbsp; &nbsp; &nbsp; &nbsp; \"url\": \"/command/endpoint?path=favorite_playlist\"\n&nbsp; &nbsp; &nbsp; &nbsp; \"payload\": \"...\"\n&nbsp; &nbsp; &nbsp; },\n&nbsp; &nbsp; &nbsp; \"title\": \"Add playlist to favorites\"\n&nbsp; &nbsp; },\n&nbsp; &nbsp; ...\n&nbsp; ]\n}</code></pre>\n\n\n\n<h3>Loader &amp; Done</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.049-1-1024x576.jpeg\" alt=\"\" class=\"wp-image-792\" srcset=\"/media/server-driven-ui.049-1-1024x576.jpeg 1024w, /media/server-driven-ui.049-1-300x169.jpeg 300w, /media/server-driven-ui.049-1-768x432.jpeg 768w, /media/server-driven-ui.049-1-1536x864.jpeg 1536w, /media/server-driven-ui.049-1-1200x675.jpeg 1200w, /media/server-driven-ui.049-1.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Three states; a loader, a \u201cDONE\u201d popup, and a new action sheets showing a \u201cremove\u201d action</figcaption></figure>\n\n\n\n<p>When we tap the top button \u201cAdd playlist to favourites\u201d.<br>We see a loader while the API call is happening, when it completes we see \u201cDONE\u201d and the page behind it is refreshed.<br>If we then open the menu again, we see that it now contains an entry to remove the playlist from favourites.</p>\n\n\n\n<p>It\u2019s worth noting here that everything we saw here was very generic. In fact when we started implementing favouriting, this was the first version, and it was completely implemented on the backend. We build and shipped the first version of this feature without doing any work on the clients. Later though, we made it better.</p>\n\n\n\n<h3>Requesting user input</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.050-1024x576.jpeg\" alt=\"\" class=\"wp-image-794\" srcset=\"/media/server-driven-ui.050-1024x576.jpeg 1024w, /media/server-driven-ui.050-300x169.jpeg 300w, /media/server-driven-ui.050-768x432.jpeg 768w, /media/server-driven-ui.050-1536x864.jpeg 1536w, /media/server-driven-ui.050-1200x675.jpeg 1200w, /media/server-driven-ui.050.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Another state, requesting user input</figcaption></figure>\n\n\n\n<p>Here\u2019s a second type of response a server can send. Instead of just showing a big \u201cDONE\u201d, the server can request input, from either a multiple choice list, or, like in this case, a piece of text. Here we see part of the flow of creating and editing playlists, and again, this is all orchestrated by the server.</p>\n\n\n\n<h3>Customising actions</h3>\n\n\n\n<p>Now, there are some things that can\u2019t be implemented by the server. For some things, we need the client.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.052-1024x576.jpeg\" alt=\"\" class=\"wp-image-795\" srcset=\"/media/server-driven-ui.052-1024x576.jpeg 1024w, /media/server-driven-ui.052-300x169.jpeg 300w, /media/server-driven-ui.052-768x432.jpeg 768w, /media/server-driven-ui.052-1536x864.jpeg 1536w, /media/server-driven-ui.052-1200x675.jpeg 1200w, /media/server-driven-ui.052.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Navigation bar has a \u201cshare\u201d button that opens the system share sheet</figcaption></figure>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>{\n&nbsp; ...\n&nbsp; \"secondaryButtons\": [ ... ],\n&nbsp; \"share\": {\n&nbsp; &nbsp; \"title\": \"Check out \u201cEpisode 1: Baroque Era\u201c on Primephonic\",\n&nbsp; &nbsp; \"url\": \"https://play.primephonic.com/playlist/AEl7xsGhQegEhOoyY6cge\"\n&nbsp; }\n}</code></pre>\n\n\n\n<p>Here we see a share button in the top right, when we tap that the system share sheet pops up. For this we need client side logic. So what the server sends us is fairly compact. The server decides the text that is shared, and includes the Universal URL. But it\u2019s up to all the clients to decided what to do with this. In the iOS example, we insert a share sheet button in the navigation bar. We also use this URL for Handoff between different Apple devices.</p>\n\n\n\n<h3>A beter favourite button ??</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.053-1024x576.jpeg\" alt=\"\" class=\"wp-image-796\" srcset=\"/media/server-driven-ui.053-1024x576.jpeg 1024w, /media/server-driven-ui.053-300x169.jpeg 300w, /media/server-driven-ui.053-768x432.jpeg 768w, /media/server-driven-ui.053-1536x864.jpeg 1536w, /media/server-driven-ui.053-1200x675.jpeg 1200w, /media/server-driven-ui.053.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">A playlist screen showing a heart icon button for favouriting</figcaption></figure>\n\n\n\n<p>And finally, in case you were worried we left favouriting in that ugly menu from before: of course not. When we had some time on the client side, we went in and wrote special case code to intercept the favourite item. Instead of showing it in the menu, we create a nice button with a heart icon.</p>\n\n\n\n<p>In theory, when you press it, we can show a nice animation and proactively select the item. The API call to favourite the item can then be done in the background. We then of course need to also implement all the logic for handling all edge cases, like network errors or if the server returns an error, we need to unselect the item again.</p>\n\n\n\n<p>Unfortunately, Primephonic isn\u2019t there quite yet. Currently when you press the button, there\u2019s still a full screen spinner, but at least the button looks nice already!</p>\n\n\n\n<h2 id=\"api-design\">API design</h2>\n\n\n\n<p>Here are some specifics related to the API, and how we integrated that in the app.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.056-1024x576.jpeg\" alt=\"\" class=\"wp-image-797\" srcset=\"/media/server-driven-ui.056-1024x576.jpeg 1024w, /media/server-driven-ui.056-300x169.jpeg 300w, /media/server-driven-ui.056-768x432.jpeg 768w, /media/server-driven-ui.056-1536x864.jpeg 1536w, /media/server-driven-ui.056-1200x675.jpeg 1200w, /media/server-driven-ui.056.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Slide showing multiple GET and POST calls that include a version number in URL</figcaption></figure>\n\n\n\n<p>As you can see, we include versioning in our API. And in the 2 years the app has been live, we\u2019ve already created 15 versions. But, that\u2019s not a big issue. Versions are cheap.</p>\n\n\n\n<p>We create a new version in this API whenever we add a new component type that the existing clients don\u2019t know about. So when a requests comes in, the server looks at the version number of the request, and only returns the component types that that client supports.</p>\n\n\n\n<p>If an older client sends a request, the server has to decide what to do as a fallback for a certain component. Maybe there\u2019s a nice new carousel, but the client doesn\u2019t support that yet. Then the server can send back an old boring list of items. Or simply skip the item, for that old client.</p>\n\n\n\n<p>On the server there\u2019s a single codebase that supports all versions. The server maintains has a big list that shows which API version supports which component types.</p>\n\n\n\n<p>Should it ever become too much of a hassle to maintain all these versions, and statistics show a certain version is rarely used anymore. Then the server can decide to no longer support that version and send back a generic error message, using the \u201ctext\u201d component type that already existed in version 1.</p>\n\n\n\n<h3>JSON Decoding</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.057-1024x576.jpeg\" alt=\"\" class=\"wp-image-798\" srcset=\"/media/server-driven-ui.057-1024x576.jpeg 1024w, /media/server-driven-ui.057-300x169.jpeg 300w, /media/server-driven-ui.057-768x432.jpeg 768w, /media/server-driven-ui.057-1536x864.jpeg 1536w, /media/server-driven-ui.057-1200x675.jpeg 1200w, /media/server-driven-ui.057.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Screenshot of the Composer page</figcaption></figure>\n\n\n\n<p>Again, this screen is implemented as a UITableViewController. It uses a diffable data source to animate between changes when reloading after performing remote command.</p>\n\n\n\n<p>The clients and server also make use of an HTTP Etag to indicate that there are no changes, so we don\u2019t need to animate anything.<br>The same goes for a client side triggered cancellation.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>struct Section: Decodable {\n&nbsp; let heading: Heading?\n&nbsp; let components: [ValueOrDecodingError&lt;Component&gt;]\n}\n\n\nenum Component {\n&nbsp; case carousel(Carousel)\n&nbsp; case detailRow(DetailRow)\n&nbsp; ...\n}\n\n\nstruct DetailRow: Decodable {\n&nbsp; let title: String\n&nbsp; let subtitle: String?\n&nbsp; let disclosureText: String?\n&nbsp; let primaryAction: Action?\n&nbsp; let deleteAction: Action?\n}</code></pre>\n\n\n\n<p>JSON parsing is all implemented using Swift\u2019s Codable. We create dedicated structs that directly match with the JSON that the server returns.<br>Sometimes we need domain objects to sit between the API models and the views. In that case we map from these API models to separate domain models.</p>\n\n\n\n<p>I should note that while the server needs to support multiple API versions at the same time, for the clients its a lot easier. It only needs to support 1 API version. So we can even remove old component types in newer API versions, and just throw away that code from the clients. The server isn\u2019t so lucky.</p>\n\n\n\n<p>One thing you may have noticed is the ValueOrDecodingError type in the components array of the top Section struct. This is there to protect against programmer error.</p>\n\n\n\n<p>In theory the server should never send something to a client that a client doesn\u2019t support. But in practice, programmers are people, and people make mistakes.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>public enum ValueOrDecodingError {\n  case value(Wrapped)\n  case error(DecodingError)\n}</code></pre>\n\n\n\n<p>So the ValueOrDecodingError exists as a safeguard. It is a generic enum, that either wraps a single value, or it contains an error.</p>\n\n\n\n<p>The Decodable implementation for this types tries to decode a value of the generic Wrapped type. If it succeeds, it returns that value, if it fails, it returns the decode error.</p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>struct Menu: Decodable {\n  let buttons: [ValueOrDecodingError]\n}</code></pre>\n\n\n\n<p>What that gives us in practice is that parsing doesn\u2019t stop when there\u2019s an error in our JSON. In this example, when we parse this menu, if the JSON array contains 4 objects, but one of those isn\u2019t actually a button, we still get the other buttons.</p>\n\n\n\n<p>The array will contain 3 buttons, and 1 error. We can then decide what to do with that error. In the case of Primephonic, we mostly log the error and skip the item, still showing the rest.</p>\n\n\n\n<p>In practice, this has saved us a couple of times, when someone made a mistake that wasn\u2019t caught in testing.</p>\n\n\n\n<p>Library: <a href=\"https://github.com/tomlokhorst/Statham\">https://github.com/tomlokhorst/Statham</a></p>\n\n\n\n<h3>Offline page</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.061-1024x576.jpeg\" alt=\"\" class=\"wp-image-803\" srcset=\"/media/server-driven-ui.061-1024x576.jpeg 1024w, /media/server-driven-ui.061-300x169.jpeg 300w, /media/server-driven-ui.061-768x432.jpeg 768w, /media/server-driven-ui.061-1536x864.jpeg 1536w, /media/server-driven-ui.061-1200x675.jpeg 1200w, /media/server-driven-ui.061.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">The offline experience of the app shows a list of downloaded albums</figcaption></figure>\n\n\n\n<p>Finally, one interesting aspect of this Server Driven UI, is that we can even use this for our offline experience.</p>\n\n\n\n<p>When you\u2019re online, the app also downloads one special page, that contains a component screen for the offline version of the app.</p>\n\n\n\n<p>Then, when you&#8217;re offline, you can use that version to browse around your offline downloaded music.</p>\n\n\n\n<h2 id=\"when-not-appropriate\">When is Server Driven UI not appropriate?</h2>\n\n\n\n<p>For the past year, I haven\u2019t worked on Primephonic. So I asked the one of the current developers what their experiences were with Server Driven UI.</p>\n\n\n\n<p>And one example that came up was the Offline experience.</p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.064-1024x576.jpeg\" alt=\"\" class=\"wp-image-806\" srcset=\"/media/server-driven-ui.064-1024x576.jpeg 1024w, /media/server-driven-ui.064-300x169.jpeg 300w, /media/server-driven-ui.064-768x432.jpeg 768w, /media/server-driven-ui.064-1536x864.jpeg 1536w, /media/server-driven-ui.064-1200x675.jpeg 1200w, /media/server-driven-ui.064.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Quote about how JSON for offline experience became too large</figcaption></figure>\n\n\n\n<p>He said the single JSON response for the offline version became too large. While the initial development went very fast, because of the reuse of components.</p>\n\n\n\n<p>They\u2019ve since switched back to a more traditional API.</p>\n\n\n\n<p>For the offline page we needed to send the JSON for everything, not just the first page, but any page you might navigate to. And it turned out that some users had 20 megabyte big offline JSONs that needed to be synced all the time.</p>\n\n\n\n<p>So this is an example were the Server Driven UI with big JSON responses didn\u2019t work so well.</p>\n\n\n\n<h3>Music player &amp; Subscription flow</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.065-1024x576.jpeg\" alt=\"\" class=\"wp-image-807\" srcset=\"/media/server-driven-ui.065-1024x576.jpeg 1024w, /media/server-driven-ui.065-300x169.jpeg 300w, /media/server-driven-ui.065-768x432.jpeg 768w, /media/server-driven-ui.065-1536x864.jpeg 1536w, /media/server-driven-ui.065-1200x675.jpeg 1200w, /media/server-driven-ui.065.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">The music player and the subscription flow in Primephonic are implemented without Server Driven UI</figcaption></figure>\n\n\n\n<p>Two other things in this app that weren\u2019t implemented using Server Driven UI are the music player itself and the subscription flow. These are both deep native OS integrations.</p>\n\n\n\n<p>When you\u2019re playing music, it needs to integrate with the Now Playing screen on your iPhone, it needs to respond to remote control commands from your AirPods. And you can also control playback from your Apple Watch or CarPlay.</p>\n\n\n\n<p>The subscription flow is an In-App Purchase, so that\u2019s also very specific logic. </p>\n\n\n\n<h3>The Rijksmuseum app</h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"576\" src=\"/media/server-driven-ui.066-1024x576.jpeg\" alt=\"\" class=\"wp-image-808\" srcset=\"/media/server-driven-ui.066-1024x576.jpeg 1024w, /media/server-driven-ui.066-300x169.jpeg 300w, /media/server-driven-ui.066-768x432.jpeg 768w, /media/server-driven-ui.066-1536x864.jpeg 1536w, /media/server-driven-ui.066-1200x675.jpeg 1200w, /media/server-driven-ui.066.jpeg 1920w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /><figcaption class=\"wp-element-caption\">Screenshots of the Rijksmuseum iPhone app</figcaption></figure>\n\n\n\n<p>And finally, here\u2019s an example of a completely different app, as an example of when not to use Server Driven UI.</p>\n\n\n\n<p>This is another app I\u2019ve also worked on, the Rijksmuseum app.<br>It allows you to walk tours in the museum where you can listen to audio descriptions of artworks.</p>\n\n\n\n<p>In this app almost all the logic happens in the client. One big JSON file is download that contains all the information about tours. And the client does all sorts of Bluetooth logic to show you what room you\u2019re in.</p>\n\n\n\n<p>For this type of app, a Server Driven UI isn\u2019t appropriate, even though there is a server that maintains all the data about the tours in the museum.</p>\n\n\n\n<h2 id=\"summary\">Summary</h2>\n\n\n\n<p>Server Driven UI can be a great setup for certain types of applications (or parts of applications). If you have a lot of domain objects, but ultimately a relatively simple user interface, an approach where you deal with all the domain complexity on the server can be quite helpful.</p>\n\n\n\n<p>I think there are two extremes; On one side you sent complete domain objects to the client, on the other side, you send only UI instructions to the client (think of HTML, or more extreme an image or PDF). There\u2019s probably a gradient between these two extremes, and Server Driven UI lies somewhere in the middle.</p>\n\n\n\n<p>With Primephonic, we found that the browsing experience through a rich database of classical music could very well be implemented using Server Driven UI. On the other hand, the actual Music Player was best implemented fully in the native client app.</p>\n\n\n\n<p>Interested in Classical Music? Check out <a href=\"https://primephonic.com\">Primephonic</a>!</p>\n",
            "date_published": "2020-07-22T20:04:00+02:00",
            "date_modified": "2023-04-09T18:48:06+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2020/07/cloudstorage-property-wrapper",
            "url": "https://tom.lokhorst.eu/2020/07/cloudstorage-property-wrapper",
            "title": "@CloudStorage property wrapper",
            "content_html": "<p>I wrote a Swift library to help with prototyping (haven&#8217;t used it yet in production apps).</p>\n<p><a href=\"https://github.com/tomlokhorst/CloudStorage\">CloudStorage</a> is property wrapper that will save values in iCloud key-value storage. These values are persisted across app restarts and will sync between different devices that are signed into the same iCloud account. This property wrapper is similar to <a href=\"https://developer.apple.com/documentation/swiftui/appstorage\">AppStorage</a> and <a href=\"https://developer.apple.com/documentation/swiftui/scenestorage\">SceneStorage</a>, two new types Apple introduced with iOS 14.</p>\n<p>The basic API is the same as for AppStorage; add <code>@CloudStorage(\"someName\")</code> in front of a property, to save the value to <a href=\"https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForKey-ValueDataIniCloud.html\">iCloud key-value storage</a>.</p>\n<h2>Usage</h2>\n<p>Make sure you enable the &#8220;key-value storage&#8221; service in the iCloud capability. See <a href=\"https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html#//apple_ref/doc/uid/TP40012094-CH6-SW1\">Apple&#8217;s documenation</a>.</p>\n<pre><code lang=\"swift\">\n@CloudStorage(\"readyForAction\") var readyForAction: Bool = false\n@CloudStorage(\"numberOfItems\") var numberOfItems: Int = 0\n@CloudStorage(\"orientation\") var orientation: String?\n</code></pre>\n<p>See also the example app in <a href=\"https://github.com/tomlokhorst/CloudStorage\">the repository</a>.</p>\n<h2>For what should this be used?</h2>\n<p>The same caveats apply as with key-value storage itself:</p>\n<blockquote><p><b>Key-value storage</b> is for discrete values such as preferences, settings, and simple app state.<br />\nUse iCloud key-value storage for small amounts of data: stocks or weather information, locations, bookmarks, a recent documents list, settings and preferences, and simple game state. Every app submitted to the App Store or Mac App Store should take advantage of key-value storage.</p></blockquote>\n<p>From Apple&#8217;s documentation on <a href=\"https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/iCloudFundametals.html#//apple_ref/doc/uid/TP40012094-CH6-SW28\">choosing the proper iCloud Storage API</a>.</p>\n<p>In general, key-value storage is not meant as a general purpose syncing service. If you need any advanced capabilities to prevent data loss, consider using CloudKit instead.</p>\n<p>Check out the <a href=\"https://github.com/tomlokhorst/CloudStorage\">CloudStorage repository</a>.</p>\n",
            "date_published": "2020-07-20T11:38:00+02:00",
            "date_modified": "2020-07-21T13:51:56+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support",
            "url": "https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support",
            "title": "My wish for watchOS: Dutch-style Cycling support",
            "content_html": "\n<div class=\"wp-block-image\"><figure class=\"alignright size-large is-resized\"><img decoding=\"async\" loading=\"lazy\" src=\"/media/Outdoor-Cycle-1.png\" alt=\"\" class=\"wp-image-684\" width=\"185\" height=\"296\" srcset=\"/media/Outdoor-Cycle-1.png 545w, /media/Outdoor-Cycle-1-188x300.png 188w\" sizes=\"(max-width: 185px) 85vw, 185px\" /><figcaption>Screenshot of Outdoor Cycle activity on Apple Watch</figcaption></figure></div>\n\n\n\n<p>Currently, watchOS has a workout called \u201cOutdoor Cycle\u201d, the icon shows a person on a <a href=\"https://en.wikipedia.org/wiki/Racing_bicycle\">racing bicycle</a>. But that is not what <em>cycling</em> means, for millions of people who use a <a href=\"https://en.wikipedia.org/wiki/Utility_bicycle\">utility bicycle</a> for daily transportation.</p>\n\n\n\n<p>Instead of specific activity, that some people do, some of the time. Cycling can also be a boring part of everyday life, that everyone does, all of the time.</p>\n\n\n\n<h2>How cycling in The Netherlands is different</h2>\n\n\n\n<p>Here I\u2019m going to make some assumptions and generalisations about places I don&#8217;t live&#8230; When I think of cycling in some other places, like say California, I think of something like this:</p>\n\n\n\n<figure class=\"is-layout-flex wp-block-gallery-1 wp-block-gallery columns-2 is-cropped\"><ul class=\"blocks-gallery-grid\"><li class=\"blocks-gallery-item\"><figure><a href=\"/media/us-bicycle-example1-1024x669.jpg\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"669\" src=\"/media/us-bicycle-example1-1024x669.jpg\" alt=\"\" data-id=\"690\" data-link=\"https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support/us-bicycle-example1\" class=\"wp-image-690\" srcset=\"/media/us-bicycle-example1-1024x669.jpg 1024w, /media/us-bicycle-example1-300x196.jpg 300w, /media/us-bicycle-example1-768x502.jpg 768w, /media/us-bicycle-example1-1536x1004.jpg 1536w, /media/us-bicycle-example1-1200x784.jpg 1200w, /media/us-bicycle-example1.jpg 2048w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /></a></figure></li><li class=\"blocks-gallery-item\"><figure><a href=\"/media/us-bicycle-example2.jpg\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"708\" src=\"/media/us-bicycle-example2-1024x708.jpg\" alt=\"\" data-id=\"691\" data-full-url=\"/media/us-bicycle-example2.jpg\" data-link=\"https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support/us-bicycle-example2\" class=\"wp-image-691\" srcset=\"/media/us-bicycle-example2-1024x708.jpg 1024w, /media/us-bicycle-example2-300x207.jpg 300w, /media/us-bicycle-example2-768x531.jpg 768w, /media/us-bicycle-example2-1536x1062.jpg 1536w, /media/us-bicycle-example2-1200x830.jpg 1200w, /media/us-bicycle-example2.jpg 1932w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /></a></figure></li></ul><figcaption class=\"blocks-gallery-caption\">Photos\u2019s of people cycling in California. On racing or road bikes, wearing specialised clothing. Credits: Richard Masoner <a href=\"https://www.flickr.com/photos/bike/15372697871/in/photolist-pqr6v2-KVi4gU-nxtFzZ-H7VSW-pB3Qzn-8jdAT2-6DZmGw-ji8hdq-p9dZZj-ji6tzD-Qucxns-pz1kkh-rDA79j-8jgUNq-fDA34-35Pd5Y-ss3PsT-ioBF4-ek8nwZ-sT3ocE-ek8wy2-sTgfcz-nge4mo-ni16Z1-ek8dnM-ek8yJK-ek8ET4-ek89vK-ek88TV-vcZ22w-ekeefY-ng1xP3-ek8AQx-sTfQUX-ek8tjT-sTfRpp-sAMnpF-fzVod-rWrjA6-ek8qBe-ek8tye-ek8xe2-ek8tGz-ekdX1u-ek8652-ek8vdv-ekdWLY-w9mGxY-eke9AU-ng1xG9\">photo 1</a>, <a href=\"https://www.flickr.com/photos/bike/16834284026/in/photolist-rDA79j-fKF5m5-8jgUNq-fDA34-ioBF4-ek8nwZ-sT3ocE-ek8wy2-5WidvX-sTgfcz-nge4mo-pB1ZG1-ni16Z1-ufBT2V-ek8dnM-p9dZxs-ek8yJK-ek8ET4-ek89vK-ek88TV-pqr6v2-vcZ22w-ekeefY-nxtFzZ-ng1xP3-ek8AQx-H7VSW-sTfQUX-ek8tjT-sTfRpp-pB3Qzn-sAMnpF-fzVod-rWrjA6-ek8qBe-8jdAT2-ek8tye-6DZmGw-ek8xe2-ek8tGz-ekdX1u-ek8652-ek8vdv-ekdWLY-w9mGxY-eke9AU-ng1xG9-ji8hdq-rhxJxi-ek8dCB\">photo 2</a></figcaption></figure>\n\n\n\n<p>Here we see people, hunched over on racing or road bikes. They go fast, wear special protective clothing and are are really working out. More importantly, culturally these people are doing something abnormal, in the sense that cycling is not the norm, most people don\u2019t cycle regularly.</p>\n\n\n\n<p>In contrast, this is a typical Dutch view of cycling:</p>\n\n\n\n<figure class=\"is-layout-flex wp-block-gallery-3 wp-block-gallery columns-3 is-cropped\"><ul class=\"blocks-gallery-grid\"><li class=\"blocks-gallery-item\"><figure><a href=\"/media/cycling-nl-example1.jpg\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"683\" src=\"/media/cycling-nl-example1-1024x683.jpg\" alt=\"\" data-id=\"698\" data-full-url=\"/media/cycling-nl-example1.jpg\" data-link=\"https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support/cycling-nl-example1\" class=\"wp-image-698\" srcset=\"/media/cycling-nl-example1-1024x683.jpg 1024w, /media/cycling-nl-example1-300x200.jpg 300w, /media/cycling-nl-example1-768x512.jpg 768w, /media/cycling-nl-example1-1536x1024.jpg 1536w, /media/cycling-nl-example1-1200x800.jpg 1200w, /media/cycling-nl-example1.jpg 2048w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /></a></figure></li><li class=\"blocks-gallery-item\"><figure><a href=\"/media/cycling-nl-example2.jpg\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"683\" src=\"/media/cycling-nl-example2-1024x683.jpg\" alt=\"\" data-id=\"699\" data-full-url=\"/media/cycling-nl-example2.jpg\" data-link=\"https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support/cycling-nl-example2\" class=\"wp-image-699\" srcset=\"/media/cycling-nl-example2-1024x683.jpg 1024w, /media/cycling-nl-example2-300x200.jpg 300w, /media/cycling-nl-example2-768x512.jpg 768w, /media/cycling-nl-example2-1536x1024.jpg 1536w, /media/cycling-nl-example2-1200x800.jpg 1200w, /media/cycling-nl-example2.jpg 2048w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /></a></figure></li><li class=\"blocks-gallery-item\"><figure><a href=\"/media/cycling-nl-example3.jpg\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"683\" src=\"/media/cycling-nl-example3-1024x683.jpg\" alt=\"\" data-id=\"700\" data-full-url=\"/media/cycling-nl-example3.jpg\" data-link=\"https://tom.lokhorst.eu/2020/06/my-wish-for-watchos-cycling-support/cycling-nl-example3\" class=\"wp-image-700\" srcset=\"/media/cycling-nl-example3-1024x683.jpg 1024w, /media/cycling-nl-example3-300x200.jpg 300w, /media/cycling-nl-example3-768x512.jpg 768w, /media/cycling-nl-example3-1536x1024.jpg 1536w, /media/cycling-nl-example3-1200x800.jpg 1200w, /media/cycling-nl-example3.jpg 2048w\" sizes=\"(max-width: 709px) 85vw, (max-width: 909px) 67vw, (max-width: 1362px) 62vw, 840px\" /></a></figure></li></ul><figcaption class=\"blocks-gallery-caption\">Photo\u2019s of people cycling in The Netherlands. On utility bicycles, sitting upright, in ordinary clothing. Credits: FaceMePLS <a href=\"https://www.flickr.com/photos/faceme/26390817579/in/photolist-Gd4QCc-93xJb2-pRqrM9-MmiBxj-28a5GpC-M1CwB2-odFcNu-agje5h-Cd8hHr-M7h4Nj-2dpnkCj-agjeh7-ZgHvm3-Rm2Cjh-4BJyJV-f6i2wB-2hgeVwZ-T9t7L9-yB2qZ4-g8MLgP-avV65M-96L2UA-8TRBAP-cNEujC-obYd9y-tyKMVd-rQM91j-c7MDif-g8MKSx-SRsWJD-sci2zT-uypKTW-9VPhx2-aMRQyP-uN44Fs-aWJynZ-gxMznw-MkWnxQ-ofR5Rq-8YgXoS-ohdqHm-eSNwvT-8RoqTV-hn3Fx3-2iQX2cs-pAfuVZ-S4GwUx-otjpj9-ohdqcw-omEKZn\">photo 1</a>, <a href=\"https://www.flickr.com/photos/faceme/32859696335/in/photolist-S4GwUx-otjpj9-ohdqcw-omEKZn-oxdGqy-5ntxjh-8jLrNY-4bojY1-uLtH5H-2fHpYNy-cQ9SQq-oEnicq-9uR3fy-M1Cx2v-kfhSow-9KMq8B-PFE763-4zoc1n-cRAdaE-9Wqe8B-7cLKNP-iic5PU-9bY6Wq-ApPLe-fcMTG1-bCeTq8-7dnjUh-4rYWha-98voy5-8UndnG-8FctuX-Y7vRPG-7VTrYG-7ngLud-ptY3Ym-8M6iu5-KJMiSN-hhAePp-jDihXa-9tBeyt-96L2cj-7d6XQ3-9v9mLA-7n6BdZ-9cb39i-qLtAAg-jDkiS5-VSTcGJ-7mZQnD-aBdUfD\">photo 2</a>, <a href=\"https://www.flickr.com/photos/faceme/33069385922/in/photolist-Soefjf-YxZAhV-6cHR7E-26B2iLh-aTScFF-ADYfe8-aEWygJ-2bR6bbs-6PhBCi-2xqntn-6PhD7B-fLMHDQ-6PhALX-noaXy4-PbYRkH-6TcGwM-2bR6aqu-7bw86c-7oir7d-2byr8PB-2cX44iX-6PhBAK-6TcEx4-2cSGy6y-q6b1rW-PbYQmt-2gHYKgZ-2gRjMbK-oRUXyW-qy36eM-BeYsyn-6PhBGF-6TcEWM-cQ6yMG-6TgGE7-MqrtSe-6TgFSo-2gPKFyZ-s8eoi3-7UKfik-2gpueSy-3KZi2G-TFg4qu-2gRjdmS-7tmcao-jSJShR-HEQKZN-6TgFdh-qKk4Q8-6TgGLs\">photo 3</a></figcaption></figure>\n\n\n\n<p>In The Netherlands, cycling is a common mode of transport. 27% of all trips are made by bicycle. In Amsterdam, a third of morning commutes are made by bicycle, making it the most popular form of transport (above walking and cars). At first glance you can see the differences: people sit upright, drive slowly, and they are wearing ordinary clothing. The bicycles themselves are also different; They are relatively cheap, have a kickstand, a chain guard and angled-back handlebars. They also include one or two luggage carriers.&nbsp;</p>\n\n\n\n<p>Side note; It\u2019s not that the other type of bicycle doesn\u2019t exist in The Netherlands. We too have people who (in the weekends), put on a helmet, dress up in lycra, and take out their other sporting bicycle. To do competitive racing, or mountain biking in the woods. But those are recreational sports, not day-to-day transport.</p>\n\n\n\n<h2>My which for Apple Watch: Support for Utility Cycling</h2>\n\n\n\n<p>I would like it if watchOS 7 added an additional option. To support this normal, everyday, utility cycling. It seems a bit weird to call this a \u201cworkout\u201d, but hey \u201coutdoor walking\u201d is also in there, so what the heck. I even designed an icon for this:</p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large is-resized\"><img decoding=\"async\" loading=\"lazy\" src=\"/media/Utility-Cycle-Mockup-EN.png\" alt=\"\" class=\"wp-image-706\" width=\"435\" height=\"435\" srcset=\"/media/Utility-Cycle-Mockup-EN.png 870w, /media/Utility-Cycle-Mockup-EN-300x300.png 300w, /media/Utility-Cycle-Mockup-EN-150x150.png 150w, /media/Utility-Cycle-Mockup-EN-768x768.png 768w\" sizes=\"(max-width: 435px) 85vw, 435px\" /><figcaption>Screenshot of Apple Watch Workouts app with hypothetical Utility Cycle activity</figcaption></figure></div>\n\n\n\n<p>In a similar way to Outdoor walking, this activity should be automatically detected and activated. This could be used to more accurately track activity when commuting to work, taking the kids to school or grocery shopping. And while you\u2019re at it Apple, please also add the cycling modality to Apple Maps!</p>\n\n\n\n<hr>\n<p>PS: For those unfamiliar, here&#8217;s a little mood impression of what cycling in The Netherlands looks like:</p>\n<iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https://www.youtube-nocookie.com/embed/B3smPA17D8M\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"></iframe>\n<p><em>(Also, do check out the rest of the rest of <a href=\"https://www.youtube.com/user/markenlei\">BicycleDutch channel</a> on YouTube. It&#8217;s great!)</em></p>\n",
            "date_published": "2020-06-02T10:20:44+02:00",
            "date_modified": "2020-06-03T14:11:10+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2019/07/every-two-years-apple-increases-software-support-by-a-year",
            "url": "https://tom.lokhorst.eu/2019/07/every-two-years-apple-increases-software-support-by-a-year",
            "title": "Every two years, Apple increases software support by a year",
            "content_html": "<p>Over the last 12 years, a pattern has emerged: Every two years Apple has adds an extra year of iOS support to its <a href=\"https://en.wikipedia.org/wiki/System_on_a_chip\">System-on-chips</a> (SoCs).</p>\n<p>The original iPhone and iPhone 3G both had three years of software updates. The iPhone 3GS and iPhone 4 had four years of updates, and so on.</p>\n<p>This diagram visualises that pattern:<br />\n<a href=\"/media/ios_versions_per_soc@2x.png\"><img decoding=\"async\" loading=\"lazy\" src=\"/media/ios_versions_per_soc@2x.png\" alt=\"iOS versions per SoC. 2007 and 2008: 3 years. 2009 and A4: 4 years. A5 and A6: 5 years. A7 and A8: 6 years. A9 and A10: 7 years? A11 and A12: 8 years?\" title=\"iOS versions per SoC\" border=\"0\" width=\"812\" height=\"375\"></a></p>\n<p><small>The coloured lines indicate iOS support a specific SoC. The grey line indicates when devices with this SoC were sold.</small></p>\n<p>Note that this diagram specifically plots SoCs, and not iPhone releases. The same A-series SoCs are used across iPhones, iPads and iPod touches. I&#8217;ve merged the releases of several of these devices to create this diagram.</p>\n<p>Should this trend continue in the future, the A8 will be supported for 6 years. The A9/A10 for 7 years, and the A11/A12 for 8 years.</p>\n<p>That means the <a href=\"https://en.wikipedia.org/wiki/IPhone_XS\">iPhone XS</a> released in September 2018, which runs the <a href=\"https://en.wikipedia.org/wiki/Apple_A12\">A12 Bionic</a>, would get updates until 2025, with iOS 19.</p>\n<h2 id=\"iphone-6-exception\">The iPhone 6 exception</h2>\n<p>Again, the above diagram refers to SoCs in general, and not iPhones, because if we plot that, there&#8217;s an exception for the iPhone 6.<br />\n<a href=\"/media/ios_versions_per_iphone@2x.png\"><img decoding=\"async\" loading=\"lazy\" src=\"/media/ios_versions_per_iphone@2x.png\" alt=\"iOS versions per iPhone. iPhone 6 is only supported for 5 years, but it's SoC is supported for 6 years.\" title=\"iOS versions per iPhone\" border=\"0\" width=\"812\" height=\"375\"></a></p>\n<p><small>The coloured lines indicate support iOS support for an iPhone. The grey line indicates when the iPhone sold.</small></p>\n<p>The <a href=\"https://en.wikipedia.org/wiki/IPhone_6\">iPhone 6</a> was released in September 2014 and was for sale until March 2017.</p>\n<p>In September 2019, when iOS 13 comes out, the iPhone 6 will no longer be supported. This is unfortunate because a lot of users are still using iPhone 6 devices. At <a href=\"https://q42.nl/en\">Q42</a> we see it is the 6th most popular iPhone with 7% usage, more popular than the iPhone SE or iPhone XR.</p>\n<p>Although support for the iPhone 6 (and <a href=\"https://en.wikipedia.org/wiki/IPod_Touch_(6th_generation)\">iPod touch 6th gen</a>) is being dropped, the <a href=\"https://en.wikipedia.org/wiki/IPad_Air_2\">iPad Air 2</a> and <a href=\"https://en.wikipedia.org/wiki/IPad_Mini_4\">iPad mini 4</a> of the same generation <em>are</em> still supported. The iPad mini 4 uses the same A8 SoC as the iPhone 6.</p>\n<p>A possible explanation for why it is being dropped might be the size of the RAM. iPhone 6 uses an A8 SoC, but it only has 1GB of RAM. The iPads both have 2GB of RAM.</p>\n<p>Every device supported by iOS 13, has at least 2GB of RAM.</p>\n<aside>Interesting side note: The <a href=\"https://en.wikipedia.org/wiki/HomePod\">HomePod</a> also uses an A8 with 1GB or RAM. The HomePod is of course still supported, but I&#8217;m assuming that it is not so directly tied to iOS releases.<br />\n</aside>\n<h2 id=\"the-future\">The future?</h2>\n<p>It is impossible to predict the future, but I don&#8217;t think this &#8220;RAM filter&#8221; will occur again soon.</p>\n<p>The next obvious step would be some future version of iOS that only supports 3GB RAM. But that would cut product lines in half. iPhone 7 and 8 both have 2GB or RAM, but the iPhone 7 Plus and 8 Plus use 3 GB.</p>\n<p>The step after that is 4GB as a minimum, but that includes some very recent devices. The iPhone XR (September 2018) and iPad Air 3 (March 2019) both ship with 3GB of RAM.</p>\n<p>My assumption is that, for the coming years, device deprecations will again be solely based on SoC.</p>\n<h2 id=\"so\">So&#8230;</h2>\n<p>Will this trend of adding a support year continue indefinitely? If not, when will it stop? Will there be another iPhone 6-style hiccup? Who knows!</p>\n<p>I can&#8217;t wait for WWDC 2020, to see what iOS 14 brings!</p>\n<hr>\n<p><small>All data about SoCs and release years are taken from Wikipedia. This <a href=\"/media/ios_versions_per_device_july_2019@2x.png\">chart</a> lists all iOS-like devices from the past 12 years.</small></p>\n",
            "date_published": "2019-07-15T12:00:25+02:00",
            "date_modified": "2019-09-16T10:01:18+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2018/12/hacking-marzipan-running-ios-apps-on-mac-talk",
            "url": "https://tom.lokhorst.eu/2018/12/hacking-marzipan-running-ios-apps-on-mac-talk",
            "title": "Hacking &#8220;Marzipan&#8221; &#8211; Running iOS apps on Mac [talk]",
            "content_html": "<p>A recording from my talk at the <a href=\"https://do-ios.nl\">Do iOS</a> conference last month.</p>\n<blockquote><p>At WWDC 2018 Apple announced that in the future it will become possible to run iOS apps on macOS. Rumours about this first appeared in December 2017 and was believed to be codenamed Marzipan. Although there is no official API or support for it yet in this talk Tom Lokhorst shows how he experimented with Marzipan and what he learned.</p>\n</blockquote>\n<p><iframe loading=\"lazy\" width=\"640\" height=\"360\" src=\"https://www.youtube-nocookie.com/embed/H9NzgbkxGVA\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></p>\n<p>The example project used in the talk is available on GitHub: <a href=\"https://github.com/Q42/MarzipanDemoApp\">MarzipanDemoApp</a>.</p>\n",
            "date_published": "2018-12-10T01:10:29+02:00",
            "date_modified": "2020-07-22T12:34:20+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2018/06/machine-learning-ar-at-the-rijksmuseum",
            "url": "https://tom.lokhorst.eu/2018/06/machine-learning-ar-at-the-rijksmuseum",
            "title": "Machine Learning + AR at the Rijksmuseum",
            "content_html": "<p>For the past four months Daniello, an intern at <a href=\"https://q42.nl/\">Q42</a>, has worked on training a machine learning model to recognise artworks in the Rijksmuseum. Once recognised, the paintings are located in the AR camera feed and augmented using ARKit.</p>\n<p>This week at WWDC, Apple announced CreateML and ARKit 2. Those two solutions combined may make this process a lot easier, but nevertheless, it&#8217;s really cool to see what Daniello build:</p>\n<p><iframe loading=\"lazy\" src=\"https://player.vimeo.com/video/271170516\" width=\"640\" height=\"360\" frameborder=\"0\" allowfullscreen></iframe></p>\n<p><a href=\"https://vimeo.com/271170516\">Art recogniser Rijksmuseum</a> from <a href=\"https://vimeo.com/q42\">Q42</a>.</p>\n",
            "date_published": "2018-06-07T21:59:03+02:00",
            "date_modified": "2018-06-07T21:59:03+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2018/03/rijksmuseum-artviewer-talk",
            "url": "https://tom.lokhorst.eu/2018/03/rijksmuseum-artviewer-talk",
            "title": "Rijksmuseum ArtViewer [talk]",
            "content_html": "<p>A recording from my talk at <a href=\"http://cocoaheads.nl\">CocoaHeadsNL</a> last month</p>\n<blockquote>\n<p>Tom Lokhorst from Q42 talks about how they developed the ArtViewer for the Rijksmuseum app. This viewer is a highly optimised image viewer to display very large images. It efficiently uses caching and tiling to save memory and bandwidth.</p>\n</blockquote>\n<p><iframe loading=\"lazy\" src=\"https://player.vimeo.com/video/259127904\" width=\"640\" height=\"360\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></p>\n<p><a href=\"https://vimeo.com/259127904\">Rijksmuseum ArtViewer, Tom Lokhorst</a> from <a href=\"https://vimeo.com/cocoaheadsnl\">CocoaHeadsNL</a>.</p>\n<p>See the <a href=\"https://itunes.apple.com/nl/app/rijksmuseum/id621307961\">Rijksmuseum app</a> in the App Store.</p>\n",
            "date_published": "2018-03-08T15:02:48+02:00",
            "date_modified": "2018-03-18T15:08:53+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2018/02/leaky-abstractions-in-swift-with-dispatchqueue",
            "url": "https://tom.lokhorst.eu/2018/02/leaky-abstractions-in-swift-with-dispatchqueue",
            "title": "Leaky abstractions in Swift with DispatchQueue",
            "content_html": "\n<p><em>At <a href=\"https://q42.com\">Q42</a> we have some apps with very occasional weird multi-threading issues. After a bunch of debugging <a href=\"https://twitter.com/mac_cain13\">Mathijs Kadijk</a> and I figured out it had something to do with DispatchSpecificKey. This post details what we found out.</em></p>\n<p>Pop quiz! What do you think this prints?</p>\n<pre><code class=\"swift\">\nwhile true {\n  let key = DispatchSpecificKey&lt;Int&gt;()\n\n  if let value = DispatchQueue.main.getSpecific(key: key) {\n    print(\"ERR \\(value)\")\n    exit(1)\n  }\n  else {\n    DispatchQueue.main.setSpecific(key: key, value: 42)\n    print(\"OK\")\n    sleep(1)\n  }\n}\n</code></pre>\n<p>Answer: This does <strong>not</strong> print a infinite stream of OKs every second, as I would have thought. Instead, it stops after three iterations of the loop:</p>\n<pre><code>\nOK\nOK\nERR 42\nProgram ended with exit code: 1\n</code></pre>\n<p>The output changes a bit each time I run the program. Sometimes it makes it to three or four OKs, but often it only prints one OK and then stops with an ERR.</p>\n<p>My question after seeing this behaviour: <strong>Wut? How can two distinct keys result in the same value?!?</strong></p>\n<aside>\n<p>If you&#8217;re not familiar with the DispatchSpecificKey API, it is used to associate contextual values with specific GCD queues:</p>\n<ul>\n<li><a href=\"https://developer.apple.com/documentation/dispatch/dispatchspecifickey\"><code>DispatchSpecificKey</code></a><br />\nCreates a key that can be used to set and get a value on dispatch queue\n</li>\n<li><a href=\"https://developer.apple.com/documentation/dispatch/dispatchqueue/2883699-setspecific\"><code>setSpecific</code></a><br />\nInstance method on a queue to store a value with a key\n</li>\n<li><a href=\"https://developer.apple.com/documentation/dispatch/dispatchqueue/1780751-getspecific\"><code>getSpecific</code></a><br />\nInstance method on a queue to retrieve a value for a key\n</li>\n</ul>\n</aside>\n<h2 id=c-wrappers>Wrappers around C APIs</h2>\n<p>It turns out these (woefully underdocumented) Swift APIs are wrappers around C APIs. The code for this is all open source, see <a href=\"https://github.com/apple/swift-corelibs-libdispatch/blob/master/src/swift/Queue.swift\">Queue.swift</a>. The underlying C APIs are better documented and give some insight into how this all works internally:</p>\n<ul>\n<li><a href=\"https://developer.apple.com/documentation/dispatch/1452967-dispatch_queue_set_specific\"><code>dispatch_queue_set_specific</code></a><br />\nSets the key/value data for the specified dispatch queue\n</li>\n<li><a href=\"https://developer.apple.com/documentation/dispatch/1453067-dispatch_queue_get_specific\"><code>dispatch_queue_get_specific</code></a><br />\nGets the value for the key associated with the specified dispatch queue\n</li>\n</ul>\n<p>The documentation for <code>dispatch_queue_set_specific</code> explains how the key is just a pointer to something, it doesn&#8217;t really matter what it&#8217;s pointing to.</p>\n<blockquote><p>\nKeys are only compared as pointers and are never dereferenced. Thus, you can use a pointer to a static variable for a specific subsystem or any other value that allows you to identify the value uniquely.\n</p></blockquote>\n<p>For this reason, the Swift implementation of <a href=\"https://github.com/apple/swift-corelibs-libdispatch/blob/master/src/swift/Queue.swift#L17\"><code>DispatchSpecificKey</code></a> is very simple:</p>\n<pre><code class=swift>\npublic final class DispatchSpecificKey&lt;T&gt; {\n\tpublic init() {}\n}\n</code></pre>\n<p>The initialised object is only used to get a pointer value that can be passed to the <code>dispatch_queue_set_specific</code> function. This pointer should of course be unique (hint: it is not).</p>\n<h2 id=debugging-behaviour>Debugging the weird behaviour</h2>\n<p>Suspecting this behaviour has something to do with the pointer, lets add a print statement to see what the pointer value is:</p>\n<pre><code class=swift>\nlet p = Unmanaged.passUnretained(key).toOpaque()\nprint(\"Pointer: \\(p)\")\n</code></pre>\n<p>This resulted in the following output:</p>\n<pre><code>\nPointer: 0x000000010120b590\nOK\nPointer: 0x000000010104ab00\nOK\nPointer: 0x000000010120b590\nERR 42\nProgram ended with exit code: 1\n</code></pre>\n<p>Finally, this the explains the behaviour we&#8217;re seeing; Different instances of DispatchSpecificKey share the same pointer!</p>\n<p>Presumably ARC cleans up the <code>key</code> variable after we&#8217;re no longer using it and in a next iteration of the loop, the same memory location is reused again to store a new instance of DispatchSpecificKey.</p>\n<h2 id=possible-fixes>Possible fixes for the bug</h2>\n<p>There are different solutions to fix this unexpected behaviour. One is to simply keep an array of each DispatchSpecificKey that gets created, that way no two DispatchSpecificKeys get assigned to the same memory location.</p>\n<p>In my own code, I happened to have the key be a member of an object. The solution was to add a deinitializer to the object:</p>\n<pre><code class=swift>\nclass MyObject {\n  let key = DispatchSpecificKey&lt;Int&gt;()\n\n  deinit {\n    DispatchQueue.main.setSpecific(key: key, value: nil)\n  }\n}\n</code></pre>\n<p>A more general solution would be if DispatchSpecificKey were to be updated to clean up after itself:</p>\n<pre><code class=swift>\npublic final class DispatchSpecificKey&lt;T&gt; {\n  public init() {}\n\n  // Is it OK to keep strong references to queues?\n  internal var queues: [DispatchQueue] = []\n\n  deinit {\n    for queue in queues {\n      queue.setSpecific(key: self, value: nil)\n    }\n  }\n}\n</code></pre>\n<h2 id=closing-thoughts>Closing thoughts</h2>\n<p>It took us a lot of debugging to finally narrow down the source of our bug to this reusing of the same memory address by two distinct objects. Normally in Swift code this wouldn&#8217;t be a problem, but because the underlying C API uses just pointers to compare for equality, this suddenly matters.</p>\n<p>For the C code, it is arguable that the programmer should be responsible for cleaning up after they&#8217;re done with a C &#8220;dispatch specific key&#8221;. They should call <code>dispatch_queue_set_specific</code> with a nil value themselves.</p>\n<p>But for the Swift code; The DispatchSpecificKey class really implies programmers shouldn&#8217;t have to know about the pointer internals of the C API. So in my opinion that class should have the cleanup code build-in.</p>\n",
            "date_published": "2018-02-09T12:13:58+02:00",
            "date_modified": "2019-02-22T23:18:04+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        },
        {
            "id": "https://tom.lokhorst.eu/2017/10/cocoaheadsnl-talk-reactive-uis",
            "url": "https://tom.lokhorst.eu/2017/10/cocoaheadsnl-talk-reactive-uis",
            "title": "Building Reactive User Interfaces [talk]",
            "content_html": "<p>A recording from my talk at <a href=\"http://cocoaheads.nl\">CocoaHeadsNL</a> last month:</p>\n<p><iframe loading=\"lazy\" src=\"https://player.vimeo.com/video/237530468\" width=\"640\" height=\"360\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></p>\n<p><a href=\"https://vimeo.com/237530468\">Tom Lokhorst &#8211; Building Reactive User Interfaces</a> from <a href=\"https://vimeo.com/cocoaheadsnl\">CocoaHeadsNL</a>.</p>\n<p>Mentioned in the talk, the work-in-progress <a href=\"https://github.com/Q42/Bindable\">Bindable</a> library.</p>\n",
            "date_published": "2017-10-11T12:18:42+02:00",
            "date_modified": "2018-03-17T13:36:13+02:00",
            "author": {
                "name": "Tom Lokhorst"
            }
        }
    ]
}