paint-brush
Swift を Web 開発に使用する方法@imike
17,057 測定値
17,057 測定値

Swift を Web 開発に使用する方法

Mikhail Isaev33m2023/03/20
Read on Terminal Reader

長すぎる; 読むには

SwifWeb は、SwiftUI を使用して Web サイトを作成できるようにするフレームワークです。 HTML と CSS の標準全体、およびすべての Web API をラップします。この記事では、SwifWeb フレームワークを使用して Web サイトの構築を開始する方法を紹介します。
featured image - Swift を Web 開発に使用する方法
Mikhail Isaev HackerNoon profile picture
0-item
1-item

Web 開発の世界は広大で、毎日出現する新しいテクノロジの絶え間ない流れの中で迷子になりがちです。これらの新しいテクノロジーのほとんどは、JavaScript または TypeScript を使用して構築されています。しかし、この記事では、ブラウザー内で直接ネイティブ Swift を使用して Web 開発を行う方法を紹介します。


それはどのように可能ですか?

Swift が Web ページでネイティブに動作するには、最初に WebAssembly バイトコードにコンパイルする必要があり、その後 JavaScript がそのコードをページにロードできます。特別なツールチェーンを使用してヘルパー ファイルを作成する必要があるため、コンパイルのプロセス全体は少しトリッキーです。そのため、Carton と Webber というヘルパー CLI ツールが用意されています。

サードパーティのツールチェーンを使用しても問題ありませんか?

SwiftWasmコミュニティは、元の Swift ツールチェーンにパッチを適用することにより、Swift を WebAssembly にコンパイルできるようにするために多大な作業を行いました。毎日、元のツールチェーンから変更を自動的にプルし、テストが失敗した場合はフォークを修正することで、ツールチェーンを更新します。彼らの目標は、公式ツールチェーンの一部になることであり、近い将来それが実現することを望んでいます。

カートンかウェバーか?

Cartonは SwiftWasm コミュニティによって作成され、SwiftUI を使用して Web サイトを作成できるフレームワークである Tokamak プロジェクトに使用できます。


Webber はSwifWeb プロジェクト用に作られています。 SwifWeb は、すべての Web API だけでなく、HTML および CSS 標準全体をラップするという点で異なります。


コードの一貫性のために SwiftUI を使用して Web アプリを作成することを好むかもしれませんが、Web 開発は本質的に異なり、SwiftUI と同じ方法でアプローチすることはできないため、これは間違ったアプローチだと思います。


そのため、私は SwifWeb を作成しました。これにより、HTML、CSS、および Web API のすべての機能を Swift から直接使用でき、オートコンプリートとドキュメントを備えた美しい構文を使用できます。また、Webber ツールを作成したのは、Carton が SwifWeb アプリ用に作成されていないため、SwifWeb アプリを正しい方法でコンパイル、デバッグ、デプロイできないためです。


私の名前は Mikhail Isaev で、SwifWeb の作成者です。この記事では、SwifWeb を使用して Web サイトの構築を開始する方法を紹介します。

必要なツール


迅速

Swift をインストールする必要があります。最も簡単な方法は次のとおりです。

  • macOSではXcodeをインストールすることです
  • Linux または Windows (WSL2) では、 swiftlang.xyzのスクリプトを使用します。


それ以外の場合は、公式 Web サイトのインストール手順をご覧ください。

ウェバー CLI

アプリのビルド、デバッグ、デプロイを支援する Webber を作成しました。


macOS では、HomeBrew を使用して簡単にインストールできます ( Web サイトからインストールしてください)。

 brew install swifweb/tap/webber

後で最新バージョンに更新するには、実行するだけです

brew upgrade webber


Ubuntu または Windows (WSL2 の Ubuntu) では、Webber を手動で複製してコンパイルします。

 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

後で実行して最後のバージョンに更新する

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

メインブランチには常に安定したコードが含まれているため、そこから更新を自由にプルしてください

新しいプロジェクトの作成

ターミナルを開いて実行

webber new

インタラクティブ メニューでpwaまたはspaを選択し、プロジェクト名を入力します。


