Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

SwiftUI Views

livebooks/swiftui-views.livemd

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:

  • VStack: Vertically arranges nested views.
  • HStack: Horizontally arranges nested views.

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.

Apple Documentation

> 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.