<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Richard Taylor</title>
    <description>The latest articles on Forem by Richard Taylor (@moomerman).</description>
    <link>https://forem.com/moomerman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F259006%2F95dd0666-ef81-4705-86a4-071eec96d89f.png</url>
      <title>Forem: Richard Taylor</title>
      <link>https://forem.com/moomerman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/moomerman"/>
    <language>en</language>
    <item>
      <title>Replacing Webpack with Snowpack in a Phoenix Application</title>
      <dc:creator>Richard Taylor</dc:creator>
      <pubDate>Wed, 17 Mar 2021 15:58:15 +0000</pubDate>
      <link>https://forem.com/moomerman/replacing-webpack-with-snowpack-in-a-phoenix-application-h0e</link>
      <guid>https://forem.com/moomerman/replacing-webpack-with-snowpack-in-a-phoenix-application-h0e</guid>
      <description>&lt;p&gt;Snowpack 3 was &lt;a href="https://www.snowpack.dev/posts/2021-01-13-snowpack-3-0"&gt;recently released&lt;/a&gt; and now comes with &lt;a href="https://esbuild.github.io"&gt;esbuild&lt;/a&gt; support built-in which should mean much faster builds with fewer dependencies, so I replaced Webpack in a fresh phoenix app to see the difference.&lt;/p&gt;

&lt;p&gt;tldr; It is incredibly fast and lightweight. See &lt;a href="https://github.com/moomerman/snowball/commit/84eece762b50ef92e0c1c73986d85e48917a75eb"&gt;this commit&lt;/a&gt; for all the changes required to get this working.&lt;/p&gt;

&lt;p&gt;I created a new Phoenix application with LiveView to test this out.  LiveView so all the asset config is already set up and we can switch out Webpack for Snowpack.&lt;/p&gt;

&lt;p&gt;Firstly I removed all &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;webpack.config.js&lt;/code&gt;, &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;.babelrc&lt;/code&gt; and all dependencies in &lt;code&gt;package.json&lt;/code&gt; except &lt;code&gt;phoenix_html&lt;/code&gt; and &lt;code&gt;nprogress&lt;/code&gt; which we'll keep.&lt;/p&gt;

&lt;p&gt;Then I ran &lt;code&gt;yarn add --dev snowpack&lt;/code&gt; to add Snowpack, the final &lt;code&gt;package.json&lt;/code&gt; looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /assets/package.json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;license&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snowpack build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;watch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snowpack build --no-bundle --watch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dependencies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phoenix_html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file:../deps/phoenix_html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devDependencies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nprogress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^0.2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;snowpack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^3.0.11&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added the equivalent &lt;code&gt;deploy&lt;/code&gt; and &lt;code&gt;watch&lt;/code&gt; commands to replace the Webpack ones.&lt;/p&gt;

&lt;p&gt;Next was to create a config file for Snowpack that matches the default asset file structure for Phoenix, and configures &lt;code&gt;esbuild&lt;/code&gt; to optimize the assets for production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /assets/snowpack.config.js&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;static&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;static&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;buildOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../priv/static/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;entrypoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./static/index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es2018&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only odd thing here is the fake HTML entrypoint I had to set up which references the &lt;code&gt;app.js&lt;/code&gt; and &lt;code&gt;app.css&lt;/code&gt;.  I found without this the optimizer didn't run for &lt;code&gt;app.css&lt;/code&gt;, maybe there is a better way of doing this.&lt;/p&gt;

&lt;p&gt;Snowpack creates a &lt;code&gt;_snowpack&lt;/code&gt; folder in the output directory so we need to add this to the Static Plug in &lt;code&gt;/lib/snowball_web/endpoint.ex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Snowpack creates ES modules for your JS assets so we need to update the &lt;code&gt;script&lt;/code&gt; tag for &lt;code&gt;app.js&lt;/code&gt; from &lt;code&gt;type="text/javascipt"&lt;/code&gt; to &lt;code&gt;type="module"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then we need to update the asset watcher that Phoenix starts up to run Snowpack, which now looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /config/dev.exs&lt;/span&gt;