ディレクトリを新しく作成したプロジェクトに変更し、 webber serveを実行します。

このコマンドは、プロジェクトを WebAssembly にコンパイルし、必要なすべてのファイルを特別な.webberフォルダー内にパッケージ化し、デフォルトでポート8888を使用してすべてのインターフェイスでプロジェクトの提供を開始します。


webber serveの追加引数

  • アプリの種類

    プログレッシブ Web アプリの-t pwa

    -t spa for Single Web App

  • Service Worker ターゲットの名前 (通常、PWA プロジェクトではServiceという名前)

    -s Service

  • アプリ ターゲットの名前 (既定ではApp )

    -a App

  • コンソールに詳細情報を表示

    -v

  • Webber サーバーのポート (デフォルトは8888 )

    -p 8080

-p 443を使用して、実際の SSL のようにテストします (自己署名 SSL 設定を許可)

  • 自動起動先ブラウザ名

    --browser safariまたは--browser chrome

  • Service-Worker をデバッグするための自己署名 SSL 設定が許可されたブラウザーの追加インスタンス

    --browser-self-signed

  • シークレット モードのブラウザの追加インスタンス

    --browser-incognito

応用

アプリは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() } }

ライフサイクル

iOS のような方法で動作します。

アプリの起動直後のdidFinishLaunching

アプリが終了するときにwillTerminate

ウィンドウが非アクティブになるときのwillResignActive

ウィンドウがアクティブなときにdidBecomeActive

ウィンドウがバックグラウンドに入るときのdidEnterBackground

ウィンドウが最前面に移動するときはwillEnterForeground


ここで最も役立つメソッドはdidFinishLaunchingです。アプリを構成するのに最適な場所だからです。まるで iOS アプリのようです。 😀


appには便利な便利なメソッドが含まれています。

registerServiceWorker(“serviceName“)を呼び出して、PWA サービス ワーカーを登録します。

addScript(“path/to/script.js“)呼び出しで相対スクリプトまたは外部スクリプトを追加

addStylesheet(“path/to/style.css“)呼び出しで相対スタイルまたは外部スタイルを追加

addFont(“path/to/font.woff”, type:)相対または外部フォントを追加するための呼び出し、オプションでタイプを設定

addIcon(“path/to/icon“, type:color:)アイコンを追加するための呼び出し、オプションでタイプと色を設定


また、 AutolayoutBootstrapMaterializeなどの追加ライブラリを構成する場所でもあります。

ルート

現在の URL に基づいて適切なページを表示するには、ルーティングが必要です。


ルーティングの使用方法を理解するには、URL とは何かを理解する必要があります

https://website.com/hello/world - ここで/hello/worldパスです

ご覧のとおり、最初に App クラスですべての最上位ルートを宣言する必要があります。

トップレベルとは、これらのルートで宣言されたページがウィンドウ内のすべてのスペースを占有することを意味します。


たとえば、ルート ルートは 3 つの方法で設定できます。

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

最後が一番綺麗だと思います^^


ログインまたは登録ルートは次のように設定できます

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


パラメータ関連のルート

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

上記の例の:id は、ルートの動的部分です。 ArticlePageクラスでこの識別子を取得して、関連付けられた記事を表示できます。

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

パスには複数のパラメーターを含めることができます。すべて同じ方法で取得します。

クエリ

パスで次に興味深いのはqueryです。これも非常に使いやすいです。たとえば、検索textageクエリ パラメーターが必要な/searchルートを考えてみましょう。

https://website.com/search**?text=Alex&age=19** - 最後の部分はクエリです


探索ルートを宣言するだけ

Page("search") { SearchPage() }

そして、このようにSearchPageクラスでクエリ データを取得します

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

なんでも

*を使用して、このように特定のパス部分で何でも受け入れるルートを宣言することもできます

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

上記のルートは、foo と bar の間のすべてを受け入れます (例: /foo/aaa/bar、/foo/bbb/bar など)。

キャッチオール

**記号を使用すると、特定のパスで他のルートに一致していないものをすべて処理する特別なキャッチオール ルートを設定できます。


それを使用して、グローバル 404 ルートを作成します

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

