paint-brush
Como usar o Swift para desenvolvimento webpor@imike
17,057 leituras
17,057 leituras

Como usar o Swift para desenvolvimento web

por Mikhail Isaev33m2023/03/20
Read on Terminal Reader

Muito longo; Para ler

SwifWeb é uma estrutura que permite escrever sites usando SwiftUI. Ele envolve todos os padrões HTML e CSS, bem como todas as APIs da web. Neste artigo, mostrarei como começar a criar um site usando a estrutura SwifWeb.
featured image - Como usar o Swift para desenvolvimento web
Mikhail Isaev HackerNoon profile picture
0-item
1-item

O mundo do desenvolvimento web é vasto e é fácil se sentir perdido no fluxo constante de novas tecnologias que surgem todos os dias. A maioria dessas novas tecnologias é construída usando JavaScript ou TypeScript. No entanto, neste artigo, apresentarei a você o desenvolvimento da Web usando o Swift nativo, diretamente no seu navegador, e tenho certeza de que isso o impressionará.


Como isso é possível?

Para que o Swift funcione nativamente na página da Web, ele precisa ser compilado primeiro para o código de bytes do WebAssembly e, em seguida, o JavaScript pode carregar esse código na página. Todo o processo de compilação é um pouco complicado, pois precisamos usar a cadeia de ferramentas especial e criar arquivos auxiliares, é por isso que existem ferramentas CLI auxiliares disponíveis: Carton e Webber.

Posso usar uma cadeia de ferramentas de terceiros?

A comunidade SwiftWasm fez um tremendo trabalho para tornar possível compilar o Swift no WebAssembly corrigindo a cadeia de ferramentas Swift original. Eles atualizam a cadeia de ferramentas puxando automaticamente as alterações da cadeia de ferramentas original todos os dias e consertando sua bifurcação se os testes falharem. O objetivo deles é fazer parte da cadeia de ferramentas oficial e eles esperam que isso aconteça em um futuro próximo.

Caixa ou Webber?

Carton é feito pela comunidade SwiftWasm e pode ser usado para projetos Tokamak, que é uma estrutura que permite escrever sites usando SwiftUI.


Webber é feito para projetos SwifWeb. SwifWeb é diferente porque envolve todos os padrões HTML e CSS, bem como todas as APIs da web.


Embora você prefira escrever aplicativos da Web usando SwiftUI para consistência de código, acredito que essa seja a abordagem errada porque o desenvolvimento da Web é inerentemente diferente e não pode ser abordado da mesma forma que o SwiftUI.


É por isso que criei o SwifWeb, que oferece a capacidade de usar todo o poder do HTML, CSS e APIs da Web diretamente do Swift, usando sua bela sintaxe com preenchimento automático e documentação. E criei a ferramenta Webber porque o Carton não é capaz de compilar, depurar e implantar aplicativos SwifWeb da maneira certa porque não foi criado para isso.


Meu nome é Mikhail Isaev e sou o autor do SwifWeb. Neste artigo, mostrarei como começar a criar um site usando o SwifWeb.

ferramentas necessárias


Rápido

Você precisa ter o Swift instalado, a maneira mais fácil de tê-lo:

  • no macOS é instalar o Xcode
  • no Linux ou Windows (WSL2) é usar o script de swiftlang.xyz


Em outros casos, consulte as instruções de instalação no site oficial

Webber CLI

Criei Webber para ajudá-lo a construir, depurar e implantar seus aplicativos.


No macOS, é fácil instalar com o HomeBrew (instale-o no site deles)

 brew install swifweb/tap/webber

para atualizar para a versão mais recente mais tarde, basta executar

 brew upgrade webber


No Ubuntu ou Windows (Ubuntu no WSL2) clone e compile o Webber manualmente

 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

para atualizar para a última versão mais tarde execute

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

ramo principal sempre contém código estável, então fique à vontade para obter atualizações dele

Criando novo projeto

Abra o terminal e execute

 webber new

No menu interativo, escolha pwa ou spa e digite o nome do projeto.


Altere o diretório para o projeto recém-criado e execute webber serve .

Este comando irá compilar seu projeto no WebAssembly, empacotar todos os arquivos necessários dentro de uma pasta .webber especial e começar a servir seu projeto em todas as interfaces usando a porta 8888 por padrão.


