paint-brush
How to Use Swift for Web Developmentby@imike
17,068 reads
17,068 reads

How to Use Swift for Web Development

by Mikhail IsaevMarch 20th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

SwifWeb is a framework that gives you the ability to write websites using SwiftUI. It wraps the entire HTML and CSS standards, as well as all of the web APIs. In this article, I will show you how to start building a website using the SwifWeb framework.

People Mentioned

Mention Thumbnail
featured image - How to Use Swift for Web Development
Mikhail Isaev HackerNoon profile picture

The web development world is vast, and it's easy to feel lost in the constant stream of new technologies emerging every day. Most of these new technologies are built using JavaScript or TypeScript. However, in this article, I will introduce you to web development using native Swift, directly inside your browser, and I'm confident it will impress you.


How is it possible?

For Swift to work natively on the web page it needs to be compiled to WebAssembly byte-code first and then JavaScript could load that code onto the page. The whole process of compilation is a bit tricky since we need to use the special toolchain and build helper files, that’s why there are helper CLI-tools available: Carton and Webber.

Is it ok to use a third-party toolchain?

The SwiftWasm community has done a tremendous amount of work to make it possible to compile Swift into WebAssembly by patching the original Swift toolchain. They update the toolchain by automatically pulling changes from the original toolchain every day and fixing their fork if tests fail. Their goal is to become a part of the official toolchain and they hope that it will happen in the near future.

Carton or Webber?

Carton is made by the SwiftWasm community and can be used for Tokamak projects, which is a framework that gives you the ability to write websites using SwiftUI.


Webber is made for SwifWeb projects. SwifWeb is different in that it wraps the entire HTML and CSS standards, as well as all of the web APIs.


Although you may prefer to write web apps using SwiftUI for code consistency, I believe this is the wrong approach because web development is inherently different and cannot be approached the same way as SwiftUI.


That's why I created SwifWeb, which gives you the ability to use all the power of HTML, CSS, and web APIs directly from Swift, using its beautiful syntax with auto-completion and documentation. And I created the Webber tool because Carton is not able to compile, debug and deploy SwifWeb apps the right way because it wasn’t created for it.


My name is Mikhail Isaev, and I am the author of SwifWeb. In this article, I will show you how to start building a website using the SwifWeb.

Required tools


Swift

You need to have Swift installed, the easiest way to have it:

  • on macOS is to install Xcode
  • on Linux or Windows(WSL2) is to use script from swiftlang.xyz


In other cases take a look at installation instructions on the official website

Webber CLI

I created Webber to help you to build, debug and deploy your apps.


On macOS it is easy to install with HomeBrew (install it from their website)

brew install swifweb/tap/webber

to update to the latest version later just run

brew upgrade webber


On Ubuntu or Windows(Ubuntu in WSL2) clone and compile Webber manually

sudo apt-get install binaryen
curl https://get.wasmer.io -sSfL | sh
apt-get install npm
cd /opt
sudo git clone https://github.com/swifweb/webber
cd webber
sudo swift build -c release
sudo ln -s /opt/webber/.build/release/Webber /usr/local/bin/webber

to update to the last version later run

cd /opt/webber
sudo git pull
sudo swift build -c release

main branch always contains stable code so feel free to pull updates from it

Creating new project

Open the terminal and execute

webber new

In the interactive menu choose pwa or spa, and enter the project name.


Change the directory to the newly created project and execute webber serve.

This command will compile your project into WebAssembly, package all necessary files inside a special .webber folder, and start serving your project on all interfaces using port 8888 by default.


Additional arguments for webber serve

  • App type

    -t pwa for Progressive Web App

    -t spa for Single Web App

  • Name of service worker target (usually named Service in PWA project)

    -s Service

  • Name of app target (App by default)

    -a App

  • Print more info in console

    -v

  • Port for the Webber server (default is 8888)

    -p 8080

Use -p 443 to test like real SSL (with allowed self-signed SSL setting)

  • Destination browser name to automatically launch in

    --browser safari or --browser chrome

  • Additional instance of browser with allowed self-signed SSL setting to debug service-workers

    --browser-self-signed

  • Additional instance of browser in incognito mode

    --browser-incognito

Application

The app begins in Sources/App/App.swift

import Web

@main
class App: WebApp {
    @AppBuilder
    override var app: Configuration {
        Lifecycle.didFinishLaunching { app in
            app.registerServiceWorker("service")
        }
        Routes {
            Page { IndexPage() }
            Page("login") { LoginPage() }
            Page("article/:id") { ArticlePage() }
            Page("**") { NotFoundPage() }
        }
        MainStyle()
    }
}