または特定のパス、たとえばユーザーが見つからない場合

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


上記で宣言されたルートの状況を明確にしましょう

/user/1 - /user/:id のルートがある場合、 UserPageが返されます。そうでなければ、それは…に陥ります


UserNotFoundPage

/user/1/hello - /user/:id/hello のルートがある場合、 UserNotFoundPageに分類されます

/something - /something へのルートがない場合、 NotFoundPageに分類されます

ネストされたルーティング

次のルートのためにページのコンテンツ全体を置き換えるのではなく、特定のブロックのみを置き換えたい場合があります。ここでFragmentRouterが役に立ちます。


/userページにタブがあるとします。各タブはサブルートであり、 FragmentRouterを使用してサブルートの変更に対応したいと考えています。


Appクラスで最上位ルートを宣言する

Page("user") { UserPage() }

そしてUserPageクラスでFragmentRouter を宣言します

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


上記の例では、 FragmentRouter は/user/profileおよび/user/friendsサブルートを処理し、それをNavbarの下にレンダリングするため、ページはコンテンツ全体をリロードすることはなく、特定のフラグメントのみをリロードします。


同じまたは異なるサブルートを持つ複数のフラグメントを宣言することもでき、それらはすべて魔法のように連携します!


ところでFragmentRouterDivであり、呼び出すことで構成できます

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

スタイルシート

従来の CSS ファイルを使用できますが、Swift で記述されたスタイルシートを使用する新しい魔法の機能も備えています。

基本

Swift を使用して CSS ルールを宣言するために、 Ruleオブジェクトがあります。


メソッドを呼び出すことで宣言的に構築できます

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

または @resultBuilder を使用した SwiftUI のような方法

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


どちらの方法も同じですが、入力した直後にオートコンプリートされるため、最初の方法を好みます. 😀

MDN で説明されているすべての CSS メソッドが利用可能です。

それ以上に、ブラウザのプレフィックスを自動的に処理します!

ただし、特定のケースでは、この方法でカスタム プロパティを設定できます。

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

セレクタ

Rule が影響を与える要素を設定するには、セレクターを設定する必要があります。セレクターはデータベース内のクエリと見なされますが、そのセレクター クエリの一部をポインターと呼んでいます。


ポインターを構築する最も簡単な方法は、生の文字列を使用して初期化することです

Pointer("a")


しかし、正しい迅速な方法は、このように必要な HTML タグで.pointerを呼び出してビルドすることです

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

基本的なポインターについてですが、 :hover :first :first-childなどの修飾子もあります。

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

既存の修飾子を宣言できます。それらはすべて利用可能です。

何か不足している場合は、遠慮なく拡張機能を作成して追加してください。

また、github でプル リクエストを送信して、全員に追加することを忘れないでください。

ポインタを連結することもできます

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


ルールでセレクターを使用する方法

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

ルールで複数のセレクターを使用する方法

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

次の CSS コードを生成します。

 a, h1#myId, div > p { }

反応性

アプリの暗いスタイルと明るいスタイルを宣言しましょう。後で、それらを簡単に切り替えることができます。

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


LightStyleDarkStyle は、別のファイルまたはたとえば 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) } }


そして、あるページのUIのどこかで呼び出すだけです

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

そして、関連するスタイルシートを有効または無効にします!かっこよくないですか? 😎


しかし、CSS の代わりに Swift でスタイルを記述するのは難しいと言うかもしれません。


ポイントは反応性! @State を CSS プロパティで使用して、その場で値を変更できます!


ちょっと見てみましょう。リアクティブなプロパティを持つクラスを作成し、実行時にいつでも変更できるため、そのクラスを使用する画面上の要素はすべて更新されます!多くの要素のクラスを切り替えるよりもはるかに効果的です!


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


後でコードの任意の場所から呼び出すだけです

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

スタイルシートとそれを使用するすべての要素の色が更新されます 😜


また、未加工の CSS をスタイルシートに追加することもできます

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

生のCSS文字列を必要なだけ何度でも混在させることができます

ページ

ルーターは各ルートでページをレンダリングしています。 Page は、 PageControllerから継承された任意のクラスです。