Argumentos adicionais para webber serve

  • tipo de aplicativo

    -t pwa para Progressive Web App

    -t spa para aplicativo da Web único

  • Nome do destino do service worker (geralmente chamado de Service no projeto PWA)

    -s Service

  • Nome do destino do aplicativo ( App por padrão)

    -a App

  • Imprimir mais informações no console

    -v

  • Porta para o servidor Webber (o padrão é 8888 )

    -p 8080

Use -p 443 para testar como SSL real (com configuração SSL autoassinada permitida)

  • Nome do navegador de destino para iniciar automaticamente

    --browser safari ou --browser chrome

  • Instância adicional de navegador com configuração SSL autoassinada permitida para depurar service workers

    --browser-self-signed

  • Instância adicional do navegador no modo de navegação anônima

    --browser-incognito

Aplicativo

O aplicativo começa em 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() } }

Vida útil

Funciona de maneira semelhante ao iOS:

didFinishLaunching quando o aplicativo acabou de iniciar

willTerminate quando o aplicativo for morrer

willResignActive quando a janela vai ficar inativa

didBecomeActive quando a janela está ativa

didEnterBackground quando a janela vai para o fundo

willEnterForeground quando a janela está indo para o primeiro plano


O método mais útil aqui é didFinishLaunching porque é um ótimo lugar para configurar o aplicativo. Você vê que parece realmente um aplicativo iOS! 😀


Aqui app contém métodos úteis de conveniência:

registerServiceWorker(“serviceName“) chamada para registrar o service worker do PWA

chamada addScript(“path/to/script.js“) para adicionar script relativo ou externo

addStylesheet(“path/to/style.css“) chamada para adicionar estilo relativo ou externo

addFont(“path/to/font.woff”, type:) chamada para adicionar fonte relativa ou externa, opcionalmente defina o tipo

addIcon(“path/to/icon“, type:color:) chamada para adicionar ícone, opcionalmente defina o tipo e a cor


Além disso, é o local para configurar bibliotecas adicionais como Autolayout , Bootstrap , Materialize , etc.

Rotas

O roteamento é necessário para mostrar a página apropriada com base na URL atual.


Para entender como usar o roteamento, você precisa entender o que é URL

https://website.com/hello/world - aqui /hello/world é o caminho

Como você viu, no começo, na classe App devemos declarar todas as rotas de nível superior.

Nível superior significa que as páginas declaradas nessas rotas ocuparão todo o espaço da janela.


Ok, por exemplo, a rota raiz pode ser definida de três maneiras

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

Acho que o último é o mais bonito 🙂


As rotas de login ou registro podem ser definidas assim

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


Rotas relacionadas a parâmetros

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

O :id no exemplo acima é uma parte dinâmica da rota. Podemos recuperar esse identificador na classe ArticlePage para exibir o artigo associado a ele.

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

Você pode ter mais de um parâmetro no caminho. Recupere todos eles da mesma maneira.

Consultas

A próxima coisa interessante no caminho é a query , que também é muito fácil de usar. Por exemplo, vamos considerar a rota /search , que espera ter o text de pesquisa e os parâmetros de consulta age .

https://website.com/search**?text=Alex&age=19** - a última parte é a consulta


Simplesmente declare a rota de busca

 Page("search") { SearchPage() }

E recupere os dados da consulta na classe SearchPage assim

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

Qualquer coisa

Você também pode usar * para declarar a rota que aceita qualquer coisa na parte específica do caminho como esta

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

A rota acima aceitará qualquer coisa entre foo e bar, por exemplo /foo/aaa/bar, /foo/bbb/bar, etc.

pega-tudo

Com o sinal ** , você pode definir uma rota especial que irá lidar com qualquer coisa que não corresponda a outras rotas em um caminho específico.


Use-o para fazer a rota 404 global

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

ou para um caminho específico, por exemplo, quando o usuário não foi encontrado

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


Vamos esclarecer situações com rotas declaradas acima

/user/1 - se houver uma rota para /user/:id, ele retornará UserPage . Caso contrário, cairá em…


UserNotFoundPage