Lifecycle

It works in an iOS-like manner:

didFinishLaunching when the app just started

willTerminate when the app is going to die

willResignActive when the window is going to be inactive

didBecomeActive when the window is active

didEnterBackground when the window is going into the background

willEnterForeground when the window is going into the foreground


The most useful method here is didFinishLaunching because it is a great place to configure the app. You see it feels really like an iOS app! 😀


Here app contains useful convenience methods:

registerServiceWorker(“serviceName“) call to register PWA service worker

addScript(“path/to/script.js“) call to add relative or external script

addStylesheet(“path/to/style.css“) call to add relative or external style

addFont(“path/to/font.woff”, type:) call to add relative or external font, optionally set type

addIcon(“path/to/icon“, type:color:) call to add icon, optionally set type and color


Also, it is the place to configure additional libraries like Autolayout, Bootstrap, Materialize, etc.

Routes

Routing is needed to show the appropriate page based on the current URL.


To understand how to use routing you have to understand what URL is

https://website.com/hello/world - here /hello/world is the path

As you have seen, in the beginning, in the App class we should declare all the top-level routes.

Top-level means that pages declared in these routes will take the whole space in the window.


Ok, so for example the root route can be set three ways

Page("/") { IndexPage() }
Page("") { IndexPage() }
Page { IndexPage() }

I think the last one is the most beautiful 🙂


Login or registration routes can be set like this

Page("login") { LoginPage() }
Page("registration") { RegistrationPage() }


Parameter-related routes

Page("article/:id") { ArticlePage() }

The :id in the example above is a dynamic part of the route. We can retrieve this identifier in the ArticlePage class to display the article associated with it.

class ArticlePage: PageController {
    override func didLoad(with req: PageRequest) {
        if let articleId = req.parameters.get("id") {
            // Retrieve article here
        }
    }
}

You can have more than one parameter in the path. Retrieve all of them in the same way.

Queries

The next interesting thing in the path is the query, which is also very easy to use. For example, let's consider the /search route, which expects to have the search text and age query parameters.

https://website.com/search**?text=Alex&age=19** - the last part is the query


Simply declare the search route

Page("search") { SearchPage() }

And retrieve query data in the SearchPage class like this

class SearchPage: PageController {
    struct Query: Decodable {
        let text: String?
        let age: Int?
    }

    override func didLoad(with req: PageRequest) {
        do {
            let query = try req.query.decode(Query.self)
            // use optional query.text and query.age
            // to query search results
        } catch {
            print("Can't decode query: \(error)")
        }
    }
}

Anything

You also can use * to declare route which accept anything in the specific path part like this

Page("foo", "*", "bar") { SearchPage() }

The route above will accept anything between foo and bar, e.g. /foo/aaa/bar, /foo/bbb/bar, etc.

Catch-all

With ** sign you may set a special catch-all route that will handle anything that hasn’t been matched to other routes at a specific path.


Use it to either make global 404 route

Page("**") { NotFoundPage() }

or for specific path e.g. when user not found

Page("user", "**") { UserNotFoundPage() }


Let’s clarify situations with routes declared above

/user/1 - if there is a route for /user/:id then it will return UserPage. Otherwise, it will fall into…


UserNotFoundPage

/user/1/hello - if there are route for /user/:id/hello then it will fall into UserNotFoundPage

/something - if there are no route for /something then it will fall into NotFoundPage

Nested routing

We may not want to replace the entire content on the page for the next route, but only certain blocks. This is where the FragmentRouter comes in handy!


Let's consider that we have tabs on the /user page. Each tab is a subroute, and we want to react to changes in the subroute using the FragmentRouter.


Declare the top-level route in the App class

Page("user") { UserPage() }

And declare FragmentRouter in the UserPage class

class UserPage: PageController {
    @DOM override var body: DOM.Content {
        // NavBar is from Materialize library :)
        Navbar()
            .item("Profile") { self.changePath(to: "/user/profile") }
            .item("Friends") { self.changePath(to: "/user/friends") }
        FragmentRouter(self)
            .routes {
                Page("profile") { UserProfilePage() }
                Page("friends") { UserFriendsPage() }
            }
    }
}


In the example above, FragmentRouter handles /user/profile and /user/friends subroutes and renders it under the Navbar, so the page never reloads the whole content but only specific fragments.


There are also may be declared more than one fragment with the same or different subroutes and they all will just work together like magic!


Btw FragmentRouter is a Div and you may configure it by calling

