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:
- An iOS device and/or MacOS computer
- Elixir & Erlang
- Phoenix
- Livebook
- LVN GO
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.
- Chat: Phoenix App Setup (Required)
- Chat: Hello World (Required)
- Livebook: Syntax Conversion (60 mins)
- Chat: Header (10 mins)
- Chat: Messages (20 mins)
- Livebook: Live Form Exercise (40 mins)
- Chat: Message Form (50 mins)
- Chat: Web Template (20 mins)
- Chat: PubSub (10 mins)
- Chat: Styling (60 mins)
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.
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:
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.
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 themaxWidth
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.
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.
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
-
Find the
attr
definition inside of thecore_components.swiftui.ex
file and identify the allowed values. -
Change the
TextField
value for thetype
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.
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.
- LiveView Native the server-side portion of LiveView Native. Includes the functionality for render components, setup tasks, etc.
- LiveView Native Core the core library that powers other native client libraries.
- LiveView Native SwiftUI Client the SwiftUI client library that handles rendering the platform-specific template code for native iPhone, iPad, AppleTV, Apple Watch, MacOS, or Apple Vision Pro experience.
- LiveView Native Jetpack Client the SwiftUI client library that handles rendering the platform-specific template code for Android devices.
- LiveView Native Live Form adds support for HTML-style forms on native platforms.
- LiveView Native Stylesheets provides stylesheet primitives for LiveView Native.
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.