SwiftUI Views
notebook_path = __ENV__.file |> String.split("#") |> hd()
Mix.install(
[
{:kino_live_view_native, github: "liveview-native/kino_live_view_native"}
],
config: [
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: 4000],
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}$/
]
]
]}
],
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
LiveView Native aims to use minimal SwiftUI code and use the same patterns for building interactive UIs as LiveView. However, unlike LiveView for the web, LiveView Native uses SwiftUI templates to build the native UI.
This lesson will teach you how to build SwiftUI templates using common SwiftUI views. We’ll cover common uses of each view and give you practical examples you can use to build your own native UIs. This lesson is like a recipe book you can refer back to whenever you need an example of how to use a particular SwiftUI view. In addition, once you understand how to convert these views into the LiveView Native DSL, you should have the tools to convert essentially any SwiftUI View into the LiveView Native DSL.
Render Components
LiveView Native 0.3.0
introduced render components to better encourage isolation of native and web templates and move away from co-location templates within the same LiveView module.
Render components are namespaced under the main LiveView, and are responsible for defining the render/1
callback function that returns the native template.
For example, and ExampleLive
LiveView module would have an ExampleLive.SwiftUI
render component module for the native Template.
This ExampleLive.SwiftUI
render component may define a render/1
callback function as seen below.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
# Render Component
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
Hello, from LiveView Native!
"""
end
end
# LiveView
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns) do
~H"""
Hello from LiveView!
"""
end
end
|> Server.SmartCells.LiveViewNative.register("/")
import Server.Livebook, only: []
import Kernel
:ok
To change the template, some cells only define the render component rather than the entire LiveView. Evaluate the cell below and notice the template changes in your LVN GO app.
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, _interface) do
~LVN"""
Hello, from a LiveView Native Render Component!
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Embedding Templates
Alternatively, you may omit the render callback and instead define a .neex
(Native + Embedded Elixir) template.
By default, the module above would look for a template in the swiftui/example_live*
path relative to the module’s location. You can see the LiveViewNative.Component
documentation for further explanation.
For the sake of ease when working in Livebook, we’ll prefer defining the render/1
callback. However, we recommend you generally prefer template files when working locally in Phoenix LiveView Native projects.
SwiftUI Views
In SwiftUI, a “View” is like a building block for what you see on your app’s screen. It can be something simple like text or an image, or something more complex like a layout with multiple elements. Views are the pieces that make up your app’s user interface.
Here’s an example Text
view that represents a text element.
Text("Hamlet")
LiveView Native uses the following syntax to represent the view above.
Hamlet
SwiftUI provides a wide range of Views that can be used in native templates. You can find a full reference of these views in the SwiftUI Documentation at https://developer.apple.com/documentation/swiftui/. You can also find a shorthand on how to convert SwiftUI syntax into the LiveView Native DLS in the LiveView Native Syntax Conversion Cheatsheet.
Text
We’ve already seen the Text view, but we’ll start simple to get the interactive tutorial running.
Evaluate the cell below and connect to http://localhost:4000 in the LVN GO application to view the template.
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, from LiveView Native!
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
HStack and VStack
SwiftUI includes many Layout container views you can use to arrange your user Interface. Here are a few of the most commonly used:
Below, we’ve created a simple 3X3 game board to demonstrate how to use VStack
and HStack
to build a layout of horizontal rows in a single vertical column.o
Here’s a diagram to demonstrate how these rows and columns create our desired layout.
flowchart
subgraph VStack
direction TB
subgraph H1[HStack]
direction LR
1[O] --> 2[X] --> 3[X]
end
subgraph H2[HStack]
direction LR
4[X] --> 5[O] --> 6[O]
end
subgraph H3[HStack]
direction LR
7[X] --> 8[X] --> 9[O]
end
H1 --> H2 --> H3
end
Evaluate the example below and view the working 3X3 layout in your LVN GO app.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
O
X
X
X
O
O
X
X
O
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Your Turn: 3x3 board using columns
In the cell below, use VStack
and HStack
to create a 3X3 board using 3 columns instead of 3 rows as demonstrated above. The arrangement of X
and O
does not matter, however the content will not be properly aligned if you do not have exactly one character in each Text
element.
flowchart
subgraph HStack
direction LR
subgraph V1[VStack]
direction TB
1[O] --> 2[X] --> 3[X]
end
subgraph V2[VStack]
direction TB
4[X] --> 5[O] --> 6[O]
end
subgraph V3[VStack]
direction TB
7[X] --> 8[X] --> 9[O]
end
V1 --> V2 --> V3
end
Example Solution
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns, _interface) do
~LVN"""
O
X
X
X
O
O
X
X
O
"""
end
end
Enter Your Solution Below
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Grid
VStack
and HStack
do not provide vertical-alignment between horizontal rows. Notice in the following example that the rows/columns of the 3X3 board are not aligned, just centered.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
X
X
X
O
O
X
O
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Fortunately, we have a few common elements for creating a grid-based layout.
- Grid: A grid that arranges its child views in rows and columns that you specify.
- GridRow: A view that arranges its children in a horizontal line.
A grid layout vertically and horizontally aligns elements in the grid based on the number of elements in each row.
Evaluate the example below and notice that rows and columns are aligned.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
XX
X
X
X
X
X
X
X
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
List
The SwiftUI List view provides a system-specific interface, and has better performance for large amounts of scrolling elements.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Item 1
Item 2
Item 3
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Multi-dimensional lists
Alternatively we can separate children within a List
view in a Section
view as seen in the example below. Views in the Section
can have the template
attribute with a "header"
or "footer"
value which controls how the content is displayed above or below the section.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Header
Content
Footer
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
ScrollView
The SwiftUI ScrollView displays content within a scrollable region. ScrollView is often used in combination with LazyHStack, LazyVStack, LazyHGrid, and LazyVGrid to create scrollable layouts optimized for displaying large amounts of data.
While ScrollView
also works with typical VStack
and HStack
views, they are not optimal choices for large amounts of data.
ScrollView with VStack
Here’s an example using a ScrollView
and a VStack
to create scrollable text arranged vertically.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Item <%= n %>
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
ScrollView with HStack
By default, the axes of a ScrollView
is vertical. To make a horizontal ScrollView
, set the axes
attribute to "horizontal"
as seen in the example below.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Item <%= n %>
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Optimized ScrollView with LazyHStack and LazyVStack
VStack
and HStack
are inefficient for large amounts of data because they render every child view. To demonstrate this, evaluate the example below. You should experience lag when you attempt to scroll.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Item <%= n %>
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
To resolve the performance problem for large amounts of data, you can use the Lazy views. Lazy views only create items as needed. Items won’t be rendered until they are present on the screen.
The next example demonstrates how using LazyVStack
instead of VStack
resolves the performance issue.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Item <%= n %>
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Spacers
Spacers take up all remaining space in a container.
> Image originally from https://developer.apple.com/documentation/swiftui/spacer
Evaluate the following example and notice the Text
element is pushed to the right by the Spacer
.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
This text is pushed to the right
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Your Turn: Bottom Text Spacer
In the cell below, use VStack
and Spacer
to place text in the bottom of the native view.
Example Solution
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns, _interface) do
~LVN"""
Hello
"""
end
end
Enter Your Solution Below
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
AsyncImage
AsyncImage
is best for network images, or images served by the Phoenix server.
Here’s an example of AsyncImage
with a lorem picsum image from https://picsum.photos/400/600.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Loading Spinner
AsyncImage
displays a loading spinner while loading the image. Here’s an example of using AsyncImage
without a URL so that it loads forever.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Relative Path
For images served by the Phoenix server, LiveView Native evaluates URLs relative to the LiveView’s host URL. This way you can use the path to static resources as you normally would in a Phoenix application.
For example, the path /images/logo.png
evaluates as http://localhost:4000/images/logo.png below. This serves the LiveView Native logo.
Evaluate the example below to see the LiveView Native logo in the iOS simulator.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Image
The Image
element is best for system images such as the built in SF Symbols.
You can also use Image
with asset catalogue, which requires using an XCode application instead of LVN GO.
System Images
You can use the systemName
attribute to provide the name of system images to the Image
element.
For the full list of SF Symbols you can download Apple’s Symbols 5 application.
Evaluate the cell below to see an example using the square.and.arrow.up
symbol.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Button
A Button is a clickable SwiftUI View.
The label of a button can be any view, such as a Text view for text-only buttons or a Label view for buttons with icons.
Evaluate the example below to see the SwiftUI Button element.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.ExampleLive.SwiftUI do
use LiveViewNative.Component,
format: :swiftui
def render(assigns, _interface) do
~LVN"""
Text Button
Icon Button
"""
end
end
|> Server.SmartCells.RenderComponent.register()
import Server.Livebook, only: []
import Kernel
:ok
Further Resources
See the SwiftUI Documentation for a complete list of SwiftUI elements and the LiveView Native SwiftUI Documentation for LiveView Native examples of the SwiftUI elements.