FragmentRouter(self)
    .configure { div in
        // do anything you want with the div
    }

Stylesheets

You can use traditional CSS files, but you also have the new, magical ability to use a stylesheet written in Swift!

Basics

To declare a CSS rule using Swift we have Rule object.


It can be built declaratively by calling its methods

Rule(...selector...)
    .alignContent(.baseline)
    .color(.red) // or rgba/hex color
    .margin(v: 0, h: .auto)

or SwiftUI-like way using @resultBuilder

Rule(...selector...) {
    AlignContent(.baseline)
    Color(.red)
    Margin(v: 0, h: .auto)
}


Both ways are equal, however, I prefer the first one because of the autocompletion right after I type . 😀

All CSS methods described on MDN are available.

More than that it handles browser prefixes automatically!

However, you can set custom property this way in some specific case

Rule(...selector...)
  .custom("customKey", "customValue")

Selector

To set which elements Rule should affect we have to set a selector. I see the selector as the query in the database, but parts of that selector query I call pointers.


The easiest way to build a pointer is to initialize it using the raw string

Pointer("a")


But the right swifty way is to build it by calling .pointer at needed HTML tag like this

H1.pointer // h1
A.pointer // a
Pointer.any // *
Class("myClass").pointer // .myClass
Id("myId").pointer // #myId

It is about basic pointers, but they also have modifiers like :hover :first :first-child etc.

H1.pointer.first // h1:first
H1.pointer.firstChild // h1:first-child
H1.pointer.hover // h1:hover

You can declare any existing modifier, they are all available.

If something is missing don’t hesitate to make an extension to add it!

And don’t forget to send pull request on github to add it for everyone.

You also can concatenate pointers

H1.class(.myClass) // h1.myClass
H1.id(.myId) // h1#myId
H1.id(.myId).disabled // h1#myId:disabled
Div.pointer.inside(P.pointer) // div p
Div.pointer.parent(P.pointer) // div > p
Div.pointer.immediatedlyAfter(P.pointer) // Div + p
P.pointer.precededBy(Ul.pointer) // p ~ ul


How to use the selector in the Rule

Rule(Pointer("a"))
// or
Rule(A.pointer)

How to use more than one selector in the Rule

Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))

It produces the following CSS code

a, h1#myId, div > p {

}

Reactivity

Let’s declare dark and light styles for our app, and later, we will be able to easily switch between them.

import Web

@main
class App: WebApp {
    enum Theme {
        case light, dark
    }

    @State var theme: Theme = .light

    @AppBuilder
    override var app: Configuration {
        // ... Lifecycle, Routes ...
        LightStyle().disabled($theme.map { $0 != .happy })
        DarkStyle().disabled($theme.map { $0 != .sad })
    }
}


LightStyle and DarkStyle may be declared in separate files or e.g. in the App.swift


class LightStyle: Stylesheet {
    @Rules
    override var rules: Rules.Content {
        Rule(Body.pointer).backgroundColor(.white)
        Rule(H1.pointer).color(.black)
    }
}
class DarkStyle: Stylesheet {
    @Rules
    override var rules: Rules.Content {
        Rule(Body.pointer).backgroundColor(.black)
        Rule(H1.pointer).color(.white)
    }
}


And then somewhere in the UI of some page just call

App.current.theme = .light // to switch to light theme
// or
App.current.theme = .dark // to switch to dark theme

And it will activate or deactivate the related stylesheets! Isn't that cool? 😎


But you may say that describing styles in Swift instead of CSS is harder, so what’s the point?


The main point is reactivity! We can use @State with CSS properties and change values on the fly!


Just take a look, we can create a class with some reactive property and change it anytime in runtime, so any element on the screen which uses that class will be updated! It is much more effective than switching classes for many elements!


import Web

@main
class App: WebApp {
    @State var reactiveColor = Color.cyan

    @AppBuilder
    override var app: Configuration {
        // ... Lifecycle, Routes ...
        MainStyle()
    }
}

extension Class {
    static var somethingCool: Class { "somethingCool" }
}

class MainStyle: Stylesheet {
    @Rules
    override var rules: Rules.Content {
        // for all elements with `somethingCool` class
        Rule(Class.hello.pointer)
            .color(App.current.$reactiveColor)
        // for H1 and H2 elements with `somethingCool` class
        Rule(H1.class(.hello), H2.class(.hello))
            .color(App.current.$reactiveColor)
    }
}


Later from any place in the code just call

App.current.reactiveColor = .yellow // or any color you want

and it will update the color in the Stylesheet and in all the elements that uses it 😜