&lt;span class="ss"&gt;watchers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="ss"&gt;node:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"node_modules/snowpack/index.bin.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"--watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;cd:&lt;/span&gt; &lt;span class="no"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../assets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final hurdle was getting the &lt;code&gt;phoenix&lt;/code&gt; and &lt;code&gt;phoenix_live_view&lt;/code&gt; assets to load.  Snowpack didn't like the default import that Phoenix generates so I ended up linking to the Snowpack &lt;code&gt;skypack&lt;/code&gt; CDN to get ES module versions of those dependencies and that works great.&lt;/p&gt;

&lt;p&gt;The result is incredibly fast start and rebuild times for assets in development and in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# development&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;snowpack build &lt;span class="nt"&gt;--no-bundle&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; building &lt;span class="nb"&gt;source &lt;/span&gt;files...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ build &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.01s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; building dependencies...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ dependencies ready! &lt;span class="o"&gt;[&lt;/span&gt;0.09s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; verifying build...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ verification &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.00s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; writing build to disk...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; optimizing build...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ optimize &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.09s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ▶ Build Complete!

✨  Done &lt;span class="k"&gt;in &lt;/span&gt;0.68s.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# production (on Heroku)&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;snowpack build
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; building &lt;span class="nb"&gt;source &lt;/span&gt;files...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ build &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.03s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; building dependencies...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ dependencies ready! &lt;span class="o"&gt;[&lt;/span&gt;0.29s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; verifying build...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ verification &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.00s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; writing build to disk...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] &lt;span class="o"&gt;!&lt;/span&gt; optimizing build...
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ✔ optimize &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;0.05s]
&lt;span class="o"&gt;[&lt;/span&gt;snowpack] ▶ Build Complete!

Done &lt;span class="k"&gt;in &lt;/span&gt;0.84s.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a vast improvement over the equivalent Webpack setup and results in 17MB of node_modules vs 225MB before the change.  &lt;/p&gt;

&lt;p&gt;There are caveats to using the built-in &lt;code&gt;esbuild&lt;/code&gt; as it says it isn't quite ready for production use yet, but you can add other tried-and-tested plugins to Snowpack if you prefer.&lt;/p&gt;

&lt;p&gt;Overall the future is looking good, and I'll be using this from now on.&lt;/p&gt;

&lt;p&gt;If you have any comments please add them to &lt;a href="https://twitter.com/moomerman/status/1350855425810436097"&gt;this thread on Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>webpack</category>
      <category>snowpack</category>
    </item>
    <item>
      <title>Build a Hybrid SwiftUI app for iOS with Phoenix LiveView</title>
      <dc:creator>Richard Taylor</dc:creator>
      <pubDate>Thu, 31 Oct 2019 20:25:45 +0000</pubDate>
      <link>https://forem.com/moomerman/build-a-hybrid-swiftui-app-for-ios-with-phoenix-liveview-1b26</link>
      <guid>https://forem.com/moomerman/build-a-hybrid-swiftui-app-for-ios-with-phoenix-liveview-1b26</guid>
      <description>&lt;p&gt;I've succesfully published  hybrid mobile apps with &lt;a href="https://github.com/turbolinks/turbolinks-ios"&gt;Turbolinks iOS&lt;/a&gt; in the past but Phoenix LiveViews are better suited due to the websocket event handling there is little (or no) HTTP navigation happening.&lt;/p&gt;

&lt;p&gt;To demonstrate this we're going to build an iOS app for &lt;a href="https://www.richardtaylor.dev/articles/flappy-phoenix-live-view"&gt;Flappy Phoenix&lt;/a&gt; a LiveView game I created previously.&lt;/p&gt;

&lt;p&gt;Create a new iOS project using &lt;code&gt;Single View App&lt;/code&gt; in Xcode&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/5Kbm2tdLAtrATdkpAvPh6y/7504b7f431df0bcc3235cfbf378ae3ab/image.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/5Kbm2tdLAtrATdkpAvPh6y/7504b7f431df0bcc3235cfbf378ae3ab/image.png" alt="image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure you've selected the &lt;code&gt;SwiftUI&lt;/code&gt; user interface&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/1FLl47U3qnVSJayS2cN49H/437f5abdb3205068ff37b265683edd91/image.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/1FLl47U3qnVSJayS2cN49H/437f5abdb3205068ff37b265683edd91/image.png" alt="image"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Using SwiftUI we need very little code to get the WebView onto the screen.  Update the generated &lt;code&gt;ContentView&lt;/code&gt; with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ContentView.swift&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;WebKit&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edgesIgnoringSafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewRepresentable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrollView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isScrollEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;liveView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://plappy-phoenix.herokuapp.com/game"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;liveView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView_Previews&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PreviewProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;previews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This results in a playable iOS app that wraps the LiveView!&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/3Khed9wJDJdNdaiyF4rzSN/97265bb99885b6b467e7bb58b8ed4fa9/image.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/3Khed9wJDJdNdaiyF4rzSN/97265bb99885b6b467e7bb58b8ed4fa9/image.png" alt="Flappy Phoenix"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is now possible to extend the interface with SwiftUI components to make it feel more native, while keeping the main body of the app served via the LiveView.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;NavigationView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigationBarTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Flappy Phoenix"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edgesIgnoringSafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This is a simplified example, but hopefully it sparks your imagination of what is possible with LiveView hybrid apps.&lt;/p&gt;

