View Source Stylesheets
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.
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.
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