View Source Interactive SwiftUI Views
Overview
In this guide, you'll learn how to build interactive LiveView Native applications using event bindings.
This guide assumes some existing familiarity with Phoenix Bindings and how to set/access state stored in the LiveView's socket assigns. To get the most out of this material, you should already understand the assign/3
/assign/2
function, and how event bindings such as phx-click
interact with the handle_event/3
callback function.
We'll use the following LiveView and define new render component examples throughout the guide.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<Text>Hello, from LiveView Native!</Text>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
end
Event Bindings
We can bind any available phx-*
Phoenix Binding to a SwiftUI Element. However certain events are not available on native.
LiveView Native currently supports the following events on all SwiftUI views:
phx-window-focus
: Fired when the application window gains focus, indicating user interaction with the Native app.phx-window-blur
: Fired when the application window loses focus, indicating the user's switch to other apps or screens.phx-focus
: Fired when a specific native UI element gains focus, often used for input fields.phx-blur
: Fired when a specific native UI element loses focus, commonly used with input fields.phx-click
: Fired when a user taps on a native UI element, enabling a response to tap events.
The above events work on all SwiftUI views. Some events are only available on specific views. For example,
phx-change
is available on controls andphx-throttle/phx-debounce
is available on views with events.
There is also a Pull Request to add Key Events which may have been merged since this guide was published.
Basic Click Example
The phx-click
event triggers a corresponding handle_event/3 callback function whenever a SwiftUI view is pressed.
In the example below, the client sends a "ping"
event to the server, and trigger's the LiveView's "ping"
event handler.
Evaluate the example below, then click the "Click me!"
button. Notice "Pong"
printed in the server logs below.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<Button phx-click="ping">Press me on native!</Button>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("ping", _params, socket) do
IO.puts("Pong")
{:noreply, socket}
end
end
Click Events Updating State
Event handlers in LiveView can update the LiveView's state in the socket.
Evaluate the cell below to see an example of incrementing a count.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<Button phx-click="increment">Count: <%= @count %></Button>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("increment", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
end
Your Turn: Decrement Counter
You're going to take the example above, and create a counter that can both increment and decrement.
There should be two buttons, each with a phx-click
binding. One button should bind the "decrement"
event, and the other button should bind the "increment"
event. Each event should have a corresponding handler defined using the handle_event/3
callback function.
Example Solution
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<!-- Displays the current count -->
<Text><%= @count %></Text>
<!-- Enter your solution below -->
<Button phx-click="increment">Increment</Button>
<Button phx-click="decrement">Decrement</Button>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("increment", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
def handle_event("decrement", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
Enter Your Solution Below
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<!-- Displays the current count -->
<Text><%= @count %></Text>
<!-- Enter your solution below -->
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
@impl true
def render(assigns), do: ~H""
end
Selectable Lists
List
views support selecting items within the list based on their id. To select an item, provide the selection
attribute with the item's id.
Pressing a child item in the List
on a native device triggers the phx-change
event. In the example below we've bound the phx-change
event to send the "selection-changed"
event. This event is then handled by the handle_event/3
callback function and used to change the selected item.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<List selection={@selection} phx-change="selection-changed">
<Text :for={i <- 1..10} id={"#{i}"}>Item <%= i %></Text>
</List>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, selection: "None")}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("selection-changed", %{"selection" => selection}, socket) do
{:noreply, assign(socket, selection: selection)}
end
end
Expandable Lists
List
views support hierarchical content using the DisclosureGroup view. Nest DisclosureGroup
views within a list to create multiple levels of content as seen in the example below.
To control a DisclosureGroup
view, use the isExpanded
boolean attribute as seen in the example below.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<List>
<DisclosureGroup phx-change="toggle" isExpanded={@is_expanded}>
<Text template="label">Level 1</Text>
<Text>Item 1</Text>
<Text>Item 2</Text>
<Text>Item 3</Text>
</DisclosureGroup>
</List>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :is_expanded, false)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("toggle", %{"isExpanded" => is_expanded}, socket) do
{:noreply, assign(socket, is_expanded: is_expanded)}
end
end
Multiple Expandable Lists
The next example shows one pattern for displaying multiple expandable lists without needing to write multiple event handlers.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<List>
<DisclosureGroup phx-change="toggle-1" isExpanded={@expanded_groups[1]}>
<Text template="label">Level 1</Text>
<Text>Item 1</Text>
<DisclosureGroup phx-change="toggle-2" isExpanded={@expanded_groups[2]}>
<Text template="label">Level 2</Text>
<Text>Item 2</Text>
</DisclosureGroup>
</DisclosureGroup>
</List>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :expanded_groups, %{1 => false, 2 => false})}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("toggle-" <> level, %{"isExpanded" => is_expanded}, socket) do
level = String.to_integer(level)
{:noreply,
assign(
socket,
:expanded_groups,
Map.replace!(socket.assigns.expanded_groups, level, is_expanded)
)}
end
end
Forms
In Phoenix, form elements must be inside of a form. Phoenix only captures events if the element is wrapped in a form. However in SwiftUI there is no similar concept of forms. To bridge the gap, we built the LiveView Native Live Form library. This library provides several views to enable writing views in a single form.
Phoenix Applications setup with LiveView native include a core_components.ex
file. This file contains several components for building forms. Generally, We recommend using core components rather than the views. We're going to cover the views directly so you understand how to build forms from scratch and how we built the core components. However, in the Forms and Validations reading we'll cover using core components.
LiveForm
The LiveForm
view must wrap views to capture events from the phx-change
or phx-submit
event. The phx-change
event sends a message to the LiveView anytime the control or indicator changes its value. The phx-submit
event sends a message to the LiveView when a user clicks the LiveSubmitButton
. The params of the message are based on the name of the Binding argument of the view's initializer in SwiftUI.
Here's some example boilerplate for a LiveForm
. The id
attribute is required.
<LiveForm id="some-id" phx-change="change" phx-submit="submit">
<!-- inputs go here -->
<LiveSubmitButton>Button Text</LiveSubmitButton>
</LiveForm>
Basic Example using TextField
The following example shows you how to connect a SwiftUI TextField with a phx-change
event and phx-submit
binding to a corresponding event handler.
Evaluate the example below. Type into the text field and press submit on your iOS simulator. Notice the inspected params
appear in the server logs in the console below as a map of %{"my-input" => value}
based on the name
attribute on the TextField
view.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="change" phx-submit="submit">
<TextField name="my-input">Enter text here</TextField>
<LiveSubmitButton>Submit</LiveSubmitButton>
</LiveForm>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
require Logger
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("change", params, socket) do
Logger.info("Change params: #{inspect(params)}")
{:noreply, socket}
end
@impl true
def handle_event("submit", params, socket) do
Logger.info("Submitted params: #{inspect(params)}")
{:noreply, socket}
end
end
Event Handlers
The phx-change
and phx-submit
event handlers should generally be bound to the LiveForm. However, you can also bind the event handlers directly to the input view if you want to separately handle a single view's change events.
<LiveForm id="my-form">
<TextField name="my-input" phx-change="change">Enter text here</TextField>
<LiveSubmitButton>Submit</LiveSubmitButton>
</LiveForm>
Controls and Indicators
SwiftUI organizes interactive views in the Controls and Indicators section. You may refer to this documentation when looking for views that belong within a form.
We'll demonstrate how to work with a few common control and indicator views.
Slider
This code example renders a SwiftUI Slider. It triggers the change event when the slider is moved and sends a "slide"
message. The "slide"
event handler then logs the value to the console.
The Slider view uses named content areas minumumValueLabel
and maximumValueLabel
. The example below demonstrates how to represent these areas using the template
attribute.
This example also demonstrates how to use the params sent by the slider to store a value in the socket and use it elsewhere in the template.
Evaluate the example and enter some text in your iOS simulator.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="slide">
<Slider
name="my-slider"
lowerBound={0}
upperBound={100}
step={1}
>
<Text template={:minimumValueLabel}>0%</Text>
<Text template={:maximumValueLabel}>100%</Text>
</Slider>
</LiveForm>
<Text><%= @percentage %></Text>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :percentage, 0)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("slide", %{"my-slider" => value}, socket) do
{:noreply, assign(socket, :percentage, value)}
end
end
Stepper
This code example renders a SwiftUI Stepper. It triggers the change event and sends a "change-tickets"
message when the stepper increments or decrements. The "change-tickets"
event handler then updates the number of tickets stored in state, which appears in the UI.
Evaluate the example and increment/decrement the step.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form">
<Stepper
name="my-stepper"
lower-bound={0}
upper-bound={16}
step={1}
phx-change="change-tickets"
>
Tickets <%= @tickets %>
</Stepper>
</LiveForm>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :tickets, 0)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("change-tickets", %{"my-stepper" => tickets}, socket) do
{:noreply, assign(socket, :tickets, tickets)}
end
end
Toggle
This code example renders a SwiftUI Toggle. It triggers the change event and sends a "toggle"
message when toggled. The "toggle"
event handler then updates the :on
field in state, which allows the Toggle
view to be toggled o through the isOn
attribute.
Evaluate the example below and click on the toggle.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="toggle">
<Toggle isOn={@on} name="my-toggle">On/Off</Toggle>
</LiveForm>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :on, false)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("toggle", %{"my-toggle" => on}, socket) do
{:noreply, assign(socket, :on, on)}
end
end
DatePicker
The SwiftUI Date Picker provides a native view for selecting a date. The date is selected by the user and sent back as a string. Evaluate the example below and select a date to see the date params appear in the console below.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="pick-date">
<DatePicker name="my-date-picker"/>
</LiveForm>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
require Logger
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :date, nil)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("pick-date", params, socket) do
Logger.info("Date Params: #{inspect(params)}")
{:noreply, socket}
end
end
Parsing Dates
The date from the DatePicker
is in iso8601 format. You can use the from_iso8601
function to parse this string into a DateTime
struct.
iso8601 = "2024-01-17T20:51:00.000Z"
DateTime.from_iso8601(iso8601)
Your Turn: Displayed Components
The DatePicker
view accepts a displayedComponents
attribute with the value of "hourAndMinute"
or "date"
to only display one of the two components. By default, the value is "all"
.
You're going to change the displayedComponents
attribute in the example below to see both of these options. Change "all"
to "date"
, then to "hourAndMinute"
. Re-evaluate the cell between changes and see the updated UI.
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="pick-date">
<DatePicker displayedComponents="date" name="my-date-picker" />
</LiveForm>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("pick-date", _params, socket) do
{:noreply, socket}
end
end
Small Project: Todo List
Using the previous examples as inspiration, you're going to create a todo list.
Requirements
- Items should be
Text
views rendered within aList
view. - Item ids should be stored in state as a list of integers i.e.
[1, 2, 3, 4]
- Use a
TextField
to provide the name of the next added todo item. - An add item
Button
should add items to the list of integers in state when pressed. - A delete item
Button
should remove the currently selected item from the list of integers in state when pressed.
Example Solution
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<LiveForm id="my-form" phx-change="type-name" >
<TextField name="text" text={@item_name}>Todo...</TextField>
</LiveForm>
<Button phx-click="add-item">Add Item</Button>
<Button phx-click="delete-item">Delete Item</Button>
<List selection={@selection} phx-change="selection-changed">
<Text id={id} :for={{id, content} <- @items}><%= content %></Text>
</List>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, items: [], selection: "None", item_name: "", next_item_id: 1)}
end
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("type-name", %{"text" => name}, socket) do
{:noreply, assign(socket, :item_name, name)}
end
def handle_event("add-item", _params, socket) do
updated_items = [
{"item-#{socket.assigns.next_item_id}", socket.assigns.item_name}
| socket.assigns.items
]
{:noreply,
assign(socket,
item_name: "",
items: updated_items,
next_item_id: socket.assigns.next_item_id + 1
)}
end
def handle_event("delete-item", _params, socket) do
updated_items =
Enum.reject(socket.assigns.items, fn {id, _name} -> id == socket.assigns.selection end)
{:noreply, assign(socket, :items, updated_items)}
end
def handle_event("selection-changed", %{"selection" => selection}, socket) do
{:noreply, assign(socket, selection: selection)}
end
end
Enter Your Solution Below
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
<!-- Enter your solution below -->
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
# Define your mount/3 callback
@impl true
def render(assigns), do: ~H""
# Define your render/3 callback
# Define any handle_event/3 callbacks
end