PageControllerには、 willLoad didLoad willUnload didUnload 、UI メソッドbuildUIおよびbody 、HTML 要素のプロパティ ラッパー変数などのライフサイクル メソッドがあります。

技術的には、 PageController は単なる Div であり、 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") } } }


あなたのページが小さい場合は、この短い方法でも宣言できます

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 }

それは美しく簡潔ではありませんか? 🥲


ボーナスの便利な方法

alert(message: String) - 直接の JS alertメソッド

changePath(to: String) - URL パスの切り替え

HTML 要素

最後に、HTML 要素を作成して使用する方法 (!) について説明します。


属性を持つすべての HTML 要素は Swift で利用できます。完全なリストはMDNなどにあります。


HTML 要素の短いリストの例:

SwifWeb コード

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


ご覧のとおり、Swift で HTML タグにアクセスするのは非常に簡単です。入力を除いて、それらはすべて同じ名前で表されているからです。これは、異なる入力タイプには異なるメソッドがあり、それらを混在させたくなかったためです。


単純Div

 Div()

このように、すべての属性とスタイル プロパティにアクセスできます。

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

サブクラス化

HTML 要素をサブクラス化してスタイルを事前定義するか、多数の事前定義された子要素と外部で使用できるいくつかの便利なメソッドを含む複合要素を作成するか、 didAddToDOMdidRemoveFromDOMなどのライフサイクル イベントを実現します。

単なるDivであるが定義済みの.dividerクラスを持つDivider要素を作成しましょう

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

サブクラス化するときは、スーパー メソッドを呼び出すことが非常に重要です。

これがないと、予期しない動作が発生する可能性があります。

DOM への追加

Element は、すぐにまたは後でPageControllerまたはHTML 要素の DOM に追加できます。


すぐに

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


または後でlazy varを使用して

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

そのため、事前にHTML 要素を宣言し、後でいつでも DOM に追加できます。

DOM からの削除

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

親要素にアクセスする

すべての HTML 要素には、DOM に追加された場合にその親へのアクセスを提供するオプションのスーパービュー プロパティがあります。

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

if/else 条件

特定の条件でのみ要素を表示する必要があることが多いので、そのためにif/else使用しましょう

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

しかし、それは反応的ではありません。 showDiv2falseに設定しようとしても、何も起こりません。


リアクティブな例

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 }


$showDiv2.map {…}を使用する必要があるのはなぜですか?

並べ替え:SwiftUIではないため。まったく。


@State詳細については、以下を参照してください。


生の HTML

生の HTML をページまたは HTML 要素に追加する必要がある場合もありますが、それは簡単に可能です。

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


ForEach

静的な例

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

CSS

上記の例と同じですが、 BuilderFunctionも利用できます

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


ForEachループでBuilderFunction使用して、次の例のdelay値のように、値を 1 回だけ計算できます。

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


関数を引数として取ることもできます

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

BuilderFunctionは HTML 要素でも利用できます:)

@State との反応性

@Stateは、今日の宣言型プログラミングにとって最も望ましいものです。


上記で説明したように、これは SwiftUI ではないため、すべてを追跡して再描画するグローバル ステート マシンはありません。また、HTML 要素は一時的な構造体ではなくクラスであるため、実際のオブジェクトであり、直接アクセスできます。それははるかに優れており、柔軟性があり、すべてを制御できます。

ボンネットの下は何ですか?

これは、変更についてすべてのサブスクライバーに通知するプロパティ ラッパーです。

変更を購読するには?

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

HTML 要素が変更にどのように反応できるか?

簡単なテキストの例

@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

簡単な数字の例

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

簡単なブール値の例

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

マッピング例

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

2 つの状態のマッピング

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

3 つ以上の州のマッピング

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

すべての HTML および CSS プロパティは@State値を処理できます

拡張機能

HTML 要素を拡張する

Div のような具体的な要素にいくつかの便利なメソッドを追加できます

extension Div { func makeItBeautiful() {} }

または、親classがわかっている場合は要素のグループ。


親子クラスは少ないです。