Also, it is possible to add raw CSS into a Stylesheet

class MainStyle: Stylesheet {
    @Rules
    override var rules: Rules.Content {
        // for all elements with `somethingCool` class
        Rule(Class.hello.pointer)
            .color(App.current.$reactiveColor)
        // for H1 and H2 elements with `somethingCool` class
        Rule(H1.class(.hello), H2.class(.hello))
            .color(App.current.$reactiveColor)
        """
        /* Raw CSS goes here */
        body {
          margin: 0;
          padding: 0;
        }
        """
    }
}

you can mix-in raw CSS strings as many times as needed

Pages

Router is rendering pages on each route. Page is any class inherited from the PageController.


PageController has lifecycle methods like willLoad didLoad willUnload didUnload, UI methods buildUI and body, and property wrapper variable for HTML elements.

Technically PageController is just a Div and you may set any its properties in buildUI method.


class IndexPage: PageController {
    // MARK: - Lifecycle

    override func willLoad(with req: PageRequest) {
        super.willLoad(with: req)
    }

    override func didLoad(with req: PageRequest) {
        super.didLoad(with: req)
        // set page title and metaDescription
        self.title = "My Index Page"
        self.metaDescription = "..."
        // also parse query and hash here
    }
    
    override func willUnload() {
        super.willUnload()
    }
    
    override func didUnload() {
        super.didUnload()
    }

    // MARK: - UI

    override func buildUI() {
        super.buildUI()
        // access any properties of the page's div here
        // e.g.
        self.backgroundcolor(.lightGrey)
        // optionally call body method here to add child HTML elements
        body {
            P("Hello world")
        }
        // or alternatively
        self.appendChild(P("Hello world"))
    }

    // the best place to declare any child HTML elements
    @DOM override var body: DOM.Content {
        H1("Hello world")
        P("Text under title")
        Button("Click me") {
            self.alert("Click!")
            print("button clicked")
        }
    }
}


If your page is tiny little you may declare it even this short way

PageController { page in
    H1("Hello world")
    P("Text under title")
    Button("Click me") {
        page.alert("Click!")
        print("button clicked")
    }
}
.backgroundcolor(.lightGrey)
.onWillLoad { page in }
.onDidLoad { page in }
.onWillUnload { page in }
.onDidUnload { page in }

Isn’t it beautiful and laconic? 🥲


Bonus convenience methods

alert(message: String) - direct JS alert method

changePath(to: String) - switching URL path

HTML Elements

Finally, I will tell you how(!) to build and use HTML elements!


All HTML elements with their attributes are available in Swift, the full list is e.g. on MDN.


Just an example short list of HTML elements:

SwifWeb code

HTML Code

Div()

<div></div>

H1(“text“)

<h1>text</h1>

A(“Click me“).href(““).target(.blank)

<a href=”” target=”_blank”>Click me</a>

Button(“Click“).onClick { print(“click“) }

<button onclick=”…”>Click</button>

InputText($text).placeholder("Title")

<input type=”text” placeholder=”title”>

InputCheckbox($checked)

<input type=”checkbox”>


As you can see, it is very easy to access any HTML tag in Swift because they are all represented under the same name, except for inputs. This is because different input types have different methods, and I didn't want to mix them.


Simple Div

Div()

we can access all its attributes and style properties like this

Div().class(.myDivs) // <div class="myDivs">
     .id(.myDiv) // <div id="myDiv">
     .backgroundColor(.green) // <div style="background-color: green;">
     .onClick { // adds `onClick` listener directly to the DOM element
         print("Clicked on div")
     }
     .attribute("key", "value") // <div key="value">
     .attribute("boolKey", true, .trueFalse) // <div boolKey="true">
     .attribute("boolKey", true, .yesNo) // <div boolKey="yes">
     .attribute("checked", true, .keyAsValue) // <div checked="checked">
     .attribute("muted", true, .keyWithoutValue) // <div muted>
     .custom("border", "2px solid red") // <div style="border: 2px solid red;">

Subclassing

Subclass HTML element to predefine style for it, or to make a composite element with a lot of predefined child elements and some convenient methods available outside, or to achieve lifecycle events like didAddToDOM and didRemoveFromDOM.

Let’s create a Divider element which is just a Div but with predefined .divider class

public class Divider: Div {
    // it is very important to override the name
    // because otherwise it will be <divider> in HTML
    open class override var name: String { "\(Div.self)".lowercased() }
    
    required public init() {
        super.init()
    }
    
