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