Native Navigation
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
This guide will teach you how to create multi-page applications using LiveView Native. We will cover navigation patterns specific to native applications and how to reuse the existing navigation patterns available in LiveView.
Before diving in, you should have a basic understanding of navigation in LiveView. You should be familiar with the redirect/2, push_patch/2 and push_navigate/2 functions, which are used to trigger navigation from within a LiveView. Additionally, you should know how to define routes in the router using the live/4 macro.
NavigationStack
LiveView Native applications are generally wrapped in a NavigationStack view. This view usually exists in the root.swiftui.heex
file, which looks something like the following:
<.csrf_token />
<%= @inner_content %>
The NavigationStack
view stacks pages on top of eachother. To see this in action, we’ll walk through an example of viewing the LiveView Native template code sent by the application.
Evaluate the code cell below. We’ll view the source code in a moment.
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
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
Visit http://localhost:4000/?_format=swiftui. The ?_format
query parameter specifies the Phoenix server should respond with the swiftui template rather than the web template. You should see source code similar to the example below. We’ve replaced long tokens with "some token"
for the sake of readability.
Hello, from LiveView Native!
Notice the NavigationStack view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages.
Navigation Links
We can use the NavigationLink view for native navigation, similar to how we can use the .link component with the navigate
attribute for web navigation.
We’ve created the same example of navigating between the Main
and About
pages. Each page using a NavigationLink
to navigate to the other page.
Evaluate both of the code cells below and click on the NavigationLink
in your simulator to navigate between the two views.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.HomeLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
You are on the home page
To about
"""
end
end
defmodule ServerWeb.HomeLive 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
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.AboutLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
You are on the about page
To home
"""
end
end
defmodule ServerWeb.AboutLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
end
|> Server.SmartCells.LiveViewNative.register("/about")
import Server.Livebook, only: []
import Kernel
:ok
The destination
attribute works the same as the navigate
attribute on the web. The current LiveView will shut down, and a new one will mount without re-establishing a new socket connection.
Link Component
The link component wraps the NavigationLink
and Link
view. It accepts both the navigation
and href
attributes depending on the type of navigation you want to trigger. navigation
preserves the socket connection and is best used for navigation within the application. href
uses the Link
view to navigate to an external resource using the native browser.
Evaluate both of the code cells below and click on the NavigationLink
in your simulator to navigate between the two views.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.HomeLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
You are on the home page
<.link navigate="about" >To about
"""
end
end
defmodule ServerWeb.HomeLive 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
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"""
You are on the about page
<.link navigate="home" >To home
"""
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
The href
attribute is best used for external sites that the device will open in the native browser. Evaluate the example below and click the link to navigate to https://www.google.com.
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"""
<.link href="https://www.google.com">To Google
"""
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
Push Navigation
For LiveView Native views, we can still use the same redirect/2, push_patch/2, and push_navigate/2 functions used in typical LiveViews.
These functions are preferable over NavigationLink
views when you want to share navigation handlers between web and native, and/or when you want to have more customized navigation handling.
Evaluate both of the code cells below and click on the Button
view in your simulator that triggers the handle_event/3
navigation handler to navigate between the two views.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.HomeLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
You are on the home page
To about
"""
end
end
defmodule ServerWeb.HomeLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("to-about", _params, socket) do
{:noreply, push_navigate(socket, to: "/about")}
end
end
|> Server.SmartCells.LiveViewNative.register("/")
import Server.Livebook, only: []
import Kernel
:ok
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
defmodule ServerWeb.AboutLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
You are on the about page
To home
"""
end
end
defmodule ServerWeb.AboutLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def render(assigns), do: ~H""
@impl true
def handle_event("to-main", _params, socket) do
{:noreply, push_navigate(socket, to: "/")}
end
end
|> Server.SmartCells.LiveViewNative.register("/about")
import Server.Livebook, only: []
import Kernel
:ok
Routing
The KinoLiveViewNative
smart cells used in this guide automatically define routes for us. Be aware there is no difference between how we define routes for LiveView or LiveView Native.
The routes for the main and about pages might look like the following in the router:
live "/", Server.MainLive
live "/about", Server.AboutLive
Native Navigation Events
LiveView Native navigation mirrors the same navigation behavior you’ll find on the web.
Evaluate the example below and press each button. Notice that:
-
redirect/2
triggers themount/3
callback and re-establishes a socket connection. -
push_navigate/2
triggers themount/3
callback and re-uses the existing socket connection. -
push_patch/2
does not trigger themount/3
callback, but does trigger thehandle_params/3
callback. This is often useful when using navigation to trigger page changes such as displaying a modal or overlay.
You can see this for yourself using the following example. Click each of the buttons for redirect, navigate, and patch behavior. Try to understand each navigation type, and which callback functions the navigation type triggers.
require Server.Livebook
import Server.Livebook
import Kernel, except: [defmodule: 2]
# This module built for example purposes to persist logs between mounting LiveViews.
defmodule PersistantLogs do
def get do
:persistent_term.get(:logs)
end
def put(log) when is_binary(log) do
:persistent_term.put(:logs, [{log, Time.utc_now()} | get()])
end
def reset do
:persistent_term.put(:logs, [])
end
end
PersistantLogs.reset()
defmodule ServerWeb.ExampleLive.SwiftUI do
use ServerNative, [:render_component, format: :swiftui]
def render(assigns) do
~LVN"""
Redirect
Navigate
Patch
Socket ID<%= @socket_id %>
LiveView PID:<%= @live_view_pid %>
<%= for {log, time} <- Enum.reverse(@logs) do %>
<%= Calendar.strftime(time, "%H:%M:%S") %>:
<%= log %>
<% end %>
"""
end
end
defmodule ServerWeb.ExampleLive do
use ServerWeb, :live_view
use ServerNative, :live_view
@impl true
def mount(_params, _session, socket) do
PersistantLogs.put("MOUNT")
{:ok,
assign(socket,
socket_id: socket.id,
connected: connected?(socket),
logs: PersistantLogs.get(),
live_view_pid: inspect(self())
)}
end
@impl true
def handle_params(_params, _url, socket) do
PersistantLogs.put("HANDLE PARAMS")
{:noreply, assign(socket, :logs, PersistantLogs.get())}
end
@impl true
def render(assigns),
do: ~H"""
Do thing
"""
def handle_event("do-thing", _params, socket) do
IO.inspect("DOING THING")
{:noreply, socket}
end
@impl true
def handle_event("redirect", _params, socket) do
PersistantLogs.reset()
PersistantLogs.put("--REDIRECTING--")
{:noreply, redirect(socket, to: "/")}
end
def handle_event("navigate", _params, socket) do
PersistantLogs.put("---NAVIGATING---")
{:noreply, push_navigate(socket, to: "/")}
end
def handle_event("patch", _params, socket) do
PersistantLogs.put("----PATCHING----")
{:noreply, push_patch(socket, to: "/")}
end
end
|> Server.SmartCells.LiveViewNative.register("/")
import Server.Livebook, only: []
import Kernel
:ok