/user/1/hello - se houver rota para /user/:id/hello, ele cairá em UserNotFoundPage

/algo - se não houver rota para /algo, ele cairá em NotFoundPage

Roteamento aninhado

Podemos não querer substituir todo o conteúdo da página pela próxima rota, mas apenas alguns blocos. É aqui que o FragmentRouter é útil!


Vamos considerar que temos abas na página /user . Cada guia é uma subrota e queremos reagir às alterações na subrota usando o FragmentRouter .


Declare a rota de nível superior na classe App

 Page("user") { UserPage() }

E declare FragmentRouter na classe UserPage

 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() } } } }


No exemplo acima, FragmentRouter lida com as sub-rotas /user/profile e /user/friends e as renderiza na Navbar , de modo que a página nunca recarrega todo o conteúdo, mas apenas fragmentos específicos.


Também podem ser declarados mais de um fragmento com as mesmas ou diferentes sub-rotas e todos eles funcionarão juntos como mágica!


Btw FragmentRouter é um Div e você pode configurá-lo chamando

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

Folhas de estilo

Você pode usar arquivos CSS tradicionais, mas também tem a nova e mágica habilidade de usar uma folha de estilo escrita em Swift!

Fundamentos

Para declarar uma regra CSS usando Swift, temos o objeto Rule .


Ele pode ser construído declarativamente chamando seus métodos

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

ou estilo SwiftUI usando @resultBuilder

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


As duas formas são iguais, porém, prefiro a primeira por causa do preenchimento automático logo após digitar . 😀

Todos os métodos CSS descritos no MDN estão disponíveis.

Mais do que isso, ele lida com os prefixos do navegador automaticamente!

No entanto, você pode definir a propriedade personalizada dessa maneira em algum caso específico

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

Seletor

Para definir quais elementos a regra deve afetar, temos que definir um seletor. Eu vejo o seletor como a consulta no banco de dados, mas partes dessa consulta do seletor eu chamo de ponteiros.


A maneira mais fácil de construir um ponteiro é inicializá-lo usando a string bruta

 Pointer("a")


Mas a maneira mais rápida é construí-lo chamando .pointer na tag HTML necessária como esta

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

Trata-se de ponteiros básicos, mas eles também têm modificadores como :hover :first :first-child etc.

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

Você pode declarar qualquer modificador existente, eles estão todos disponíveis.

Se algo estiver faltando, não hesite em fazer uma extensão para adicioná-lo!

E não se esqueça de enviar pull request no github para adicioná-lo para todos.

Você também pode concatenar ponteiros

 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


Como usar o seletor na Regra

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

Como usar mais de um seletor na Regra

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

Ele produz o seguinte código CSS

 a, h1#myId, div > p { }

Reatividade

Vamos declarar os estilos escuro e claro para nosso aplicativo e, posteriormente, poderemos alternar facilmente entre eles.

 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 e DarkStyle podem ser declarados em arquivos separados ou, por exemplo, no 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) } }


E então, em algum lugar na interface do usuário de alguma página, basta ligar

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

E ativará ou desativará as folhas de estilo relacionadas! Isso não é legal? 😎


Mas você pode dizer que descrever estilos em Swift em vez de CSS é mais difícil, então qual é o objetivo?


O ponto principal é a reatividade! Podemos usar @State com propriedades CSS e alterar valores na hora!


Veja só, podemos criar uma classe com alguma propriedade reativa e alterá-la a qualquer momento em tempo de execução, assim qualquer elemento da tela que utilizar aquela classe será atualizado! É muito mais eficaz do que mudar de classe para muitos elementos!


 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) } }


Mais tarde, de qualquer lugar no código, basta ligar

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

e atualizará a cor na folha de estilo e em todos os elementos que a usam 😜


Além disso, é possível adicionar CSS bruto em uma folha de estilo

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

você pode misturar strings CSS brutas quantas vezes forem necessárias

Páginas

O roteador está renderizando páginas em cada rota. Page é qualquer classe herdada do PageController .


PageController tem métodos de ciclo de vida como willLoad didLoad willUnload didUnload , métodos de interface do usuário buildUI e body e variável wrapper de propriedade para elementos HTML.

