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/2triggers themount/3callback and re-establishes a socket connection. -
push_navigate/2triggers themount/3callback and re-uses the existing socket connection. -
push_patch/2does not trigger themount/3callback, but does trigger thehandle_params/3callback. 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