Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Elixir Conf Workshop 2024

workshop.livemd

Elixir Conf Workshop 2024

notebook_path = __ENV__.file |> String.split("#") |> hd()

Mix.install(
  [
    {:kino_live_view_native,
     github: "liveview-native/kino_live_view_native", branch: "rescue-no-file-error"}
  ],
  config: [
    kino_live_view_native: [
      qr_enabled: true
    ],
    server: [
      {ServerWeb.Endpoint,
       [
         server: true,
         url: [host: "localhost"],
         adapter: Phoenix.Endpoint.Cowboy2Adapter,
         render_errors: [
           formats: [html: ServerWeb.ErrorHTML, json: ServerWeb.ErrorJSON],
           layout: false
         ],
         pubsub_server: Server.PubSub,
         live_view: [signing_salt: "JSgdVVL6"],
         http: [ip: {0, 0, 0, 0}, port: 4001],
         check_origin: false,
         secret_key_base: String.duplicate("a", 64),
         live_reload: [
           patterns: [
             ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg|styles)$",
             ~r/#{notebook_path}$/
           ]
         ],
         code_reloader: true
       ]}
    ],
    kino: [
      group_leader: Process.group_leader()
    ],
    phoenix: [
      template_engines: [neex: LiveViewNative.Engine]
    ],
    phoenix_template: [format_encoders: [swiftui: Phoenix.HTML.Engine]],
    mime: [
      types: %{"text/swiftui" => ["swiftui"], "text/styles" => ["styles"]}
    ],
    live_view_native: [plugins: [LiveViewNative.SwiftUI]],
    live_view_native_stylesheet: [
      attribute_parsers: [
        style: [
          livemd: &Server.AttributeParsers.Style.parse/2
        ]
      ],
      content: [
        swiftui: [
          "lib/**/*swiftui*",
          notebook_path
        ]
      ],
      pretty: true,
      output: "priv/static/assets"
    ]
  ],
  force: true
)

Overview

Welcome to Elixir Conf US 2024! These materials are for the Unlock the Power of LiveView Native: Build Cross-Platform Applications with Ease Workshop hosted by Brian Cardarella, Brooklin Myers, and Carson Katri.

Prerequisites

To complete this workshop you’ll need:

This workshop assumes the attendee has some foundational knowledge on the command line, using a code editor, and Phoenix + LiveView.

However, your instructors will be happy to support you and fill in any gaps that the reading material doesn’t cover so that you can get the most out of the workshop.

Class Format

This workshop focused on practical and hands-on application of concepts. Workshop attendees will be given time to complete exercises using the provided reading material. The instructor will also provide an explanation of the concepts required to complete each exercise and answer any questions attendees have.

If time allows, the Instructor will ask an attendee who has completed the current exercise to demonstrate their solution and the group will discuss (asking questions, considering alternative implementation options, explaining concepts to each other, etc.)

Workshop Purpose

By the end of this workshop, you will have created a cross-platform chat application built using Phoenix and LiveView Native. This Livebook provides reading material and examples the explain and demonstrate the concepts necessary to complete each exercise.

Visit https://chat-demo.fly.dev/ on your browser, and/or enter the URL into the LVN GO app to see the application we’ll build in this workshop.

Getting Support

If you are stuck on an exercise or are unsure how to begin, refer to the reading material, provided exercises, or the example chat_demo application contained within the same folder as this Livebook. The exercises are intended to be completed using only concepts explained in the reading material. We have included everything required to complete each exercise within this Livebook and the linked resources within this Livebook. You are not expected to need information from outside sources (except those specifically linked as being necessary to complete the material).

Of course, you should also rely upon your workshop instructors, as well as your fellow attendees for help!

Timed Exercises

Workshop Pace and Expectations

We recognize that attendees have diverse backgrounds and work at different speeds. To keep the workshop on track, we’ve set estimated times (ETAs) for each exercise. These ETAs are meant to help us manage our time, not to dictate how long it should take you to complete the work.

If you don’t finish an exercise within the allotted time, simply copy the example solution and move on. You can revisit and complete the exercises later at your own pace. Don’t worry if you end up copying many of the solutions—this workshop covers a lot, and it’s not realistic to master everything in one day!

Exercises will indicate whether they should be completed in Livebook or within the Phoenix Chat application.

Exercises

> The instructor will use these ETA times to pace the exercise. They may change times depending on class flow.

Allotted exercise time: ~4.5 hours.

Prerequisite: VS Code Extension

If you’re using visual studio code, we recommend you install the LiveView Native extension to access useful features such as syntax highlighting and autocomplete in your code editor.

LiveView Native Extension on Visual Studio Code