Tecnicamente, PageController é apenas um Div e você pode definir quaisquer propriedades no método buildUI .


 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 // eg 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") } } }


Se sua página for minúscula, você pode declará-la mesmo desta forma

 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 }

Não é lindo e lacônico? 🥲


Métodos de conveniência de bônus

alert(message: String) - método alert JS direto

changePath(to: String) - troca de caminho de URL

Elementos HTML

Por fim, direi como (!) Construir e usar elementos HTML!


Todos os elementos HTML com seus atributos estão disponíveis no Swift, a lista completa está, por exemplo, no MDN .


Apenas um pequeno exemplo de lista de elementos HTML:

código swifweb

Código HTML

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


Como você pode ver, é muito fácil acessar qualquer tag HTML no Swift porque todas elas são representadas com o mesmo nome, exceto as entradas. Isso ocorre porque diferentes tipos de entrada têm métodos diferentes e eu não queria misturá-los.


Div Simples

 Div()

podemos acessar todos os seus atributos e propriedades de estilo assim

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

Subclasse

Elemento HTML de subclasse para predefinir o estilo para ele ou para criar um elemento composto com muitos elementos filho predefinidos e alguns métodos convenientes disponíveis fora ou para obter eventos de ciclo de vida como didAddToDOM e didRemoveFromDOM .

Vamos criar um elemento Divider que é apenas um Div , mas com a classe .divider predefinida

 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) } }

É muito importante chamar supermétodos ao criar subclasses.

Sem ele, você pode experimentar um comportamento inesperado.

Anexando ao DOM

O elemento pode ser anexado ao DOM de PageController ou elemento HTML imediatamente ou posteriormente.


Agora mesmo

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


Ou mais tarde usando lazy var

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

Assim, você pode declarar um elemento HTML com antecedência e adicioná-lo ao DOM a qualquer momento!

Removendo do DOM

 lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()

Acessar elemento pai

Qualquer elemento HTML tem uma propriedade superview opcional que dá acesso ao seu pai se for adicionado ao DOM

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

condições if/else

Muitas vezes precisamos mostrar elementos apenas em certas condições, então vamos usar if/else para isso

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

Mas não é reativo. Se você tentar definir showDiv2 como false , nada acontecerá.


exemplo reativo

 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 }


Por que devemos usar $showDiv2.map {…} ?

Em ordem: porque não é SwiftUI. De forma alguma.


Leia mais sobre @State abaixo.


HTML bruto

Você também pode precisar adicionar HTML bruto na página ou elemento HTML e é facilmente possível

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


Para cada

Exemplo estático

 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) } }

Exemplo dinâmico

 @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

Igual aos exemplos acima, mas também BuilderFunction está disponível

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


Você pode usar BuilderFunction em loops ForEach para calcular algum valor apenas uma vez como um valor delay no exemplo a seguir

 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) } }


Também pode funcionar como um argumento

 BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }

BuilderFunction também está disponível para elementos HTML :)

Reatividade com @State

@State é a coisa mais desejável hoje em dia para programação declarativa.


Como eu disse acima: não é SwiftUI, então não há máquina de estado global que rastreie e redesenhe tudo. E os elementos HTML não são estruturas temporárias, mas classes, portanto, são objetos reais e você tem acesso direto a eles. É muito melhor e flexível, você tem todo o controle.

O que há sob o capô?

É um wrapper de propriedade que notifica todos os assinantes sobre suas alterações.

Como subscrever as alterações?

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

Como os elementos HTML podem reagir às mudanças?

Exemplo de texto simples

 @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

Exemplo de número simples

 @State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div

Exemplo booleano simples

 @State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div

Exemplo de mapeamento

 @State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })

Mapeando dois estados

 @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 })

Mapeando mais de dois estados

 @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 })

Todas as propriedades HTML e CSS podem lidar com valores @State

Extensões

Estender elementos HTML

Você pode adicionar alguns métodos convenientes para elementos concretos como Div

 extension Div { func makeItBeautiful() {} }

Ou grupos de elementos se você conhece sua class pai.


Existem poucas classes pai.

BaseActiveStringElement - é para elementos que podem ser inicializados com string, como a , h1 , etc.

BaseContentElement - é para todos os elementos que podem ter conteúdo dentro dele, como div , ul , etc.