    // this method executes immediately after any init method
    public override func postInit() {
        super.postInit()
        // here we are adding `divider` class
        self.class(.divider)
    }
}

It is very important to call super methods when subclassing.

Without it you may experience unexpected behaviour.

Appending to DOM

The Element can be appended to the DOM of PageController or HTML element right away or later.


Right away

Div {
    H1("Title")
    P("Subtitle")
    Div {
        Ul {
            Li("One")
            Li("Two")
        }
    }
}


Or later using lazy var

lazy var myDiv1 = Div()
lazy var myDiv2 = Div()

Div {
    myDiv1
    myDiv2
}

So you can declare an HTML element in advance and add it to the DOM any time later!

Removing from DOM

lazy var myDiv = Div()

Div {
    myDiv
}

// somewhere later
myDiv.remove()

Access parent element

Any HTML element has an optional superview property which gives access to its parent if it is added to the DOM

Div().superview?.backgroundColor(.red)

if/else conditions

We often need to show elements only in certain conditions so let’s use if/else for that

lazy var myDiv1 = Div()
lazy var myDiv2 = Div()
lazy var myDiv3 = Div()
var myDiv4: Div?

var showDiv2 = true

Div {
    myDiv1
    if showDiv2 {
        myDiv2
    } else {
        myDiv3
    }
    if let myDiv4 = myDiv4 {
        myDiv4
    } else {
        P("Div 4 was nil")
    }
}

But it is not reactive. If you try to set showDiv2 to false nothing happens.


Reactive example

lazy var myDiv1 = Div()
lazy var myDiv2 = Div()
lazy var myDiv3 = Div()

@State var showDiv2 = true

Div {
    myDiv1
    myDiv2.hidden($showDiv2.map { !$0 }) // shows myDiv2 if showDiv2 == true
    myDiv3.hidden($showDiv2.map { $0 }) // shows myDiv3 if showDiv2 == false
}


Why should we use $showDiv2.map {…} ?

In sort: because it is not SwiftUI. At all.


Read more about @State below.


Raw HTML

You also may need to add raw HTML into the page or HTML element and it is easily possible

Div {
    """
    <a href="https://google.com">Go to Google</a>
    """
}


ForEach

Static example

let names = ["Bob", "John", "Annie"]

Div {
    ForEach(names) { name in
        Div(name)
    }

    // or

    ForEach(names) { index, name in
        Div("\(index). \(name)")
    }

    // or with range

    ForEach(1...20) { index in
        Div()
    }

    // and even like this

    20.times {
        Div().class(.shootingStar)
    }
}

Dynamic example

@State var names = ["Bob", "John", "Annie"]

Div {
    ForEach($names) { name in
        Div(name)
    }

    // or with index

    ForEach($names) { index, name in
        Div("\(index). \(name)")
    }
}

Button("Change 1").onClick {
    // this will append new Div with name automatically
    self.names.append("George")
}

Button("Change 2").onClick {
    // this will replace and update Divs with names automatically
    self.names = ["Bob", "Peppa", "George"]
}

CSS

Same as in examples above, but also BuilderFunction  is available

Stylesheet {
    ForEach(1...20) { index in
        CSSRule(Div.pointer.nthChild("\(index)"))
            // set rule properties depending on index
    }
    20.times { index in
        CSSRule(Div.pointer.nthChild("\(index)"))
            // set rule properties depending on index
    }
}


You can use BuilderFunction in ForEach loops to calculate some value one time only like a delay value in the following example

ForEach(1...20) { index in
    BuilderFunction(9999.asRandomMax()) { delay in
        CSSRule(Pointer(".shooting_star").nthChild("\(index)"))
            .custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))")
            .custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))")
            .animationDelay(delay.ms)
        CSSRule(Pointer(".shooting_star").nthChild("\(index)").before)
            .animationDelay(delay.ms)
        CSSRule(Pointer(".shooting_star").nthChild("\(index)").after)
            .animationDelay(delay.ms)
    }
}


It can also take function as an argument

BuilderFunction(calculate) { calculatedValue in
    // CSS rule or DOM element
}

func calculate() -> Int {
    return 1 + 1
}

BuilderFunction is available for HTML elements too :)

Reactivity with @State

@State is the most desirable thing nowadays for declarative programming.


As I told you above: it is not SwiftUI, so there is no global state machine that tracks and redraws everything. And HTML elements are not temporary structs but classes, so they are real objects and you have direct access to them. It is much better and flexible, you have all the control.

What is it under the hood?

It is a property wrapper which notifies all the subscribers about its changes.

How to subscribe to changes?