Prerequisite: LVN GO

Make sure you have the LVN GO application. You can run this application on a connected iOS device, or on your macOS computer, then execute the LiveView Native code cell below.

require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]

defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    Hello Native, from Livebook!!!
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    

Hello HTML, from Livebook!

"""
end end |> Server.SmartCells.LiveViewNative.register("/") import Server.Livebook, only: [] import Kernel :ok

You can scan the QR code to run the code examples in this Livebook, or manually connect the LVN GO app to localhost:4001.

Chat: Overview

We’re going to create a Chat application that looks like the following:

”Chat

During this workshop, you’ll build each feature of the application step-by-step.

Depending on time available, we’ll also add some bonus features and improvements to our application.

Chat: Phoenix App Setup

To get started with this workshop, you’re going to create a Phoenix application set up with LiveView Native from scratch.

Run the following from your command line.

mix phx.new chat --no-ecto

This command generates a Phoenix server without a database (which we won’t need for this workshop.)

Add the following dependencies to your mix.exs file.

{:live_view_native, "~> 0.3.0"},
{:live_view_native_stylesheet, "~> 0.3.0"},
{:live_view_native_swiftui, "~> 0.3.0"},
{:live_view_native_live_form, "~> 0.3.0"}

Install dependencies.

mix deps.get

Run the LVN setup command and follow the instructions.

mix lvn.setup

You should be prompted to run two more commands. Run each command and answer Y (yes) for each prompted step. You can also answer d (diff) if you are curious what file changes each step makes.

Make sure you don’t answer n to any questions.

mix lvn.setup.config
mix lvn.setup.gen

Chat: Hello World

Route

Modify your router to use a LiveView for the "/" route.

  scope "/", ChatWeb do
    pipe_through :browser

    # get "/", PageController, :home
    live "/", ChatLive
  end

LiveView

Create a file in chat/lib/chat_web/live/chat_live.ex with the following content:

defmodule ChatWeb.ChatLive do
  use ChatWeb, :live_view
  use ChatNative, :live_view

  def render(assigns) do
    ~H"""
    

Hello from LiveView Web

"""
end end

Render Component

Run the following from your command line in the chat project folder.

mix lvn.gen.live swiftui Chat

This should generate a chat/lib/chat_web/live/chat_live.swiftui.ex render component and the associated template file chat/lib/chat_web/live/swiftui/chat_live.swiftui.neex.

Add the following content to the template file.

Hello world!

Then start your server.

mix phx.server

Then connect your LVN GO app to localhost:4000.

You should see Hello world! in your application.

”Hello

Resource: Views

Views are the building blocks of a SwiftUI interface. Views are comperable to HTML elements on the web.

LiveView Native introduces a DSL (Domain Specific Language) for writing views.

Here’s how the Text view looks in SwiftUI.

Text("Hamlet")

Here’s how we create a Text view in LiveView Native.

Hamlet

There are many SwiftUI Views supported by LiveView Native. For this workshop, you’ll only need to familiarize yourself with the following views:

  • Text for creating text
  • Spacer for filling empty space and “pushing” views where we need them.
  • HStack and VStack for horizontally and vertically placing views (similar to CSS flex).
  • ScrollView for creating a scrollable content within the page.

For more information on views, you can refer to the LVN Views guide

Resource: Modifiers

Modifiers in SwiftUI are methods that you can call on views to adjust their appearance or behavior. They return a new view with the modifier applied, allowing you to chain multiple modifiers together to compose complex views.

  Text("Hello, world!")
      .font(.title)
      .foregroundStyle(.blue)
      .frame(maxWidth: .infinity)
      .padding()

In this example:

  • .font(.title) is a modifier that changes the font style.
  • .foregroundStyle(.blue) is a modifier that changes the text color.
  • .frame(maxWidth: .infinity) is a modifier that changes the width of the view. Setting the maxWidth parameter to .infinity makes the view as wide as it can be.
  • .padding() is a modifier that adds space around the text.

You can implement modifiers as classes inside of the generated app.swiftui.ex stylesheet file.

~SHEET"""
"my-class" do
  font(.title);
  foregroundStyle(.blue);
  frame(maxWidth: .infinity);
  padding();