&lt;p&gt;In this case I haven't had to make any modifications to the web application but often you might want to make some changes on the server-side eg. hiding the web navigation if you're replacing it with native navigation.&lt;/p&gt;

&lt;p&gt;To achieve this we can set a custom user agent in the WebView and then respond to that in the web application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customUserAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"FlappyPhoenix iOS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If you have any tips or feedback please get in touch via &lt;a href="https://twitter.com/moomerman/status/1190000843170426880"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebview"&gt;The Ultimate Guide to WKWebView&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=5xvvfHNdB5c"&gt;WebKit in SwiftUI with SegmentedControl, WebView, State, UIViewRepresentable&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have any tips or feedback please get in touch via &lt;a href="https://twitter.com/moomerman/status/1190000843170426880"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>swift</category>
      <category>phoenix</category>
      <category>liveview</category>
    </item>
    <item>
      <title>Building a BBC Sounds status bar app for macOS</title>
      <dc:creator>Richard Taylor</dc:creator>
      <pubDate>Thu, 31 Oct 2019 15:04:09 +0000</pubDate>
      <link>https://forem.com/moomerman/building-a-bbc-sounds-status-bar-app-for-macos-38ag</link>
      <guid>https://forem.com/moomerman/building-a-bbc-sounds-status-bar-app-for-macos-38ag</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I listen to the radio a lot while developing and after trying the available&lt;br&gt;
apps at the time I wrote a &lt;a href="https://github.com/moomerman/motion-radio"&gt;simple native status bar app&lt;/a&gt; a few years ago in Ruby using the&lt;br&gt;
RubyMotion compiler toolkit.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/2I3zj1zWgrMR4MFLN8nmeL/f9f9074b9d22ba54added29653525836/radio.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/2I3zj1zWgrMR4MFLN8nmeL/f9f9074b9d22ba54added29653525836/radio.png" alt="Radio App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This allows you to simply switch between a preset list of radio streams&lt;br&gt;
and has media key support so you can pause/play and change station with the&lt;br&gt;
forward/back keys.&lt;/p&gt;

&lt;p&gt;This is probably my most used app on the mac but it lacks a lot of features&lt;br&gt;
I really want and I have intended to revisit it for a while.&lt;/p&gt;

&lt;p&gt;Having a few rare hours to spare over Christmas this year I started to have a&lt;br&gt;
look at what was available and determined that a WebView app of the&lt;br&gt;
relatively new &lt;a href="https://www.bbc.co.uk/sounds"&gt;BBC Sounds&lt;/a&gt; web app would be ideal,&lt;br&gt;
the main criteria being:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native status bar app&lt;/li&gt;
&lt;li&gt;WebKit webview of the BBC Sounds site&lt;/li&gt;
&lt;li&gt;Media key support for pause/play from the keyboard&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Existing tools
&lt;/h2&gt;