BaseActiveStringElement - ah1などの文字列で初期化できる要素用です。

BaseContentElement - divulなど、内部にコンテンツを持つことができるすべての要素用です。

BaseElement - すべての要素用です


したがって、すべての要素の拡張はこのように記述できます

extension BaseElement { func doSomething() {} }

色を宣言する

カラークラスは色を担当します。定義済みの HTML カラーがありますが、独自のものを使用できます

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

次にH1(“Text“).color(.myColor1)のように使用します。

クラスを宣言する

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

次に、 Div().class(.my)のように使用します

ID を宣言する

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

次に、 Div().id(.my)のように使用します

Web API

windowオブジェクトは完全にラップされ、 App.current.window変数を介してアクセスできます。

完全なリファレンスはMDNで入手できます。


以下で簡単な概要を説明しましょう

前景フラグ

App.swiftLifecycleで、またはこの方法で直接リッスンできます。

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

または、いつでもどこでも読むことができます

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

またはHTML要素でそれに反応する

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

アクティブフラグ

Foreground フラグと同じですが、 App.current.window.isActiveからアクセスできます。

ユーザーがまだウィンドウ内で対話しているかどうかを検出します。

オンライン状態

Foreground フラグと同じですが、 App.current.window.isOnlineからアクセスできます

ユーザーがまだインターネットにアクセスできるかどうかを検出します。

ダークモードのステータス

Foreground フラグと同じですが、 App.current.window.isDarkからアクセスできます

ユーザーのブラウザーまたはオペレーティング システムがダーク モードであるかどうかを検出します。

内寸

スクロールバーを含むウィンドウのコンテンツ領域 (ビューポート) のサイズ

App.current.window.innerSize 、内部のwidthheight値内のSizeオブジェクトです。

@State変数としても使用できます。

外寸

ツールバー/スクロールバーを含むブラウザ ウィンドウのサイズ。

App.current.window.outerSize 、内部のwidthheight値内のSizeオブジェクトです。

@State変数としても使用できます。

画面

現在のウィンドウがレンダリングされている画面のプロパティを検査するための特別なオブジェクト。 App.current.window.screenから利用できます。

通常、最も興味深いプロパティはpixelRatioです。

歴史

ユーザーが (ブラウザ ウィンドウ内で) アクセスした URL が含まれます。

App.current.window.historyまたは単にHistory.sharedで利用できます。

@State変数としてアクセスできるため、必要に応じてその変更をリッスンできます。

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

単純変数としてもアクセス可能

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

詳細はMDNで入手できます。

位置

現在の URL に関する情報が含まれます。

App.current.window.locationまたはLocation.sharedから利用できます。

@State変数としてアクセスできるため、必要に応じてその変更をリッスンできます。

これは、たとえばルーターの仕組みです。

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


単純な変数としてもアクセスできます

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

詳細はMDNで入手できます。

ナビゲーター

ブラウザに関する情報が含まれています。

App.current.window.navigatorまたは単にNavigator.shared経由で利用可能

通常、最も興味深いプロパティはuserAgent platform language cookieEnabled

ローカルストレージ

キーと値のペアを Web ブラウザーに保存できます。有効期限なしでデータを保存します。

App.current.window.localStorageまたは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()

変更の追跡

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

すべてのアイテムの削除の追跡

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

セッションストレージ

キーと値のペアを Web ブラウザーに保存できます。 1 つのセッションだけのデータを保存します。

App.current.window.sessionStorageまたは単にSessionStorage.sharedとして利用できます。

API は上記のLocalStorageとまったく同じです。

書類

ブラウザーに読み込まれた Web ページを表し、Web ページのコンテンツへのエントリ ポイントとして機能します。

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]

ローカリゼーション

静的ローカリゼーション

従来のローカリゼーションは自動で、ユーザーのシステム言語に依存します

使い方

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

動的ローカリゼーション

オンザフライで画面上のローカライズされた文字列を変更したい場合 (ページのリロードなし)

呼び出して現在の言語を変更できます

Localization.current = .es

ユーザーの言語を Cookie またはローカル ストレージのどこかに保存した場合は、アプリの起動時に設定する必要があります。

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

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