BaseElement - é para todos os elementos


Portanto, a extensão para todos os elementos pode ser escrita dessa maneira

 extension BaseElement { func doSomething() {} }

Declarar cores

A classe Color é responsável pelas cores. Tem cores HTML pré-definidas, mas você pode ter as suas próprias

 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) }

Em seguida, use-o como H1(“Text“).color(.myColor1)

Declarar classes

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

Em seguida, use-o como Div().class(.my)

Declarar IDs

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

Em seguida, use-o como Div().id(.my)

APIs da Web

Janela

o objeto window é totalmente encapsulado e acessível por meio da variável App.current.window .

A referência completa está disponível no MDN .


Vamos fazer uma breve visão geral abaixo

Bandeira de primeiro plano

Você pode ouvi-lo em Lifecycle no App.swift ou diretamente desta forma

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

ou apenas leia a qualquer hora em qualquer lugar

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

ou reagir com elemento HTML

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

Sinalizador ativo

É o mesmo que o sinalizador Foreground, mas acessível via App.current.window.isActive

Ele detecta se um usuário ainda está interagindo dentro da janela.

Status online

Igual ao sinalizador Foreground, mas acessível via App.current.window.isOnline

Ele detecta se um usuário ainda tem acesso à internet.

status de modo escuro

Igual ao sinalizador Foreground, mas acessível via App.current.window.isDark

Ele detecta se o navegador ou sistema operacional de um usuário está no modo escuro.

Tamanho interno

O tamanho da área de conteúdo da janela (janela de visualização), incluindo barras de rolagem

App.current.window.innerSize é o objeto Size dentro dos valores de width e height dentro.

Também disponível como variável @State .

Tamanho Externo

O tamanho da janela do navegador, incluindo barras de ferramentas/barras de rolagem.

App.current.window.outerSize é o objeto Size dentro dos valores de width e height dentro.

Também disponível como variável @State .

Tela

Objeto especial para inspecionar as propriedades da tela na qual a janela atual está sendo renderizada. Disponível via App.current.window.screen .

A propriedade mais interessante geralmente é pixelRatio .

História

Contém as URLs visitadas pelo usuário (dentro de uma janela do navegador).

Disponível via App.current.window.history ou apenas History.shared .

É acessível como variável @State , para que você possa ouvir suas alterações, se necessário.

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

Também é acessível como variável simples

 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

Mais detalhes estão disponíveis no MDN .

Localização

Contém informações sobre o URL atual.

Disponível via App.current.window.location ou apenas Location.shared .

É acessível como variável @State , para que você possa ouvir suas alterações, se necessário.

É assim que o roteador funciona, por exemplo.

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


Também é acessível como uma variável simples

 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

Mais detalhes estão disponíveis no MDN .

Navegador

Contém informações sobre o navegador.

Disponível via App.current.window.navigator ou apenas Navigator.shared

As propriedades mais interessantes geralmente são language platform userAgent cookieEnabled .

LocalStorage

Permite salvar pares chave/valor em um navegador da web. Armazena dados sem data de validade.

Disponível como App.current.window.localStorage ou apenas 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()

Alterações de rastreamento

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

Acompanhando a remoção de todos os itens

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

SessionStorage

Permite salvar pares chave/valor em um navegador da web. Armazena dados para apenas uma sessão.

Disponível como App.current.window.sessionStorage ou apenas SessionStorage.shared .

API é absolutamente igual ao LocalStorage descrito acima.

Documento

Representa qualquer página da web carregada no navegador e serve como ponto de entrada para o conteúdo da página da web.

Disponível 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]

Localização

localização estática

A localização clássica é automática e depende do idioma do sistema do usuário

Como usar

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

localização dinâmica

Se você quiser alterar as strings localizadas na tela em tempo real (sem recarregar a página)

Você pode alterar o idioma atual chamando

 Localization.current = .es

Se você salvou o idioma do usuário em algum lugar nos cookies ou localstorage, deverá defini-lo na inicialização do aplicativo

 Lifecycle.didFinishLaunching { Localization.current = .es }

Como usar

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

Exemplo avançado

 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 }

Buscar

 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“) é equivalente a console.log('Hello world') em JavaScript


