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

Stylesheets

livebooks/stylesheets.livemd

Stylesheets

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

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.

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

Multiple Modifiers

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

Hello, from LiveView Native!

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


Hello, from LiveView Native!

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.


Invalid Example

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


Red or Blue Text

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

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

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.

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

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.

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

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.

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
    foregroundStyle(.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
      foregroundStyle(.red)
      font(.title)
    end
  """
end

You can apply these classes through the class attribute.

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


  

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"""
    
      Turtle Rock
      
        Joshua Tree National Park
        
        California
      
      
      About Turtle Rock
      Descriptive text goes here
    
    """
  end
end

Enter your solution below.

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"""
    
    """
  end
end

|> Server.SmartCells.RenderComponent.register()

import Server.Livebook, only: []
import Kernel
:ok