コンソール

単純なprint(“Hello world“) JavaScript のconsole.log('Hello world')に相当します


コンソールメソッドも愛で包まれています❤️

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

ライブプレビュー

ライブ プレビューを機能させるには、必要な各ファイルで WebPreview クラスを宣言します。

 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

リポジトリページの説明をお読みください。トリッキーですが、完全に機能するソリューションです😎


VSコード

VSCode内のExtensionsに移動し、 Webberを検索します。

インストールしたら、 Cmd+Shift+P (Linux/Windows ではCtrl+Shift+P ) を押します。

Webber Live Previewを見つけて起動します。

右側にライブ プレビュー ウィンドウが表示され、 WebPreviewクラスを含むファイルを保存するたびに更新されます。

JavaScript へのアクセス

SwifWebの基盤であるJavaScriptKitを通じて利用できます。

公式リポジトリにある方法をお読みください。

資力

cssjspngjpg 、およびプロジェクト内のその他の静的リソースを追加できます。

ただし、デバッグ中または最終リリースファイルでそれらを使用できるようにするには、次のようにPackage.swiftですべて宣言する必要があります。

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

後で、このImg().src(“/images/logo.png“)のようにアクセスできるようになります。

デバッグ

次の方法でWebberを起動します

webber serveすばやく起動するためだけに

webber serve -t pwa -s Service

追加パラメータ

-vまたは--verboseデバッグ目的でコンソールに詳細情報を表示します

-p 443または--port 443デフォルトの 8888 の代わりに 443 ポートで Webber サーバーを起動します

--browser chrome/safariすると、目的のブラウザが自動的に開きます。デフォルトでは何も開きません

--browser-self-signed Service Worker をローカルでデバッグするために必要です。そうしないと機能しません

--browser-incognitoモードでブラウザーの追加のインスタンスを開くには、chrome でのみ機能します。


したがって、アプリをデバッグ モードでビルドするには、Chrome でアプリを自動的に開き、ファイルを変更するたびにブラウザを自動的に更新します。

スパ用

webber serve --browser chrome

実際の PWA テスト用

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


アプリの初期読み込み

初期読み込みプロセスを改善したい場合があります。

そのためには、プロジェクト内の.webber/entrypoint/devフォルダーを開き、 index.htmlファイルを編集します。

これには、非常に便利なリスナーを持つ初期 HTML コードが含まれています: WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError

そのコードを自由に編集して、カスタム スタイルを実装したいものに変更できます 🔥

新しい実装が完了したら、同じものを.webber/entrypoint/releaseフォルダーに保存することを忘れないでください

建物のリリース

webber releaseまたはwebber release -t pwa -s Service for PWA を実行するだけです。

次に、コンパイルされたファイルを.webber/releaseフォルダーから取得し、サーバーにアップロードします。

導入方法

ファイルを任意の静的ホスティングにアップロードできます。

ホスティングはwasmファイルに正しい Content-Type を提供する必要があります!

はい、 wasmファイルの正しいヘッダーContent-Type: application/wasmを持つことが非常に重要です。そうしないと、残念ながらブラウザーは WebAssembly アプリケーションをロードできません。

たとえば、GithubPages はwasmファイルの正しい Content-Type を提供しないため、残念ながら WebAssembly サイトをホストすることはできません。

ニンクス

nginx で独自のサーバーを使用している場合は、 /etc/nginx/mime.typesを開き、 application/wasm wasm;記録。はいの場合は、準備完了です。

結論

少なくとも SwifWeb を試してみて、最大で次の大きな Web プロジェクトで SwifWeb を使い始めるようになることを願っています。


SwifWeb ライブラリのいずれかに自由に貢献し、それらすべてにスター ⭐️ を付けてください!


Discord には素晴らしい SwiftStream コミュニティがあり、大きなサポートを見つけたり、小さなチュートリアルを読んだり、今後の更新について最初に通知を受け取ることができます!私たちと一緒に会えたら最高です!


これはまだ始まったばかりなので、SwifWeb に関する記事を今後もお楽しみに!


友達に教えて!