Métodos de console também são embalados com amor ❤️

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

Visualização ao vivo

Para fazer a visualização ao vivo funcionar, declare a classe WebPreview em cada arquivo desejado.

 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() } }


código X

Por favor, leia as instruções na página do repositório . É uma solução complicada, mas totalmente funcional 😎


VSCode

Vá para Extensions dentro do VSCode e pesquise Webber .

Depois de instalado, pressione Cmd+Shift+P (ou Ctrl+Shift+P no Linux/Windows)

Encontre e inicie Webber Live Preview .

No lado direito, você verá a janela de visualização ao vivo e ela será atualizada sempre que você salvar o arquivo que contém a classe WebPreview .

Acesso ao JavaScript

Está disponível através do JavaScriptKit , que é a base do SwifWeb .

Leia como você está no repositório oficial .

Recursos

Você pode adicionar css , js , png , jpg e quaisquer outros recursos estáticos dentro do projeto.

Mas para tê-los disponíveis durante a depuração ou nos arquivos de lançamento final, você deve declará-los todos no Package.swift assim

 .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") ]),

Mais tarde você poderá acessá-los, por exemplo, desta forma Img().src(“/images/logo.png“)

Depuração

Inicie o Webber da seguinte maneira

webber serve apenas para iniciá-lo rapidamente

webber serve -t pwa -s Service para iniciá-lo no modo PWA

Parâmetros adicionais

-v ou --verbose para mostrar mais informações no console para fins de depuração

-p 443 ou --port 443 para iniciar o servidor webber na porta 443 em vez do padrão 8888

--browser chrome/safari para abrir automaticamente o navegador desejado, por padrão não abre nenhum

--browser-self-signed necessário para depurar service workers localmente, caso contrário, eles não funcionam

--browser-incognito para abrir uma instância adicional do navegador no modo de navegação anônima, funciona apenas com o Chrome


Portanto, para criar seu aplicativo no modo de depuração, abra-o automaticamente no Chrome e atualize o navegador automaticamente sempre que alterar qualquer arquivo, inicie-o dessa maneira

para SPA

webber serve --browser chrome

para testes reais de PWA

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


Carregamento inicial do aplicativo

Você pode querer melhorar o processo de carregamento inicial.

Para isso basta abrir a pasta .webber/entrypoint/dev dentro do projeto e editar o arquivo index.html .

Ele contém o código HTML inicial com ouvintes muito úteis: WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError .

Você é livre para editar esse código para o que quiser para implementar seu estilo personalizado 🔥

Quando terminar a nova implementação, não se esqueça de salvá-la na pasta .webber/entrypoint/release

Liberação de construção

Basta executar webber release ou webber release -t pwa -s Service para PWA.

Em seguida, pegue os arquivos compilados da pasta .webber/release e envie-os para o seu servidor.

como implantar

Você pode enviar seus arquivos para qualquer hospedagem estática.

A hospedagem deve fornecer o tipo de conteúdo correto para arquivos wasm !

Sim, é muito importante ter o cabeçalho correto Content-Type: application/wasm para arquivos wasm , caso contrário, infelizmente, o navegador não poderá carregar seu aplicativo WebAssembly.

Por exemplo, o GithubPages não fornece o tipo de conteúdo correto para arquivos wasm , portanto, infelizmente, é impossível hospedar sites WebAssembly nele.

NginxGenericName

Se você usa seu próprio servidor com nginx, abra /etc/nginx/mime.types e verifique se ele contém application/wasm wasm; registro. Se sim, então você está pronto para ir!

Conclusão

Espero ter te surpreendido hoje e que você pelo menos experimente o SwifWeb e, no máximo, comece a usá-lo em seu próximo grande projeto da web!


Sinta-se à vontade para contribuir com qualquer uma das bibliotecas SwifWeb e também para marcar ⭐️ todas elas!


Eu tenho uma grande comunidade SwiftStream no Discord, onde você pode encontrar grande suporte, ler pequenos tutoriais e ser notificado primeiro sobre as próximas atualizações! Seria legal ver você conosco!


É apenas o começo, então fique ligado para mais artigos sobre SwifWeb!


Conte aos seus amigos!