- 原文地址:The missing ☑️: SwiftWebUI
- 原文做者:The Always Right Institute
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:iWeslie,Pingren
這個月初,蘋果在 2019 年 WWDC 大會公佈了 SwiftUI。它是一個獨立的「跨平臺」、「聲明式」框架,可用於構建 tvOS、macOS、watchOS 以及 iOS 的用戶界面(UI)。而 SwiftWebUI 正在將這個框架遷移到 Web 研發✔️。html
免責聲明:SwiftWebUI 只是一個玩具級項目!不要用於生產環境。建議用它來學習 SwiftUI 和它的內部工做原理。前端
因此 SwiftWebUI 到底能夠用來作什麼?答案是使用 SwiftWebUI,它能夠在 web 瀏覽器內展現你編寫的 SwiftUI View。android
import SwiftWebUI
struct MainPage: View {
@State var counter = 0
func countUp() { counter += 1 }
var body: some View {
VStack {
Text("🥑🍞 #\(counter)")
.padding(.all)
.background(.green, cornerRadius: 12)
.foregroundColor(.white)
.tapAction(self.countUp)
}
}
}
複製代碼
代碼運行的結果是:ios
和其餘一些代碼庫做出的努力不一樣,它並不只僅將 SwiftUI Views 渲染爲 HTML。它同時也會在瀏覽器和 Swift 服務器的代碼之間創建一個鏈接,用來支持用戶交互 —— 包括 button、picker、stepper、list、navigation 等等,所有均可以支持。git
換句話說:SwiftWebUI 是 SwiftUI API 於瀏覽器的實現(實現了大部分的 API,但不是所有)。github
重申一次免責聲明:SwiftWebUI 只是一個玩具級項目!不要用於生產環境。建議用它來學習 SwiftUI 和它的內部工做原理。web
SwiftUI 的核心目標不是「一次編碼,隨處可運行」,而是「一次學習,隨處可用」。不要期待着能夠將 iOS 上好看的 SwiftUI 應用直接拿來,把代碼拷貝到 SwiftWebUI 項目中而後就能夠在瀏覽器看到如出一轍的渲染效果。由於這並非 SwiftWebUI 的重點。macos
重點是可以像 knoff-hoff 同樣讓開發者模仿 SwiftUI 進行代碼實驗並看到運行結果,同時還能夠跨平臺共享。在這個意義上,Web 比較有優點。swift
如今讓咱們就開始着手細節,寫一個簡單的 SwiftWebUI 應用吧。秉承着「一次學習,隨處可用」這樣的理念,先看看這兩個 WWDC 會議記錄吧:SwiftUI 介紹 和 SwiftUI 核心。雖然在這篇博客中咱們不會深刻講解,可是推薦你看看 SwiftUI 的數據流(其中的大部分概念也適用於 SwiftWebUI)。後端
目前因爲 Swift ABI 不兼容,SwiftWebUI 須要 macOS Catalina 才能運行。幸運的是,在單獨的 APFS 宗捲上安裝 Catalina 很簡單。同時還須要安裝 Xcode 11,這樣才能使用最新的 Swift 5.1 特性,這些特性 SwiftUI 將會大量使用。都懂了嗎?很是好!
若是你使用的是 Linux 系統該怎麼辦?這個項目已經即將準備運行在 Linux 上了,可是工做還並無完成。目前項目還缺乏的部分是一個對 Combine PassthroughSubject 的簡單實現,而且在這個方面,我遇到了一點困難。目前準備好的代碼在:NoCombine。歡迎你們爲項目提 pull request!
若是你使用的是 Mojave 該怎麼辦?有一個方法能夠在 Mojave 和 Xcode 11 上運行項目。你須要建立一個 iOS 13 模擬器項目,而後將整個項目在模擬器中運行。
打開 Xcode 11,選擇 「File > New > Project…」 或者直接使用快捷鍵 Cmd-Shift-N:
選擇 「macOS / Command Line Tool」 項目模版:
給項目起一個合適的名字,咱們就用 「AvocadoToast」 吧:
而後,將 SwiftWebUI 添加到 Swift 包管理器並導入項目。這個選項在 「File / Swift Packages」 菜單中:
輸入 https://github.com/SwiftWebUI/SwiftWebUI.git
做爲包的 URL 地址:
「Branch」 設置爲 master
選項,這樣就總能夠獲取到最新和最優秀的代碼(你也可使用修訂版或者使用 develop
分支):
最後將 SwiftWebUI
庫加入到目標工具中:
這樣就能夠了。如今你有了一個能夠直接 import SwiftWebUI
的工具項目了。(Xcode 可能會須要一段時間來獲取和構建依賴。)
咱們如今就開始學習使用 SwiftWebUI 吧。打開 main.swift
文件而後將內容替換爲:
import SwiftWebUI
SwiftWebUI.serve(Text("Holy Cow!"))
複製代碼
將代碼進行編譯並在 Xcode 中運行應用,打開 Safari 瀏覽器而後訪問 http://localhost:1337/
:
這背後究竟發生了什麼事呢:首先 SwiftWebUI 模塊被引用進來(請注意不要不當心引用了 macOS SwiftUI 😀)
接下來咱們調用 SwiftWebUI.serve
,它可能會使用返回一個 View 的閉包,或者僅僅是一個 View —— 而如上所示,這裏返回的是個 Text
View(又名 「UILabel」,它能夠展現出簡單的或者格式化的文字)。
serve
函數中建立了一個很是簡單的SwiftNIO HTTP 服務器,這個服務器會監聽端口 1337。當瀏覽器訪問這個服務器的時候,它建立了一個 session 並將咱們的 (Text) View 傳遞給這個 session 了。 最後 SwiftWebUI 在服務器中建立了一個 「Shadow DOM」,將 View 渲染爲 HTML 並將結果發送給瀏覽器。這個 「Shadow DOM」(以及一個會和它綁定在一塊兒的狀態對象)會被保存在 session 中。
SwiftWebUI 應用和 watchOS 或者 iOS 上的 SwiftUI 應用是有區別的。一個 SwiftWebUI 應用能夠服務多個用戶,而不是像 SwiftUI 應用那樣只服務於一個用戶。
第一步完成後,咱們將代碼結構優化一下。在項目中建立一個新的 Swift 文件並命名爲 `MainPage.swift。併爲其添加一個簡單的 SwiftUI View 定義:
import SwiftWebUI
struct MainPage: View {
var body: some View {
Text("Holy Cow!")
}
}
複製代碼
調整 main.swift,使之能夠服務於咱們的自定義 View:
SwiftWebUI.serve(MainPage())
複製代碼
如今咱們能夠先不用去管 main.swift
了,能夠在咱們自定義的 View
中完成其餘的工做。如今咱們爲它添加一些用戶交互的功能:
struct MainPage: View {
@State var counter = 3
func countUp() { counter += 1 }
var body: some View {
Text("Count is: \(counter)")
.tapAction(self.countUp)
}
}
複製代碼
咱們的 View
有一個名爲 counter
的 State
變量(不清楚這是什麼?建議你能夠看一看 SwiftUI 介紹)。以及一個能夠增長 counter 的簡單函數。 而後咱們使用 SwiftUI 的修飾符 tapAction
將時間處理函數綁定到咱們的 Text
上。最後,咱們在標籤中展現當前的數值:
🧙♀️ 簡直像魔法同樣 🧙
這一切都是如何運做的呢?當咱們點擊瀏覽器後,SwiftWebUI 建立了一個含有 「Shadow DOM」 的 session。接下來它將會把 View 的 HTML 描述發送給瀏覽器。tapAction
經過 HTML 添加的 onclick
事件處理能夠被調用執行。SwiftWebUI 也能夠將 JavaScript 代碼傳輸給瀏覽器(只能傳輸少許代碼,不能夠是大型框架代碼!),這部分代碼將會處理點擊事件,並將事件轉發給咱們的 Swift 服務器。
而後就輪到 SwiftUI 魔法登場了。SwiftWebUI 讓點擊事件和咱們在 「Shadow DOM」 中的事件處理函數關聯在一塊兒,並會調用 countUp
函數。經過修改變量 counter
State
,函數將 View 的渲染設置爲無效。此時 SwiftWebUI 開始對比 「Shadow DOM」 中出現的區別和變化。接下來這些改變將會被髮回到瀏覽器中。
這些修改將會以 JSON 數組的形式發送,這些數組能夠被頁面上的 JavaScript 代碼片解析。若是 HTML 結構的一個子樹的全部內容都改變了(例如,假設用戶進入了一個新的 View),那麼這個修改就多是一個比較大的 HTML 代碼片,將會被應用於 innerHTML
或
outerHTML方法。 可是一般狀況下,修改都比較微小,例如
add class,
set HTML attribute` 這樣的(即瀏覽器 DOM 修改)。
棒極了,如今咱們已經完成了全部基礎工做。讓咱們來加入更多的交互性吧。下面的內容都是基於 「Avocado Toast 應用」的,它在 SwiftUI 核心演講中被用做爲 SwiftUI 的範例。你尚未看過它的話,建議你看一看,畢竟它是關於美味烤麪包片的(toast 又意爲麪包片)。
HTML 和 CSS 樣式還不是很完美,也不太美觀。而你也知道咱們並非 web 設計師,因此這方面咱們須要你們的幫助。歡迎給項目提出 pull request!
若是你想要跳過細節講解,直接查看應用的動圖,能夠在 GitHub 上下載:🥑🍞。
咱們從以下這段代碼開始吧(它在視頻中大約 6 分鐘的位置),首先咱們將它寫入一個新建的 OrderForm.swift
文件:
struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
}
struct OrderForm: View {
@State private var order = Order()
func submitOrder() {}
var body: some View {
VStack {
Text("Avocado Toast").font(.title)
Toggle(isOn: $order.includeSalt) {
Text("Include Salt")
}
Toggle(isOn: $order.includeRedPepperFlakes) {
Text("Include Red Pepper Flakes")
}
Stepper(value: $order.quantity, in: 1...10) {
Text("Quantity: \(order.quantity)")
}
Button(action: submitOrder) {
Text("Order")
}
}
}
}
複製代碼
這能夠直接測試 main.swift
中的 SwiftWebUI.serve()
以及新的 OrderForm` View。
以下是在瀏覽器展現的效果:
SemanticUI 可用於爲 SwiftWebUI 中的一些內容定義樣式。對於操做邏輯,它並非必需的,可是它能夠幫助你完成一些看起來不錯的小部件。 注意:它只用了 CSS/fonts,而沒有用 JavaScript 組件。
在 SwiftUI 核心演講的第 16 分鐘左右,他們開始解說 SwiftUI 佈局和 View 修飾符順序:
var body: some View {
HStack {
Text("🥑🍞")
.background(.green, cornerRadius: 12)
.padding(.all)
Text(" => ")
Text("🥑🍞")
.padding(.all)
.background(.green, cornerRadius: 12)
}
}
複製代碼
結果在這裏,注意觀察修飾符順序是如何相互聯繫的:
SwiftWebUI 在嘗試複製一些經常使用的 SwiftUI 佈局,但還並無徹底成功。畢竟這項工做與瀏覽器的佈局系統有關。咱們須要幫助,尤爲歡迎 flexbox 佈局方面的專家!
咱們接着回到應用的介紹中來,演講在大約 19 分 50 秒的時候介紹了能夠用於展現 Avocado toast 應用歷史訂單的 List View。這是它在 web 端展現的樣子:
List
View 遍歷了包含全部訂單的數組,而後爲每一項都建立了一個子 View(OrderCell
),並將列表中每一項訂單的信息傳入這個 OrderCell
。
這是咱們使用的代碼:
struct OrderHistory: View {
let previousOrders : [ CompletedOrder ]
var body: some View {
List(previousOrders) { order in
OrderCell(order: order)
}
}
}
struct OrderCell: View {
let order : CompletedOrder
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(order.summary)
Text(order.purchaseDate)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
if order.includeSalt {
SaltIcon()
}
else {}
if order.includeRedPepperFlakes {
RedPepperFlakesIcon()
}
else {}
}
}
}
struct SaltIcon: View {
let body = Text("🧂")
}
struct RedPepperFlakesIcon: View {
let body = Text("🌶")
}
// Model
struct CompletedOrder: Identifiable {
var id : Int
var summary : String
var purchaseDate : String
var includeSalt = false
var includeRedPepperFlakes = false
}
複製代碼
SwiftWebUI List View 的效率極低,它老是渲染出子元素的整個集合。Cell(列表單元格) 徹底沒有複用 😎。在 web 應用中,有不少不一樣的方式能夠解決這個問題,例如,經過使用分頁或者使用更多客戶端邏輯。
咱們已經爲你準備好了演講中使用的樣本數據代碼,你不須要再次打字輸入了:
let previousOrders : [ CompletedOrder ] = [
.init(id: 1, summary: "Rye with Almond Butter", purchaseDate: "2019-05-30"),
.init(id: 2, summary: "Multi-Grain with Hummus", purchaseDate: "2019-06-02",
includeRedPepperFlakes: true),
.init(id: 3, summary: "Sourdough with Chutney", purchaseDate: "2019-06-08",
includeSalt: true, includeRedPepperFlakes: true),
.init(id: 4, summary: "Rye with Peanut Butter", purchaseDate: "2019-06-09"),
.init(id: 5, summary: "Wheat with Tapenade", purchaseDate: "2019-06-12"),
.init(id: 6, summary: "Sourdough with Vegemite", purchaseDate: "2019-06-14",
includeSalt: true),
.init(id: 7, summary: "Wheat with Féroce", purchaseDate: "2019-06-31"),
.init(id: 8, summary: "Rhy with Honey", purchaseDate: "2019-07-03"),
.init(id: 9, summary: "Multigrain Toast", purchaseDate: "2019-07-04",
includeSalt: true),
.init(id: 10, summary: "Sourdough with Chutney", purchaseDate: "2019-07-06")
]
複製代碼
Picker 的控制以及如何與枚舉類型一塊兒使用它會在大約 43 分鐘的時候講解。首先咱們來看不一樣 toast 彈窗選項的枚舉類型;
enum AvocadoStyle {
case sliced, mashed
}
enum BreadType: CaseIterable, Hashable, Identifiable {
case wheat, white, rhy
var name: String { return "\(self)".capitalized }
}
enum Spread: CaseIterable, Hashable, Identifiable {
case none, almondButter, peanutButter, honey
case almou, tapenade, hummus, mayonnaise
case kyopolou, adjvar, pindjur
case vegemite, chutney, cannedCheese, feroce
case kartoffelkase, tartarSauce
var name: String {
return "\(self)".map { $0.isUppercase ? " \($0)" : "\($0)" }
.joined().capitalized
}
}
複製代碼
咱們能夠將這些都加入咱們的 Order
結構體:
struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
var avocadoStyle = AvocadoStyle.sliced
var spread = Spread.none
var breadType = BreadType.wheat
}
複製代碼
而後使用不一樣類型的 Picker 來展現它們。你能夠很是簡便的直接循環遍歷枚舉類型的全部值:
Form {
Section(header: Text("Avocado Toast").font(.title)) {
Picker(selection: $order.breadType, label: Text("Bread")) {
ForEach(BreadType.allCases) { breadType in
Text(breadType.name).tag(breadType)
}
}
.pickerStyle(.radioGroup)
Picker(selection: $order.avocadoStyle, label: Text("Avocado")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}
.pickerStyle(.radioGroup)
Picker(selection: $order.spread, label: Text("Spread")) {
ForEach(Spread.allCases) { spread in
Text(spread.name).tag(spread) // there is no .name?!
}
}
}
}
複製代碼
代碼運行的結果:
再次聲明,咱們須要一些 CSS 高手來讓界面更好看一些…
咱們和原生的 SwiftUI 界面其實還略有不一樣,如今也並無徹底的完成它。雖然看上去還不很是完美,可是畢竟已經能夠用來演示了 😎
最終完成的應用代碼能夠在 GitHub 上查看:AvocadoToast。
UIViewRepresentable
在 SwiftWebUI 中的等價物用於生成原生 HTML 代碼。
它提供了兩個變量,HTML
會按原樣輸出字符串,或者經過 HTML 轉譯內容:
struct MyHTMLView: View {
var body: some View {
VStack {
HTML("<blink>Blinken Lights</blink>")
HTML("42 > 1337", escape: true)
}
}
}
複製代碼
使用這種結構,你基本能夠構建出任何想要的 HTML。
級別稍微高級一些,可是也被用在 SwiftWebUI 中的是 HTMLContainer
。例如這是 Stepper
控制的實現方法:
var body: some View {
HStack {
HTMLContainer(classes: [ "ui", "icon", "buttons", "small" ]) {
Button(self.decrement) {
HTMLContainer("i", classes: [ "minus", "icon" ], body: {EmptyView()})
}
Button(self.increment) {
HTMLContainer("i", classes: [ "plus", "icon" ], body: {EmptyView()})
}
}
label
}
}
複製代碼
HTMLContainer
要更加靈活一些,例如,若是元素的 class,樣式或者屬性變化了,它將會生成一個常規的 DOM 變化(而不是從新渲染全部內容)。
SwiftWebUI 也包含了一些 SemanticUI 控制的預配置:
VStack {
SUILabel(Image(systemName: "mail")) { Text("42") }
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...), Color("blue"),
detail: Text("Friend"))
{
Text("Veronika")
} ...
}
}
複製代碼
…渲染結果爲:
注意,SwiftWebUI 也支持一些內置圖標庫(SFSymbols)圖像名(使用方法是 Image(systemName:)`)。SemanticUI 對 Font Awesome 的支持 是其幕後的技術支持。
同時 SwiftWebUI 還包括SUISegment
、SUIFlag
和 SUICard
:
SUICards {
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Zebra", "Animal"),
Text("Some Zebra"),
meta: Text("Roaming the world since 1976"))
{
Text("A striped animal.")
}
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Cow", "Animal"),
Text("Some Cow"),
meta: Text("Milk it"))
{
Text("Holy cow!.")
}
}
複製代碼
…其渲染效果爲:
添加這樣的 View 很是輕鬆愉快。使用 WOComponent 的 SwiftUI Views,每一個人都能快速地創做複雜又好看的佈局。
Image.unsplash
會根據運行在http://source.unsplash.com
的 API 構建圖片請求。只須要傳遞給它一些請求參數,好比你想要的圖片大小和其餘的可配置選項。 注意:這個 Unsplash 服務有時候會比較慢,有點靠不住。
上述全部就是本次的演示內容啦。但願你喜歡!可是重申一次免責聲明:SwiftWebUI 只是一個玩具級項目!不要用於生產環境。建議用它來學習 SwiftUI 和它的內部工做原理。
但咱們認爲它是一個很好的入門級試玩項目,也是一個學習 SwiftUI 內部工做原理的頗有價值的工具。
這裏列出了一系列關於技術的不一樣方面的提示信息。你能夠跳過不看,這些內容沒那麼有趣了 😎
咱們的項目就有不少的 issue,有一部分在 Github 上:Issues。你也能夠嘗試給咱們提更多的 issue。
這裏麪包括了不少與 HTML 佈局相關的內容(例如,ScrollView
有時候不會滾動),但同時也有不少開放式的問題,好比關於 Shape 的(若是使用 SVG 或者 CSS,可能會更容易實現)。
還有一個是關於 If-ViewBuilder 無效的問題。目前還不知道是什麼緣由:
var body: some View {
VStack {
if a > b {
SomeView()
}
// 目前還須要一個空的 else 語句:`else {}` 來使其能夠編譯。
}
}
複製代碼
咱們須要幫助,歡迎爲咱們提出 pull request!
目前咱們的實現方法很是簡單,也並不高效。正式版必需要處理高頻率的狀態改變,還要將全部的動畫效果都改成 60Hz 的幀率等等。
咱們目前主要集中經歷於將基礎的操做完成,例如,狀態和綁定如何運做,View 在什麼時候並以何種方式更新等等。不少時候實現方法均可能會出錯,然而 Apple 忘記將原始代碼做爲 Xcode 11 的一部分發送給咱們。
咱們如今使用 AJAX 來鏈接瀏覽器和服務器。而其實使用 WebSockets 可以帶來更大優點:
這會讓聊天客戶端的演示更輕鬆。
而爲項目添加 WebSocket 實際上很是簡單,由於目前事件已是以 JSON 的格式發送了。咱們只須要客戶端和服務端的 shim 就能夠了。這部份內容已經在 swift-nio-irc-webclient 實現,只須要遷移到項目中便可。
目前 SwiftWebUI 是一個 SPA(單頁應用)項目,和一個支持狀態的後端服務綁定。
也有其餘方式能夠實現 SPA,好比,當用戶經過普通連接在應用中的不一樣頁面切換時,保持狀態樹不變。又稱爲 WebObjects ;-)
一般狀況下,若是你想要對 DOM ID 的生成、連接的生成以及路由等等作更多更全面的控制,這是一個不錯的選擇。 可是最後,用戶可能不得不放棄「一次學習,隨處可用」,由於 SwiftUI 的行爲處理函數一般是圍繞着它們要捕獲任意的狀態這樣的事實構建的。
接下來咱們將會看到基於 Swift 的服務端框架作了什麼 👽
當咱們使用了合適的 Swift WASM,全部的代碼都能變得更加實用了。來一塊兒學習吧 WASM!
一些 SwiftUI View,好比 ForEach
,都須要 Identifiable
對象,使用了它那麼 id
就能夠是任意的 Hashable
值。可是用於 DOM 的時候,它的性能並不很是好,由於咱們須要字符串類型的 ID 來分辨節點。 而經過一個全局的 map 結構將 ID 映射爲字符串,它就能夠正常工做了。從技術上來講這並不難(就是一個特定的關於類引用的問題)。
總結:對於 web 端的代碼,使用字符串或者數字來識別項目是比較明智的選擇。
表單收到了不少人的青睞:Issue。
SemanticUI 有不少很好的表單佈局。咱們也許會重寫這部分的子樹。還有待完善。
等一下再點擊它:
爲 40s+ 的用戶做出的 SwiftUI 的總結。pic.twitter.com/6cflN0OFon
— Helge Heß (@helje5) 2019 年 6 月 7 日
使用 SwiftUI,Apple 真的給了咱們 Swift 模式的 WebObjects 6!
接下來:(讓咱們期待新時代的) Direct To Web 和 Swift 化的 EOF (即 CoreData 或 ZeeQL)。
嗨,咱們但願你喜歡這篇文章,咱們也很是歡迎你向咱們做出反饋! 反饋能夠發送在 Twitter 或者:@helje5、@ar_institute 均可以。 Email 地址爲:wrong@alwaysrightinstitute.com。 Slack:能夠在 SwiftDE、swift-server、noze、ios-developers 找到咱們。
寫於 2019 年 6 月 30 日
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。