View Source Stylesheets

Run in Livebook

Overview

In this guide, you'll learn how to use stylesheets to customize the appearance of your LiveView Native Views. You'll also learn about the inner workings of how LiveView Native uses stylesheets to implement modifiers, and how those modifiers style and customize SwiftUI Views. By the end of this lesson, you'll have the fundamentals you need to create beautiful native UIs.

The Stylesheet AST

LiveView Native parses through your application at compile time to create a stylesheet AST representation of all the styles in your application. This stylesheet AST is used by the LiveView Native Client application when rendering the view hierarchy to apply modifiers to a given view.

sequenceDiagram
    LiveView->>LiveView: Create stylesheet
    Client->>LiveView: Send request to "http://localhost:4000/?_format=swiftui"
    LiveView->>Client: Send LiveView Native template in response
    Client->>LiveView: Send request to "http://localhost:4000/assets/app.swiftui.styles"
    LiveView->>Client: Send stylesheet in response
    Client->>Client: Parses stylesheet into SwiftUI modifiers
    Client->>Client: Apply modifiers to the view hierarchy

We've setup this Livebook to be included when parsing the application for modifiers. You can visit http://localhost:4000/assets/app.swiftui.styles to see the Stylesheet AST created by all of the styles in this Livebook and any other styles used in the kino_live_view_native project.

LiveView Native watches for changes and updates the stylesheet, so those will be dynamically picked up and applied, You may notice a slight delay as the Livebook takes 5 seconds to write its contents to a file.

Modifiers

SwiftUI employs modifiers to style and customize views. In SwiftUI syntax, each modifier is a function that can be chained onto the view they modify. LiveView Native has a minimal DSL (Domain Specific Language) for writing SwiftUI modifiers.

You can apply modifiers through a class defined in a LiveView Native Stylesheet as described in the LiveView Native Stylesheets section, or through the inline style attribute as described in the Utility Styles section.

SwiftUI Modifiers

Here's a basic example of making text red using the foregroundStyle modifier.

Text("Some Red Text")
  .foregroundStyle(.red)

Many modifiers can be applied to a view. Here's an example using foregroundStyle and frame.

Text("Some Red Text")
  .foregroundStyle(.red)
  .font(.title)

Implicit Member Expression

Implicit Member Expression in SwiftUI means that we can implicityly access a member of a given type without explicitly specifying the type itself. For example, the .red value above is from the Color structure.

Text("Some Red Text")
  .foregroundStyle(Color.red)

LiveView Native Modifiers

The DSL (Domain Specific Language) used in LiveView Native drops the . dot before each modifier, but otherwise remains largely the same. We do not document every modifier separately, since you can translate SwiftUI examples into the DSL syntax.

For example, Here's the same foregroundStyle modifier as it would be written in a LiveView Native stylesheet or style attribute, which we'll cover in a moment.

foregroundStyle(.red)

There are some exceptions where the DSL differs from SwiftUI syntax, which we'll cover in the sections below.

Utility Styles

In addition to introducing stylesheets, LiveView Native 0.3.0 also introduced Utility styles, which will be our prefered method for writing styles in these Livebook guides.

Utility styles are comperable to inline styles in HTML, which have been largely discouraged in the CSS community. We recommend Utility styles for now as the easiest way to prototype applications. However, we hope to replace Utility styles with a more mature styling framework in the future.