&lt;p&gt;There are a couple of projects I'd heard of that assisted you writing apps that&lt;br&gt;
wrap websites in web views so I thought I'd start there.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/zserge/webview"&gt;webview&lt;/a&gt; golang project assists you in writing a&lt;br&gt;
native app with cross-platform support (windows, mac, linux) very simply, this is&lt;br&gt;
literally all you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/zserge/webview"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;webview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BBC Sounds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"https://www.bbc.co.uk/sounds/play/live:bbc_6music"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It really couldn't be any simpler to get started and results in a native player,&lt;br&gt;
job done!&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/3Q6RIzVtHTSgFowrI1xy2e/96dd875df79bdd1d73c963095ab14956/webview.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/3Q6RIzVtHTSgFowrI1xy2e/96dd875df79bdd1d73c963095ab14956/webview.png" alt="webview App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only downside to this great project is that it abstracts so much away from you&lt;br&gt;
there isn't really any room for customisation, unless you want to get your hands&lt;br&gt;
dirty with C so it doesn't really meet all my criteria.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/djyde/WebShell"&gt;WebShell&lt;/a&gt; project looked&lt;br&gt;
like it would allow for more customisation at the expense of the cross-platform&lt;br&gt;
support (which isn't essential for me).&lt;/p&gt;

&lt;p&gt;Following the instructions and doing a little bit of configuration I had a&lt;br&gt;
player up and running with almost the same results as the webview app.  WebShell&lt;br&gt;
also allows you to configure it as a status bar app and has references to the media&lt;br&gt;
keys in the code so I thought I'd hit the jackpot.&lt;/p&gt;

&lt;p&gt;Unfortunately there were a number of major bugs, eg. 2 audio streams playing at&lt;br&gt;
the same time and the media keys not doing anything for me that meant it wasn't&lt;br&gt;
going to work.&lt;/p&gt;

&lt;p&gt;I figured it would be a good starting point though but after&lt;br&gt;
delving deeper into the code I realised it was doing so much more that I would need&lt;br&gt;
(or understand) that I wouldn't be happy maintaining my fork, if I could even&lt;br&gt;
get it working as I wanted.&lt;/p&gt;

&lt;p&gt;At this point though I was starting to understand what was needed to make it work,&lt;br&gt;
and had a great reference app to look at I decided to have a go building it from scratch.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the solution
&lt;/h2&gt;

&lt;p&gt;I cracked open a new macOS app in Xcode and found this great tutorial &lt;a href="https://www.raywenderlich.com/450-menus-and-popovers-in-menu-bar-apps-for-macos"&gt;Menus and Popovers in Menu Bar Apps for macOS&lt;/a&gt;&lt;br&gt;
that walks you through building a simple status bar app with a popover.  This &lt;a href="http://footle.org/WeatherBar/"&gt;WeatherBar&lt;/a&gt; tutorial was also really useful as a second reference.  I'm not going to duplicate the setup steps of these tutorials here.&lt;/p&gt;

&lt;p&gt;The main gist of the code that gets this working (in the &lt;code&gt;AppDelegate&lt;/code&gt;) is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;statusItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSStatusBar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSStatusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variableLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;popover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSPopover&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;applicationDidFinishLaunching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;aNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;named&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;
      &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;#selector(&lt;/span&gt;&lt;span class="nf"&gt;togglePopover(_:)&lt;/span&gt;&lt;span class="kd"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;@objc&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;togglePopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;popover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isShown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;closePopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;showPopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;showPopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;popover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;relativeTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;preferredEdge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSRectEdge&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;closePopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;popover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;performClose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Once the popup was showing I needed to get a WebView in there and this incredible&lt;br&gt;
article &lt;a href="https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebview"&gt;The Ultimate Guide to WKWebView&lt;/a&gt; was my frequent reference.&lt;/p&gt;

&lt;p&gt;The main detail of the implementation here (in &lt;code&gt;WebViewController&lt;/code&gt;) is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;webConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WKWebViewConfiguration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;webConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webView&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;loadURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"https://www.bbc.co.uk/sounds/play/live:bbc_6music"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;At this point the content failed to load, and I discovered I needed to add a&lt;br&gt;
permission to the app to be a network client, apparently this is a relatively&lt;br&gt;
new sandbox security feature in macOS.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/5nx5uj1PJEFSW5zMPDXnoe/acfdedc04397314dcac1d8e1b580b92f/capabilities.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/5nx5uj1PJEFSW5zMPDXnoe/acfdedc04397314dcac1d8e1b580b92f/capabilities.png" alt="Capabilities"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the content loading we have a functional status bar player!  One drawback&lt;br&gt;
was that that the audio failed to play automatically until the popover was&lt;br&gt;
opened.  I couldn't find a perfect solution to this but hacked in a quick&lt;br&gt;
open/close on launch that worked around it in &lt;code&gt;AppDelegate&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showPopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePopover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;If you know of a better way of handling this &lt;a href="https://twitter.com/moomerman/status/1080842844527579137"&gt;please let me know&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;The next part of the puzzle is getting the media keys controlling the player&lt;br&gt;
within the WebView.&lt;/p&gt;

&lt;p&gt;I've used the &lt;a href="https://github.com/nevyn/SPMediaKeyTap"&gt;SPMediaKeyTap&lt;/a&gt; project&lt;br&gt;
before in the previous player and it worked great.  After looking for alternative&lt;br&gt;
solutions that didn't seem to work I found the successor project &lt;a href="https://github.com/nhurden/MediaKeyTap"&gt;MediaKeyTap&lt;/a&gt; for swift and it worked a treat.&lt;/p&gt;

&lt;p&gt;The main gist of the code in &lt;code&gt;AppDelegate&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;webController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WebViewController&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;mediaKeyTap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MediaKeyTap&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;applicationDidFinishLaunching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;aNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;mediaKeyTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MediaKeyTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mediaKeyTap&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mediaKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MediaKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;KeyEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;mediaKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;playPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;webController&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;togglePlay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;rewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;webController&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;fastForward&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;webController&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This basically just forwards the media key press events to the webview controller.&lt;/p&gt;

&lt;p&gt;One gotcha here is that macOS requires you have the accessibilty permission&lt;br&gt;
for the app to receive the events. Under &lt;code&gt;Security &amp;amp; Privacy&lt;/code&gt; in macOS settings you need to add the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/1iA5NOaA31dg69kEOkbAzC/defa944f794915221727d4a1f352fe9d/security-privacy.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/1iA5NOaA31dg69kEOkbAzC/defa944f794915221727d4a1f352fe9d/security-privacy.png" alt="Security &amp;amp; Privacy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app will prompt you to do this on first launch, but you need to restart the&lt;br&gt;
app after adding the permission, I couldn't find a way of responding to any&lt;br&gt;
changes in this setting within the running app. If you know a way &lt;a href="https://twitter.com/moomerman/status/1080842844527579137"&gt;please let me know&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last step is to respond to the key events by executing javascript in the&lt;br&gt;
web view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;webView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WKWebView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;togglePlay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;execJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;smphtml5iframesmp-wrapper&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).contentWindow.document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;p_audioui_playpause&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).click()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;execJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;smphtml5iframesmp-wrapper&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).contentWindow.document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;p_audioui_toLiveButton&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).click()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;execJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;smphtml5iframesmp-wrapper&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).contentWindow.document.getElementById(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;p_audioui_backToStartButton&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;).click()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;execJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateJavaScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that's it! We have a BBC Sounds app with native media key controls and now&lt;br&gt;
I can get back to work :)  I really like having the playlist available in the&lt;br&gt;
app too, you can just scroll down to see what is playing and explore further.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/7gKPwwFWNZrnsmh5VTQlEJ/8f9713ecc935971a2f2e877ed8f7d9c1/tracks.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/7gKPwwFWNZrnsmh5VTQlEJ/8f9713ecc935971a2f2e877ed8f7d9c1/tracks.png" alt="Sounds App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final code is available on &lt;a href="https://github.com/moomerman/Sounds"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any feedback please add it to &lt;a href="https://twitter.com/moomerman/status/1080842844527579137"&gt;this thread on Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>go</category>
      <category>webkit</category>
    </item>
    <item>
      <title>An Elixir/Phoenix release workflow for GitHub CI</title>
      <dc:creator>Richard Taylor</dc:creator>
      <pubDate>Sun, 27 Oct 2019 10:34:37 +0000</pubDate>
      <link>https://forem.com/moomerman/an-elixir-phoenix-release-workflow-for-github-ci-2gb7</link>
      <guid>https://forem.com/moomerman/an-elixir-phoenix-release-workflow-for-github-ci-2gb7</guid>
      <description>&lt;p&gt;GitHub has made actions available to &lt;a href="https://twitter.com/natfriedman/status/1174158823877042177"&gt;anyone that requests it&lt;/a&gt; and is planning on launching in November so now is a good time to start moving your projects across.&lt;/p&gt;

