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.
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.
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 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.
You need to have Swift installed, the easiest way to have it:
In other cases take a look at installation instructions on the official website
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
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
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()
}
}
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.
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.
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)")
}
}
}
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.
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
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
}
You can use traditional CSS files, but you also have the new, magical ability to use a stylesheet written in Swift!
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")
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 {
}
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
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
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;">
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.
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!
lazy var myDiv = Div()
Div {
myDiv
}
// somewhere later
myDiv.remove()
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)
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.
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>
"""
}
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)
}
}
@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"]
}
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 :)
@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.
It is a property wrapper which notifies all the subscribers about its 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)")
}
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
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() {}
}
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)
extension Class {
var my: Class { "my" }
}
Then use it like Div().class(.my)
extension Id {
var myId: Id { "my" }
}
Then use it like Div().id(.my)
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
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 })
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.
Same as the Foreground flag, but accessible via App.current.window.isOnline
It detects if a user still has access to the internet.
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.
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.
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.
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
.
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.
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.
Contains information about the browser.
Available via App.current.window.navigator
or just Navigator.shared
The most interesting properties usually userAgent
platform
language
cookieEnabled
.
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") }
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.
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]
Classic localization is automatic and depends on the user system language
H1(String(
.en("Hello"),
.fr("Bonjour"),
.ru("Привет"),
.es("Hola"),
.zh_Hans("你好"),
.ja("こんにちは")))
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
}
H1(LString(
.en("Hello"),
.fr("Bonjour"),
.ru("Привет"),
.es("Hola"),
.zh_Hans("你好"),
.ja("こんにちは")))
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
}
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)")
}
}
}
}
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()
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"))
}
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()
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()
}
}
Please read the instruction on repository page. It is tricky but fully working solution 😎
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.
It is available through JavaScriptKit which is the foundation of the SwifWeb.
Read how to you is in the official repository.
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“)
Launch Webber the following way
webber serve
just to quickly launch it
webber serve -t pwa -s Service
to launch it in PWA mode
-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
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
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.
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.
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!
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!