The Road Toward LiveView Native v0.2 Part 2

An overhead shot of a winding road cutting through a rocky mountainside
Brian Cardarella

CEO & Founder

Brian Cardarella

In Part 1 we went over the challenge of representing SwiftUI’s modifier system in LiveView Native and what our solution will be.

LiveView Native builds on top of LiveView, but LiveView itself was built only for the Web. How best LiveView Native (LVN) should integrate with Phoenix has been an evolving process. We had at first thought about working within the constraints of Phoenix and LiveView by not requesting upstream changes, but as the project matured it became clear that we’d need certain changes and have advocated for several changes in both Phoenix and Elixir. Let’s look at some examples, then explore how this ties into LiveView Native v0.2.

Originally we were imposing naming conventions upon SwiftUI view names to LiveView Native element names. For example, in SwiftUI you have Text() but Phoenix’s template compiler would assume that an element name that starts with a capital letter must be an Elixir module reference so this element is actually a Component.

The workaround was to write this as <text> within our LiveView Native template. Ok, not bad. What about AsyncImage, well that becomes <async-image>, and the pattern starts to reveal itself.

We were taking CamelCased names and hyphenating them. This starts to break down with common views such as VStack we get <v-stack>. Maybe it’s just me, but the single-letter hyphenation just looks bad. We discussed making certain aliases so we’d get <vstack> but now we have a deviation from the convention and two ways to do one thing. This gets more complicated with more camelcasing, for instance with LazyVStack, you guessed it <lazy-v-stack>.

Our first ask back to Phoenix was to relax this constraint. They agreed and I believe the current assumption is if it has a capital letter and a period it will do Component lookup, and that works for us. Now we can use <Text>, <AsyncImage>, <VStack>, <LazyVStack>, and so on for element names. This is great, the less mental mapping SwiftUI views back to LiveView Native the better.

Our next ask was in Elixir itself. Now with the changes above we were using a variant of the HEEx template engine and we couldn’t call it with ~H. ~N would have been nice for “native” by that sigil was claimed by NativeDateTime. For a while we went with ~Z but that letter didn’t really have any reference to LVN. We wanted to use ~LVN but Elixir had a built in limit of single-character sigils.

After a brief chat with José Valim about this, he agreed that allowing for multicharacter sigils was OK, and this was released with Elixir 1.15. While we originally considered using the ~LVN sigil, we ultimately decided to go with platform-specific ones for each template as this would be easier for syntax highlighters and be obvious which platform is being worked on. Now we have ~SWIFTUI and ~JETPACK.

Around this time, we started to think about imposing some conventions throughout our projects.

During the rapid development and exploration phase of the SwiftUI client, we had not imposed any naming conventions on the projects. We decided that the “official” name of the project would be “LiveView Native”. With the “Native” being physically separated from the “LiveView” section, if for no better reason than to indicate this project isn’t under the LiveView Core team’s responsibility.

Our casing for this is liveview-native, which more than one LiveView Core team member has pointed out to me that they use live_view. Yes, I’m aware 😀.

We do respect it for the Hex package names so it will sort properly amongst the other LiveView related projects. But we renamed our GitHub org from liveviewnative to liveview-native and renamed many of our pre-existing projects and updated the module and namespaced within those projects to reflect the change. This all took place months before v0.1.0 and I plan to write more about this topic as well as describing our organization conventions for multi-platform libs like LiveViewNative LiveForm in the future. But for now, just know that we’ve taken care to ensure consistency throughout what we expect to be many projects as well as some conventions others can follow for writing their own plugins. (another topic I’ll explore in the future)

We also normalized our platform names. Despite the casing in “SwiftUI” we don’t use the camelcasing to underscore naming conventions. Instead, it is simply just swiftui throughout. “Jetpack Compose” will simply be jetpack. These names will be used quite a bit in your apps so keeping them concise is important.

Beyond this, we are not done with architectural changes. We still have one major architectural change that we’re introducing prior to v0.2. We’re aligning with Phoenix’s own convention around response formats. But, like other changes, we will need to advocate Phoenix for upstream changes to do this.

First, for those that are unaware, when Phoneix is rendering dead views from a Controller, it does permit you to change the format of the response. If the request comes in asking for another format that your application is configured to respond to, for example, xml or json instead of html then this format will be carried through all the way down the response stack. The templates rendered will attempt home.json.heex instead of home.html.heex for example. And this extends to the layouts. Instead of root.html.heex and app.html.heex Phoenix will attempt to render the format-specific layouts.

It’s a nice convenience that has become more powerful since the release of New Rendering Formats. Now that requests and template rendering are broken out into their own modules, this promotes isolation and allows for more nuanced helper functions and components to be written. This is all great. Unfortunately, LiveView cannot do any of this. It’s not an oversight, just that LiveView is written for the web, and hardcoding html as the response format made sense.

We ran into this limitation early in the development of LVN and went with establishing the platform_id that was part of each client connection. This would allow you to pattern match the render/1 function like:

def render(%{platform_id: :swiftui} = assigns) do
  ~SWIFTUI"""
   ...
  """
end

However, this didn’t allow us to write layout templates. At least not ones that were implicitly rendered. We turned off layout rendering altogether. If the HTML layout happens to render, which can happen due to a LVN configuration issue, the application won’t render at all within the SwiftUI client. You’ll get this: lvn-1

This happens because rather than crash, the LVN client will attempt to render what it can find. In the event it encounters an unknown view name it will ignore that view, ignore all of its children, and move on to the next sibling.

How we introduce better dev ergonomics around these things is, again, another topic for another time. But for now, just know that our default rendering preference is to keep the application from crashing by just ignoring what it doesn’t know. The same goes for modifiers.

One great use for layouts that we’re missing out on currently is navigation.

In SwiftUI you use an actual view like NavigationStack to enable in-app navigation and history states. In LVN we have the option on by default that will wrap your LVN ViewTree with <NavigationStack> automatically so the entire app gets navigation. You can also enable a NavigationSplitView or a TabView but that too is a topic for another blog post.

The point here is that we should be using Phoenix layouts to wrap our LVN templates with the navigation and layout we want rather than having to configure this within the client.

The LiveView Core team agreed and José wrote a PR for us to test out this approach, but we had some work on our end to get ready to try this out. May Matyi has been working to first refactor a bunch of the internal workarounds that we make use of and we are changing the platform_id over to format. Our opinion here is that SwiftUI is a format, Jetpack Compose is a format similar to how the Web, through HTML, is a format or JSON is a format. This may seem a bit of a stretch but being able to have client platforms identify themselves via the format attribute means we can take advantage, eventually, of format implicit layout rendering.

Over the next few weeks, we will validate this approach and hopefully get a sense of what work is remaining to get format implicit layout rendering into LiveView. Making these changes now ensures that there will be less upgrade pain in the future and allows us to leverage many parts of Phoenix itself to do work we were having to work around. This means less code on our end.

At this time I don’t have any indication of if/when this change and additional format implicit layout rendering will make its way into LiveView, so we will likely ship v0.2 prior to that. As long as we are set up so that it can be taken advantage of when it does release I think that’s OK.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box