enum Countries {
  case usa, australia, mexico
}

@State var selectedCounty: Countries = .usa

$selectedCounty.listen {
    print("country changed")
}
$selectedCounty.listen { newValue in
    print("country changed to \(newValue)")
}
$selectedCounty.listen { oldValue, newValue in
    print("country changed from \(oldValue) to \(newValue)")
}

How HTML elements can react to changes?

Simple text example

@State var text = "Hello world!"

H1($text) // whenever text changes it updates inner-text in H1
InputText($text) // while user is typing text it updates $text which updates H1

Simple number example

@State var height = 20.px

Div().height($height) // whenever height var changes it updates height of the Div

Simple boolean example

@State var hidden = false

Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div

Mapping example

@State var isItCold = true

H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })

Mapping two states

@State var one = true
@State var two = true

Div().display($one.and($two).map { one, two in
    // returns .block if both one and two are true
    one && two ? .block : .none
})

Mapping more than two states

@State var one = true
@State var two = true
@State var three = 15

Div().display($one.and($two).map { one, two in
    // returns true if both one and two are true
    one && two
}.and($three).map { oneTwo, three in // here oneTwo is a result of the previous mapping
    // returns .block if oneTwo is true and three is 15
    oneTwo && three == 15 ? .block : .none
})

All HTML and CSS properties can handle @State values

Extensions

Extend HTML elements

You can add some convenient methods to concrete elements like Div

extension Div {
    func makeItBeautiful() {}
}

Or groups of elements if you know their parent class.


There are few parent classes.

BaseActiveStringElement - is for elements which can be initialized with string, like a, h1, etc.

BaseContentElement - is for all elements that can have content inside of it, like div, ul, etc.

BaseElement - is for all elements


So extension for all elements can be written this way

extension BaseElement {
    func doSomething() {}
}

Declare colors

Color class is responsible for colors. It has predefined HTML-colors, but you can have your own

extension Color {
    var myColor1: Color { .hex(0xf1f1f1) } // which is 0xF1F1F1
    var myColor2: Color { .hsl(60, 60, 60) } // which is hsl(60, 60, 60)
    var myColor3: Color { .hsla(60, 60, 60, 0.8) } // which is hsla(60, 60, 60, 0.8)
    var myColor4: Color { .rgb(60, 60, 60) } // which is rgb(60, 60, 60)
    var myColor5: Color { .rgba(60, 60, 60, 0.8) } // which is rgba(60, 60, 60, 0.8)
}

Then use it like H1(“Text“).color(.myColor1)

Declare classes

extension Class {
    var my: Class { "my" }
}

Then use it like Div().class(.my)

Declare ids

extension Id {
    var myId: Id { "my" }
}

Then use it like Div().id(.my)

Web APIs

Window

window object is fully wrapped and accessible via App.current.window variable.

Full reference is available on MDN.


Let’s do the short overview down below

Foreground Flag

You can listen for it in Lifecycle in the App.swift or directly this way

App.current.window.$isInForeground.listen { isInForeground in
    // foreground flag changed
}

or just read it anytime anywhere

if App.current.window.isInForeground {
    // do somethign
}

or react on it with HTML element

Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })

Active Flag

It is the same as Foreground flag, but accessible via App.current.window.isActive

It detects if a user is still interacting inside the window.

Online status

Same as the Foreground flag, but accessible via App.current.window.isOnline

It detects if a user still has access to the internet.

Darkmode status

Same as the Foreground flag, but accessible via App.current.window.isDark

It detects if a user’s browser or operating system is in dark mode.

Inner Size

The size of the window's content area (viewport) including scrollbars

App.current.window.innerSize is Size object within width and height values inside.

Also available as @State variable.

Outer Size

The size of the browser window, including toolbars/scrollbars.

App.current.window.outerSize is Size object within width and height values inside.

Also available as @State variable.

Screen

Special object for inspecting properties of the screen on which the current window is being rendered. Available via App.current.window.screen.

The most interesting property is usually pixelRatio.

History

Contains the URLs visited by the user (within a browser window).

Available via App.current.window.history or just History.shared.

It is accessible as @State variable, so you can listen for its changes if needed.

App.current.window.$history.listen { history in
    // read history properties
}

It is also accessible as simple variable

History.shared.length // size of the history stack
History.shared.back() // to go back in history stack
History.shared.forward() // to go forward in history stack
History.shared.go(offset:) // going to specific index in history stack

More details are available on MDN.

Location

Contains information about the current URL.

Available via App.current.window.location or just Location.shared.

