- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。