end
"""

Then you can apply the modifiers to the view using the class attribute.

Hello, world!

Alternatively, you can apply modifiers inline using the style attribute.

Hello, world!

If inline modifiers are long, you can write them on multiple lines like so. This syntax feels much like writing views and modifiers directly in SwiftUI with some slight modifications to make the syntax feel more familiar to web developers.


Hello, world!

For more on modifiers, see the LVN Stylesheets Guide.

Modifiers Example

Execute the following code cell, then visit localhost:4001 in the LVN GO app (or scan the QR code).

require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]

defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    
      Hello, world!
        
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end
|> Server.SmartCells.LiveViewNative.register("/")

import Server.Livebook, only: []
import Kernel
:ok

Resource: Attributes

In SwiftUI, both attributes and modifiers are used to describe and alter the appearance and behavior of views, but they serve different purposes and are used in different ways.

Attributes are properties defined directly in a SwiftUI view’s structure or in a view’s initializer. They set the fundamental aspects of the view, like its size, layout, or content, and are typically defined at the time of view creation.

For example, setting the title of a Text view is done with an unnamed attribute.

Text("Hello, world!")

Attributes can be named, and unnamed. For example, the VStack view accepts an alignment attribute. Passing .leading as the value to alignment aligns the content to the left side of the VStack view.

VStack(alignment: .leading) {
  Text("Hello, world")
}

In LiveView Native, SwiftUI attributes function similarly to HTML attributes. The type of value provided to the attribute can vary depending on the attribute, so when in doubt refer to the documentation for the LiveView Native view.


  Hello, world

Livebook: Syntax Conversion Exercise

Other than the templating language, you can use all of the same LiveView concepts for building interactive UIs. If you’re already familiar with LiveView, then the main learning curve for LiveView Native is the native client language (SwiftUI in our case) and how to convert those native examples into LiveView Native syntax.

As a LiveView Native developer, you’ll have most of the skills required to build Native UIs if you can find good SwiftUI examples online, then convert them into LiveView Native syntax.

You can use the Syntax Conversion Cheatsheet on our guides to complete the exercise below.

Exercise

Convert the following SwiftUI code into LiveView native code.

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    HStack {
        Text("Joshua Tree National Park")
            .font(.subheadline)
        Spacer()
        Text("California")
            .font(.subheadline)
    }
}
.padding()

Example Solution


    Turtle Rock
    
        Joshua Tree National Park
        
        California
    

Enter your solution in the code cell below.

require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]

defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view
end
|> Server.SmartCells.LiveViewNative.register("/")

import Server.Livebook, only: []
import Kernel
:ok

Chat: Header

Create the Header section of the chat app in the chat_live.swiftui.neex template. Use the font(.title) modifier to style the heading text.

Here’s a breakdown of the view hierarchy and a screenshot of what the app should look like.

”Chat ”Chat

Example Solution

Chat App


Chat: Messages

You’re going to create the list of Messages section of the chat application. Here’s a breakdown of the view hierarchy, and a screenshot of how the app will look.

The ... in the figma mock on the left means that the VStacks repeat for however many messages there are.

For now, we aren’t worried about styling.

”Chat ”Chat

Mount with Fake Data

Later, we’ll connect the page to real chat data. For now, we’ll create some dummy data. Like any LiveView, we can use data stored in the socket using the @ syntax.

Add the following mount function in the chat_live.ex file to store some fake messages in the socket.

def mount(_params, _session, socket) do
  lorem_ipsum = """
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sed ante interdum, pellentesque dolor at, rhoncus sapien. Praesent sed augue ex. Donec dignissim at turpis vitae convallis.
  """

  messages = List.duplicate(%{name: "Person 1", content: lorem_ipsum}, 20)

  {:ok, assign(socket, messages: messages)}
end

Render Messages

Next, in your chat_live.swiftui.neex template, render the list of messages using the data stored in the socket. Use a ScrollView to make the messages scrollable.

If you aren’t already familiar with comprehensions within a template, remember that you can use the following syntax to dynamically render elements using a list (or any other enumerable data type)


    <%= message.name %>
    <%= message.content %>

Example Solution

Chat App


    
        <%= message.name %>
        <%= message.content %>
    

Livebook: Live Form Exercise

The LiveView Native Form library simplifies the creation of forms using syntax very similar to Phoenix forms.

Assuming you have the :live_view_native_live_form dependency in your mix.exs, your auto-generated core_components.swiftui.ex file will include a .input component and a .simple_form component very similar to the equivalent components in the core_components.ex file generated by Phoenix. We’re going to compare the similarities and differences between these components.

Here’s a .simple_form component you might find in an HTML template:

<.simple_form for={@form} id="form" phx-submit="my-submit">
    <.input field={@form[:field]} type="text" />
    <:actions>
        <.button type="submit">
            Send Message
        
    

Here’s a .simple_form component you might find in a SwiftUI template:

<.simple_form for={@form} id="form" phx-submit="my-submit">
    <.input field={@form[:field]} type="TextField"/>
    <:actions>
      <.button type="submit">
          Send
      
    

Notice how similar the two forms are. The type attribute is the main difference, since SwiftUI uses different input types than HTML.

Exercise

  1. Find the attr definition inside of the core_components.swiftui.ex file and identify the allowed values.
  2. Change the TextField value for the type attribute to a different valid value.

Keep in mind that some input types may require other attributes.

Example Solution

<.simple_form for={@form} id="form" phx-submit="my-submit">
    <.input field={@form[:field]} type="Slider"/>
    <:actions>
      <.button type="submit">
          Send
      
    

Use the code cell below to perform the exercise. Connect your LVN GO app to localhost:4001 to view your solution. Make sure you re-evaluate the cell after changing the input type.

require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]

defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <.simple_form for={@form} id="form" phx-submit="my-submit">
        
        <.input field={@form[:field]} type="TextField"/>
        <:actions>
          <.button type="submit">
              Send
          
        
    
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :form, to_form(%{}, as: "my-form"))}
  end

  def handle_event("my-submit", params, socket) do
    IO.inspect(params, label: "Form Params")
    {:noreply, socket}
  end
end
|> Server.SmartCells.LiveViewNative.register("/")

import Server.Livebook, only: []
import Kernel
:ok

Chat: Message Form

You’re going to create a form that users can submit messages to. This form should add a new message that will be displayed at the top of the page.

You may need to refer to our Forms and Validation guide to complete this exercise.

Heres a breakdown of the view hierarchy and a screenshot of how the app will look.

”Chat ”Chat

Mount

First, modify your mount/3 callback to assign the form in the socket.

  def mount(params, _session, socket) do
    socket =
      socket
      |> assign(:form, to_form(%{}, as: "chat-form"))
      |> assign(:messages, [])

    {:ok, socket}
  end

Event Handler

Then, add the handler for sending messages.

  def handle_event(
        "send-message",
        %{"chat-form" => %{"content" => content, "name" => name}},
        socket
      ) do

    message = %{content: content, name: name}

    socket =
      socket
      |> assign(:messages, [message | socket.assigns.messages])
      # Optional: this preserves the user's name between sending messages, but clears the content field.
      |> assign(:form, to_form(%{"name" => name}, as: "chat-form"))

    {:noreply, socket}
  end

Render the Form

Now use the .simple_form component to create a form with a :name and :content field matching the picture above.

The form should send a "send-message" event on submit to match the handlers you just created.

Example Solution

The placeholder is optional.

Chat App

    
        <%= message.name %>
        <%= message.content %>
    


<.simple_form for={@form} id="form" phx-submit="send-message">
    <.input field={@form[:name]} type="TextField" placeholder="Name..."/>
    <.input field={@form[:content]} type="TextEditor"/>
    <:actions>
    <.button type="submit">
        Send
    
    

Chat: Web Template

Add the following render function to your LiveView to add a html template. Notice how easy it is to re-use event handler logic between platforms.

def render(assigns) do
  ~H"""
  