&lt;p&gt;There are a lot more features available now including matrix builds and attached services.  GitHub also detects an Elixir repository and gives you a starting point for your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/qnx6g1z4xbnr/4l63Fc26k8inP1M3gA5zBl/299950f263ca1e353d180c53593da458/image.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/qnx6g1z4xbnr/4l63Fc26k8inP1M3gA5zBl/299950f263ca1e353d180c53593da458/image.png" alt="image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Elixir CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elixir:1.9.1-slim&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;mix local.rebar --force&lt;/span&gt;
        &lt;span class="s"&gt;mix local.hex --force&lt;/span&gt;
        &lt;span class="s"&gt;mix deps.get&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Tests&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mix test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This works great for very basic setups, unfortunately for me the first stumbling block was the container image doesn't have any build tools (eg. &lt;code&gt;make&lt;/code&gt;) so it wasn't able to compile &lt;code&gt;bcrypt_elixir&lt;/code&gt; among other dependencies.&lt;/p&gt;

&lt;p&gt;Hovever, the host does have build tools available so instead of running it in a container we can install elixir and erlang (OTP) on the host.  GitHub even have &lt;a href="https://github.com/actions/setup-elixir"&gt;an action&lt;/a&gt; to do this which is handy!&lt;/p&gt;

&lt;p&gt;This makes the basic configuration look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Elixir CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-elixir@v1.0.0&lt;/span&gt;
        &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;otp-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22.x&lt;/span&gt;
          &lt;span class="na"&gt;elixir-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.9.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;mix local.rebar --force&lt;/span&gt;
        &lt;span class="s"&gt;mix local.hex --force&lt;/span&gt;
        &lt;span class="s"&gt;mix deps.get&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Tests&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mix test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This will work as a simple CI for a lot of Elixir projects, but for Phoenix projects we need a bit more.&lt;/p&gt;

