View Source Native Navigation

Run in Livebook

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.

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 />
<Style url={~p"/assets/app.swiftui.styles"} />
<NavigationStack>
  <%= @inner_content %>
</NavigationStack>

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.

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

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.

<csrf-token value="sometoken"></csrf-token>
<Style url="/assets/app.swiftui.styles"></Style>
<NavigationStack>
  <div id="phx-F8bMeC0NpvsZjPJC" data-phx-main data-phx-session="sometoken" data-phx-static="sometoken">
    <Text>Hello, from LiveView Native!</Text>
  </div>
</NavigationStack><iframe hidden height="0" width="0" src="/phoenix/live_reload/frame"></iframe>

Notice the NavigationStack view wraps the template. This view manages the state of navigation history and allows for navigating back to previous pages.

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.

defmodule ServerWeb.HomeLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the home page</Text>
    <NavigationLink destination={~p"/about"}>
        <Text>To about</Text>
    </NavigationLink>
    """
  end
end

defmodule ServerWeb.HomeLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end
defmodule ServerWeb.AboutLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the about page</Text>
    <NavigationLink destination={~p"/"}>
        <Text>To home</Text>
    </NavigationLink>
    """
  end
end

defmodule ServerWeb.AboutLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end

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.

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.

defmodule ServerWeb.HomeLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the home page</Text>
     <.link navigate="about" >To about</.link>
    """
  end
end

defmodule ServerWeb.HomeLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end
defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the about page</Text>
    <.link navigate="home" >To home</.link>
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end

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.

defmodule ServerWeb.ExampleLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <.link href="https://www.google.com">To Google</.link>
    """
  end
end

defmodule ServerWeb.ExampleLive do
  use ServerWeb, :live_view
  use ServerNative, :live_view

  @impl true
  def render(assigns), do: ~H""
end

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.

defmodule ServerWeb.HomeLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the home page</Text>
    <Button phx-click="to-about">To about</Button>
    """
  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
defmodule ServerWeb.AboutLive.SwiftUI do
  use ServerNative, [:render_component, format: :swiftui]

  def render(assigns) do
    ~LVN"""
    <Text>You are on the about page</Text>
    <Button phx-click="to-main">To home</Button>
    """
  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

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:

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

# 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"""
    <Button phx-click="redirect">Redirect</Button>
    <Button phx-click="navigate">Navigate</Button>
    <Button phx-click="patch">Patch</Button>
    <ScrollView>
      <Grid>
      <GridRow><Text>Socket ID</Text><Text><%= @socket_id %></Text></GridRow>
      <GridRow><Text>LiveView PID:</Text><Text><%= @live_view_pid %></Text></GridRow>
      <%= for {log, time} <- Enum.reverse(@logs) do %>
        <GridRow>
          <Text><%= Calendar.strftime(time, "%H:%M:%S") %>:</Text>
          <Text><%= log %></Text>
        </GridRow>
      <% end %>
      </Grid>
    </ScrollView>
    """
  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"""
    <button phx-click="do-thing">Do thing</button>
    """

  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