It is accessible as @State variable, so you can listen for its changes if needed.

This is how router works for example.

App.current.window.$location.listen { location in
    // read location properties
}


It is also accessible as a simple variable

Location.shared.href // also $href
Location.shared.host // also $host
Location.shared.port // also $port
Location.shared.pathname // also $pathname
Location.shared.search // also $search
Location.shared.hash // also $hash

More details are available on MDN.

Navigator

Contains information about the browser.

Available via App.current.window.navigator or just Navigator.shared

The most interesting properties usually userAgent platform language cookieEnabled.

LocalStorage

Allows to save key/value pairs in a web browser. Stores data with no expiration date.

Available as App.current.window.localStorage or just LocalStorage.shared.

// You can save any value that can be represented in JavaScript
LocalStorage.shared.set("key", "value") // saves String
LocalStorage.shared.set("key", 123) // saves Int
LocalStorage.shared.set("key", 0.8) // saves Double
LocalStorage.shared.set("key", ["key":"value"]) // saves Dictionary
LocalStorage.shared.set("key", ["v1", "v2"]) // saves Array

// Getting values back
LocalStorage.shared.string(forKey: "key") // returns String?
LocalStorage.shared.integer(forKey: "key") // returns Int?
LocalStorage.shared.string(forKey: "key") // returns String?
LocalStorage.shared.value(forKey: "key") // returns JSValue?

// Removing item
LocalStorage.shared.removeItem(forKey: "key")

// Removing all items
LocalStorage.shared.clear()

Tracking changes

LocalStorage.onChange { key, oldValue, newValue in
    print("LocalStorage: key \(key) has been updated")
}

Tracking all items removal

LocalStorage.onClear { print("LocalStorage: all items has been removed") }

SessionStorage

Allows to save key/value pairs in a web browser. Stores data for only one session.

Available as App.current.window.sessionStorage or just SessionStorage.shared.

API is absolutely same as in LocalStorage described above.

Document

Represents any web page loaded in the browser and serves as an entry point into the web page's content.

Available via App.current.window.document.

App.current.window.document.title // also $title
App.current.window.document.metaDescription // also $metaDescription
App.current.window.document.head // <head> element
App.current.window.document.body // <body> element
App.current.window.documentquerySelector("#my") // returns BaseElement?
App.current.window.document.querySelectorAll(".my") // returns [BaseElement]

Localization

Static localization

Classic localization is automatic and depends on the user system language

How to use

H1(String(
    .en("Hello"),
    .fr("Bonjour"),
    .ru("Привет"),
    .es("Hola"),
    .zh_Hans("你好"),
    .ja("こんにちは")))

Dynamic localization

If you want to change localized strings on the screen on-the-fly (without page reloading)

You could change the current language by calling

Localization.current = .es

If you saved the user's language somewhere in cookies or localstorage then you have to set it on the app launch

Lifecycle.didFinishLaunching {
    Localization.current = .es
}

How to use

H1(LString(
    .en("Hello"),
    .fr("Bonjour"),
    .ru("Привет"),
    .es("Hola"),
    .zh_Hans("你好"),
    .ja("こんにちは")))

Advanced example

H1(Localization.currentState.map { "Curent language: \($0.rawValue)" })
H2(LString(.en("English string"), .es("Hilo Español")))
Button("change lang").onClick {
	Localization.current = Localization.current.rawValue.contains("en") ? .es : .en
}

Fetch

import FetchAPI

Fetch("https://jsonplaceholder.typicode.com/todos/1") {
    switch $0 {
    case .failure:
        break
    case .success(let response):
        print("response.code: \(response.status)")
        print("response.statusText: \(response.statusText)")
        print("response.ok: \(response.ok)")
        print("response.redirected: \(response.redirected)")
        print("response.headers: \(response.headers.dictionary)")
        struct Todo: Decodable {
            let id, userId: Int
            let title: String
            let completed: Bool
        }
        response.json(as: Todo.self) {
            switch $0 {
            case .failure(let error):
                break
            case .success(let todo):
                print("decoded todo: \(todo)")
            }
        }
    }
}

XMLHttpRequest

import XMLHttpRequest

XMLHttpRequest()
    .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1")
    .onAbort {
        print("XHR onAbort")
    }.onLoad {
        print("XHR onLoad")
    }.onError {
        print("XHR onError")
    }.onTimeout {
        print("XHR onTimeout")
    }.onProgress{ progress in
        print("XHR onProgress")
    }.onLoadEnd {
        print("XHR onLoadEnd")
    }.onLoadStart {
        print("XHR onLoadStart")
    }.onReadyStateChange { readyState in
        print("XHR onReadyStateChange")
    }
    .send()