&lt;p&gt;This configuration sets up a standard Elixir/Phoenix environment (OTP, Elixir and Node) with a Postgres container.  It then compiles the code and assets, runs the tests and builds &amp;amp; publishes a release to GitHub releases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Elixir CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;push&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OTP ${{ matrix.otp }} | Elixir ${{ matrix.elixir }} | Node ${{ matrix.node }} | OS ${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-18.04&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;otp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;22.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;elixir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;1.9.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;12.x&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:11&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1.0.0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-elixir@v1.0.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;otp-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.otp }}&lt;/span&gt;
          &lt;span class="na"&gt;elixir-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.elixir }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.node }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mix local.rebar --force&lt;/span&gt;
          &lt;span class="s"&gt;mix local.hex --force&lt;/span&gt;
          &lt;span class="s"&gt;mix deps.get&lt;/span&gt;
          &lt;span class="s"&gt;yarn --cwd assets install&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mix compile --warnings-as-errors&lt;/span&gt;
          &lt;span class="s"&gt;mix format --check-formatted&lt;/span&gt;
          &lt;span class="s"&gt;mix test&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MIX_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare release&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mix compile&lt;/span&gt;
          &lt;span class="s"&gt;yarn --cwd assets deploy&lt;/span&gt;
          &lt;span class="s"&gt;mix phx.digest&lt;/span&gt;
          &lt;span class="s"&gt;mix release&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MIX_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish release&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;moomerman/actions/bin/ghr@master&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;RELEASE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_build/prod/rel&lt;/span&gt;
          &lt;span class="na"&gt;APPLICATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;APP NAME&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We've also added the strategy/matrix configuration above which isn't entirely necessary in this example but I like how it ends up having all the versions of dependencies clearly labelled in one place and it allows for extending the matrix to other versions very easily.&lt;/p&gt;

&lt;p&gt;A couple of gotchas I found so far (which could be user error) are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I couldn't find a way of specifying the version of an image in the matrix, eg. v11 of postgres above&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I tried to use the ofiicial nodejs docker image to execute the &lt;code&gt;yarn&lt;/code&gt; commands rather than running the &lt;code&gt;setup-node&lt;/code&gt; action on the host, but this resulted in assets that had permissions that meant the host couldn't modify them which seems like a bug.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The newer format is definitely a lot easier to follow and is extremely powerful.  It still takes a while to run, there's no official caching support (though there are a number of optimisations behind the scene) but I expect that to improve soon.&lt;/p&gt;

&lt;p&gt;You can see an example of an phoenix project running &lt;a href="https://github.com/moomerman/httping/blob/master/.github/workflows/elixir.yml"&gt;an adaptation&lt;/a&gt; of this configuration on GitHub at &lt;a href="https://github.com/moomerman/httping/actions"&gt;moomerman/httping&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any tips or feedback please get in touch via &lt;a href="https://twitter.com/moomerman/status/1180957149570187267"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>github</category>
      <category>ci</category>
    </item>
  </channel>
</rss>