The same SwiftUI syntax used inside of a stylesheet can be used directly inside of a style attribute. The example below defines the foregroundStyle(.red) modifier. Evaluate the example and view it in your simulator.

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

  def render(assigns) do
    ~LVN"""
    <Text style="foregroundStyle(.red)">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

Multiple Modifiers

You can write multiple modifiers separated by a semi-color ;.

<Text style="foregroundStyle(.blue);font(.title)">Hello, from LiveView Native!</Text>

To include newline characters in your string wrap the string in curly brackets {}. Using multiple lines can better organize larger amounts of modifiers.

<Text style={
  "
  foregroundStyle(.blue);
  font(.title);
  "
}>
Hello, from LiveView Native!
</Text>

Dynamic Style Names

LiveView Native parses styles in your project to define a single stylesheet. You can find the AST representation of this stylesheet at http://localhost:4000/assets/app.swiftui.styles. This stylesheet is compiled on the server and then sent to the client. For this reason, class names must be fully-formed. For example, the following style using string interpolation is invalid.

<Text style={"foregroundStyle(.#{Enum.random(["red", "blue"])})"}>
Invalid Example
</Text>

However, we can still use dynamic styles so long as the modifiers are fully formed.

<Text style={"#{Enum.random(["foregroundStyle(.red)", "foregroundStyle(.blue)]")}"}>
Red or Blue Text
</Text>

Evaluate the example below multiple times while watching your simulator. Notice that the text is dynamically red or blue.

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

  def render(assigns) do
    ~LVN"""
    <Text style={"#{Enum.random(["foregroundStyle(.red)", "foregroundStyle(.blue)"])}"}>
    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

Modifier Order

Modifier order matters. Changing the order that modifers are applied can have a significant impact on their behavior.

To demonstrate this concept, we're going to take a simple example of applying padding and background color.

If we apply the background color first, then the padding, The background is applied to original view, leaving the padding filled with whitespace.

background(.orange)
padding(20)
flowchart

subgraph Padding
 View
end

style View fill:orange

If we apply the padding first, then the background, the background is applied to the view with the padding, thus filling the entire area with background color.

padding(20)
background(.orange)
flowchart

subgraph Padding
 View
end

style Padding fill:orange
style View fill:orange

Evaluate the example below to see this in action.

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

  def render(assigns) do
    ~LVN"""
    <Text style="background(.orange);padding()">Hello, from LiveView Native!</Text>
    <Text style="padding();background(.orange)">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

Custom Colors

SwiftUI Color Struct

The SwiftUI Color structure accepts either the name of a color in the asset catalog or the RGB values of the color.

Therefore we can define custom RBG styles like so:

foregroundStyle(Color(.sRGB, red: 0.4627, green: 0.8392, blue: 1.0))

Evaluate the example below to see the custom color in your simulator.

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

  def render(assigns) do
    ~LVN"""
    <VStack>
      <Text style={[
        "bold()",
        "foregroundStyle(Color(\"MyColor\"))"
      ]}>Hello</Text>
    </VStack>
    """
  end
end

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

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

Custom Colors in the Asset Catalogue

Custom colors can be defined in the Asset Catalogue. Once defined in the asset catalogue of the Xcode application, the color can be referenced by name like so:

foregroundStyle(Color("MyColor"))

Generally using the asset catalog is more performant and customizable than using custom RGB colors with the Color struct.

Your Turn: Custom Colors in the Asset Catalog

Custom colors can be defined in the asset catalog (https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs). You're going to define a color in the asset catolog then evaluate the example below to see the color appear in your simulator.

To create a new color go to the Assets folder in your iOS app and create a new color set.

XCode assets folder

To create a color set, enter the RGB values or a hexcode as shown in the image below. If you don't see the sidebar with color options, click the icon in the top-right of your Xcode app and click the Show attributes inspector icon shown highlighted in blue.

Xcode enter RGB values for color set

The defined color is now available for use within LiveView Native styles. However, the app needs to be re-compiled to pick up a new color set.

Re-build your SwiftUI Application before moving on. Then evaluate the code below. You should see your custom colored text in the simulator.

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

  def render(assigns) do
    ~LVN"""
    <Text style={"foregroundStyle(Color(\"MyColor\"))"}>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"Hello"
end

LiveView Native Stylesheets

In LiveView Native, we use ~SHEET sigil stylesheets to organize modifers by classes using an Elixir-oriented DSL similar to CSS for styling web elements.

We group modifiers together within a class that can be applied to an element. Here's an example of how modifiers can be grouped into a "red-title" class in a stylesheet:

~SHEET"""
  "red-title" do
    foregroundColor(.red);
    font(.title);
  end
"""

We're mostly using Utility styles for these guides, but the stylesheet module does contain some important configuration to @import the utility styles module. It can also be used to group styles within a class if you have a set of modifiers you're repeatedly using and want to group together.