WebSocket

import WebSocket

let webSocket = WebSocket("wss://echo.websocket.org").onOpen {
    print("ws connected")
}.onClose { (closeEvent: CloseEvent) in
    print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)")
}.onError {
    print("ws error")
}.onMessage { message in
    print("ws message: \(message)")
    switch message.data {
    case .arrayBuffer(let arrayBuffer): break
    case .blob(let blob): break
    case .text(let text): break
    case .unknown(let jsValue): break
    }
}
Dispatch.asyncAfter(2) {
    // send as simple string
    webSocket.send("Hello from SwifWeb")
    // send as Blob
    webSocket.send(Blob("Hello from SwifWeb"))
}

Console

Simple print(“Hello world“) is equivalent of console.log(‘Hello world‘) in JavaScript


Console methods are also wrapped with love ❤️

Console.dir(...)
Console.error(...)
Console.warning(...)
Console.clear()

LivePreview

To make live preview work declare WebPreview class in each file you want.

class IndexPage: PageController {}

class Welcome_Preview: WebPreview {
    @Preview override class var content: Preview.Content {
        Language.en
        Title("Initial page")
        Size(640, 480)
        // add here as many elements as needed
        IndexPage()
    }
}


Xcode

Please read the instruction on repository page. It is tricky but fully working solution 😎


VSCode

Go to Extensions inside VSCode and search Webber.

Once it is installed press Cmd+Shift+P (or Ctrl+Shift+P on Linux/Windows)

Find and launch Webber Live Preview.

On the right side, you will see the live preview window and it refreshes whenever you save the file which contains WebPreview class.

Access to JavaScript

It is available through JavaScriptKit which is the foundation of the SwifWeb.

Read how to you is in the official repository.

Resources

You can add css, js, png, jpg, and any other static resources inside of the project.

But to have them available during debug or in final release files you have to declare them all in the Package.swift like this

.executableTarget(name: "App", dependencies: [
    .product(name: "Web", package: "web")
], resources: [
    .copy("css/*.css"),
    .copy("css"),
    .copy("images/*.jpg"),
    .copy("images/*.png"),
    .copy("images/*.svg"),
    .copy("images"),
    .copy("fonts/*.woff2"),
    .copy("fonts")
]),

Later you will be able to access them e.g. like this Img().src(“/images/logo.png“)

Debugging

Launch Webber the following way

webber serve just to quickly launch it

webber serve -t pwa -s Service to launch it in PWA mode

Additional parameters

-v or --verbose to show more info in console for debugging purposes

-p 443 or --port 443 to start webber server on 443 port instead of default 8888

--browser chrome/safari to automatically open desired browser, by default it doesn't open any

--browser-self-signed needed to debug service workers locally, otherwise they doesn't work

--browser-incognito to open additional instance of browser in incognito mode, works only with chrome


So to build your app in debug mode, automatically open it in Chrome and refresh browser automatically whenever you change any file launch it this way

for SPA

webber serve --browser chrome

for real PWA testing

webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito


Initial app loading

You may want to improve the initial loading process.

For that just open .webber/entrypoint/dev folder inside of the project and edit index.html file.

It contains initial HTML code with very useful listeners: WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError.

You are free to edit that code to whatever you want to implement your custom style 🔥

When you finish the new implementation don’t forget to save same into .webber/entrypoint/release folder

Building Release

Simply execute webber release or webber release -t pwa -s Service for PWA.

Then grab compiled files from the .webber/release folder and upload them to your server.

How to deploy

You can upload your files to any static hosting.

Hosting should provide the correct Content-Type for wasm files!

Yes, it is very important to have the correct header Content-Type: application/wasm for wasm files, otherwise unfortunately browser will not be able to load your WebAssembly application.

For example GithubPages doesn’t provide the correct Content-Type for wasm files so so unfortunately it is impossible to host WebAssembly sites on it.

Nginx

If you use your own server with nginx, then open /etc/nginx/mime.types and check if it contains application/wasm wasm; record. If yes then you are good to go!

Conclusion

I hope I amazed you today and you will at least give a try to SwifWeb and at max will start using it for your next big web project!


Please feel free to contribute to any of SwifWeb libraries and also to star ⭐️them all!


I have a great SwiftStream community in Discord where you can find huge support, read little tutorials and be notified first about any upcoming updates! Would be cool to see you with us!


It is just the very beginning, so stay tuned for more articles about SwifWeb!


Tell your friends!