Chat App

<%= for message <- @messages do %>

<%= message.name %>

<%= message.content %>

<% end %> <.simple_form for={@form} id="form" phx-submit="send-message"> <.input field={@form[:name]} placeholder="From..." /> <.input field={@form[:content]} type="textarea" placeholder="Say..." /> <:actions> <.button type="submit"> Send Message """
end

That’s it! Now visit http://localhost:4000 in the browser to see the HTML template in action.

Chat: PubSub

Now that we already have a working chat application, you’re going to connect your cross-platform clients using PubSub.

Subscribe

First, subscribe to the "messages" topic in your mount/3 callback.

def mount(params, _session, socket) do
  # Subscribe to the messages topic
  if connected?(socket) do
    ChatWeb.Endpoint.subscribe("messages")
  end
  
  socket =
    socket
    |> assign(:form, to_form(%{}, as: "chat-form"))
    |> assign(:messages, [])

    {:ok, socket}
end

Broadcast

Then, add a broadcast_from function to your "send-message" event handler to broadcast an event on the PubSub topic to all other connected clients.

def handle_event(
      "send-message",
      %{"chat-form" => %{"content" => content, "name" => name}},
      socket
    ) do

  message = %{content: content, name: name}

  socket =
    socket
    |> assign(:messages, [message | socket.assigns.messages])
    |> assign(:form, to_form(%{"name" => name}, as: "chat-form"))

  # Add broadcast_from/4 function call.
  ChatWeb.Endpoint.broadcast_from(self(), "messages", "send-message", message)

  {:noreply, socket}
end

Event Handler

Finally, handle the broadcasted message in a handle_info/2 callback.

def handle_info(%Phoenix.Socket.Broadcast{topic: "messages", event: "send-message", payload: message}, socket) do
  {:noreply, assign(socket, messages: [message | socket.assigns.messages])}
end

That’s all it takes to make your application work in real-time across multiple platforms!

Resource: Named Content Areas

Modifiers can sometimes accept a View within a named content area. For example, here’s how the safeAreaInset modifiers allows us to overlay a view ontop of another view.

ScrollView {
    ScrolledContent()
}
.safeAreaInset(edge: .bottom) {
    Footer()
}

This presents a problem in LiveView Native, which attempts to remain as similar to HTML on the web as possible. This would be like if CSS accepted an HTML element as a value. To circumvent this problem, you can specify a content value for the modifier as an atom, then inject the view by giving it a template attribute matching the specified atom.

Here’s how the above code would look in LiveView Native


  

You’ll need to understand these named content areas for the next exercise.

Chat: Styling

We’re going to use the following modifiers to improve the styling of our chat application. Refer to the SwiftUI documentation links to complete the next exercise.

  • navigationTitle to render the title of the Chat Application
  • safeAreaInset to overlay the form on top of the messages and frame to set the height of the form.
  • font to change the font for the name of the user.
  • frame to align the text to the left within the VStack view.
  • padding to create space between each message

navigationTitle

Remove the Text view at the top of the page, and replace it using the navigationTitle modifier.

Example Solution


  ...

safeAreaInset

Use the safeAreaInset modifier to overly the form at the bottom of the ScrollView view. You’ll likely also need to use the frame modifier to set the height of the form.

Example Solution

You’ll need to move the .simple_form inside of the scroll view. You’ll also have to use the frame modifier to set a height for the form, since by default it takes up any available space.



  ...
  
  
  <.simple_form template="form" for={@form} id="form" phx-submit="send-message" style="frame(height: 250);">
    ...
  <.simple_form>

font

Use the font modifier to make the name of the user have the .title2 font.

Example Solution

<%= message.name %>

frame

Use the frame modifier on the VStack to left-align the content. Here’s how that would look in SwiftUI:

VStack() {
  ...
}
.frame(maxWidth: .infinity, alignment: .leading);

Example Solution


  ...

padding

Use the padding modifier to create space between each message.

Example Solution

You may choose to put the padding above on the name. The value of 10 is arbitrary.

<%= message.name %>

Or below on the content.

<%= message.content %>

Putting it all together

Here’s how your SwiftUI template should look after all of these styling changes.


    
    <%= for message <- @messages do %>
        <%= message.name %>
        <%= message.content %>
    <% end %>
    
    <.simple_form template="form" for={@form} id="form" phx-submit="send-message" style="frame(height: 250);">
        <.input field={@form[:name]} type="TextField" placeholder="Name..."/>
        <.input field={@form[:content]} type="TextEditor"/>
        <:actions>
        <.button type="submit">
            Send
        
        
    

Chat: Markdown Support (Bonus)

The Text view includes markdown support through the markdown attribute.

Instead of rendering the message content as a child to the Text view, use the markdown attribute.

Example Solution

Keep in mind that in a real application we would also need to implement markdown rendering for html, but that’s beyond the scope of this workshop.

Chat: Stretch Goal (Bonus)

Want to keep extending your LiveView Native skills? For simplicity’s sake, the chat app currently renders messages at the top of the app. Try rendering the messages at the bottom, and make it so that the ScrollView scrolls to the bottom whenever a new message is created so that the latest message is always visible.

Resource: Continued Learning

These resources are not required to complete this workshop, but may be useful to continue your LiveView Native learning journey.

LVN Cookbook

Our cookbook demonstrates common code examples in LiveView Native.

Scan this QR code in the LVN GO app or manually connect to https://cookbook.liveviewnative.dev/ to see the cookbook examples.

You can see the code for these examples on the LVN Cookbook Github

LVN Guides

We’ve created a series of interactive guides using Livebook. Checkout the Livebooks section on our documentation.

LVN Slack

Have a question or want some help with LiveView Native?

Check out the #liveview-native channel on the Elixir Lang Slack.

LiveView Native Ecosystem Overview

LiveView Native is a platform for building native applications using Elixir and Phoenix LiveView. It allows a single LiveView to serve both web and non-web clients by transforming platform-specific template code into native UIs.

The LiveView Native Ecosystem is split into multiple different libraries. Here are a few of the LiveView Native libraries you may want to be aware of for some context on this workshop.

This list is only to provide a high level overview of the LVN ecosystem. You do not need to remember or be familiar with these libraries to complete this workshop.