defmodule ServerWeb.Styles.App.SwiftUI do
  use LiveViewNative.Stylesheet, :swiftui
  @import LiveViewNative.SwiftUI.UtilityStyles

  ~SHEET"""
    "red-title" do
      foregroundColor(.red);
      font(.title);
    end
  """
end

You can apply these classes through the class attribute.

<Text class="red-title">Red Title Text</Text>

Injecting Views in Stylesheets

SwiftUI modifiers sometimes accept SwiftUI views as arguments. Here's an example using the clipShape modifier with a Circle view.

Image("logo")
  .clipShape(Circle())

However, LiveView Native does not support using SwiftUI views directly within a stylesheet. Instead, we have a few alternative options in cases like this where we want to use a view within a modifier.

Using Members on a Given Type

We can't use the Circle view directly. However, the Getting standard shapes documentation describes methods for accessing standard shapes. For example, we can use Circle.circle for the circle shape.

We can use Circle.circle instead of the Circle view. So, the following code is equivalent to the example above.

Image("logo")
  .clipShape(Circle.circle)

However, in LiveView Native we only support using implicit member expression syntax, so instead of Circle.circle, we only write .circle.

Image("logo")
  .clipShape(.circle)

Which is simple to convert to the LiveView Native DSL using the rules we've already learned.

"example-class" do
  clipShape(.circle)
end

Injecting a View

For more complex cases, we can inject a view directly into a stylesheet.

Here's an example where this might be useful. SwiftUI has modifers that represent a named content area for views to be placed within. These views can even have their own modifiers, so it's not enough to use a simple static property on the Shape type.

Image("logo")
  .overlay(content: {
    Circle().stroke(.red, lineWidth: 4)
  })

To get around this issue, we instead inject a view into the stylesheet. First, define the modifier and use an atom to represent the view that's going to be injected.

"overlay-circle" do
  overlay(content: :circle)
end

Then use the template attribute on the view to be injected into the stylesheet.

<Image style="overlay-circle">
  <Circle template="circle" style="stroke(.red, lineWidth: 4)" >
</Image>

Apple Documentation

You can find documentation and examples of modifiers on Apple's SwiftUI documentation which is comprehensive and thorough, though it may feel unfamiliar at first for Elixir Developers when compared to HexDocs.

Finding Modifiers

The Configuring View Elements section of apple documentation contains links to modifiers organized by category. In that documentation you'll find useful references such as Style Modifiers, Layout Modifiers, and Input and Event Modifiers.

You can also find more on modifiers with LiveView Native examples on the liveview-client-swiftui HexDocs.

Visual Studio Code Extension

If you use Visual Studio Code, we strongly recommend you install the LiveView Native Visual Studio Code Extension which provides autocompletion and type information thus making modifiers significantly easier to write and lookup.

Your Turn: Syntax Conversion

Part of learning LiveView Native is learning SwiftUI. Fortunately we can leverage the existing SwiftUI ecosystem and convert examples into LiveView Native syntax.

You're going to convert the following SwiftUI code into a LiveView Native template. This example is inspired by the official SwiftUI Tutorials.

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    HStack {
        Text("Joshua Tree National Park")
        Spacer()
        Text("California")
    }
    .font(.subheadline)

    Divider()

    Text("About Turtle Rock")
        .font(.title2)
    Text("Descriptive text goes here")
}
.padding()

Example Solution

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

  def render(assigns) do
    ~LVN"""
    <VStack>
    <VStack alignment="leading" style="padding()">
      <Text style="font(.title)">Turtle Rock</Text>
      <HStack style="font(.subheadline)">
        <Text>Joshua Tree National Park</Text>
        <Spacer/>
        <Text>California</Text>
      </HStack>
      <Divider/>
      <Text style="font(.title2)">About Turtle Rock</Text>
      <Text>Descriptive text goes here</Text>
    </VStack>
    """
  end
end

Enter your solution below.

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

  def render(assigns) do
    ~LVN"""
    <!-- Template Code Goes Here -->
    """
  end
end