
6 月初,苹果在 WWDC 2019 大会上发布了 SwiftUI。这是一个单一的“跨平台”“声明性”框架,可以用来构建 tvOS、macOS、watchOS 和 iOS/iPad OS 平台的 UI。而本文要介绍的SwiftWebUI则能将 SwiftUI 带到 Web 平台上。
免责声明:记住这是一个娱乐项目!可别把它用到生产环境里哦。它可以帮助你深入了解 SwiftUI 及其内部的工作机制。
SwiftWebUI
那么 SwiftWebUI 到底是什么?简单来说,你可以用它编写能显示在浏览器中的SwiftUI视图。详情请查看:
结果如下:

SwiftWebUI 和其他的一些方案不太一样,它不仅会将 SwiftUI 视图呈现为 HTML,还会在浏览器和 Swift 服务器中托管的代码之间建立一个连接,由此可以实现交互操作——按钮、选择器、步进器、列表、导航,所有这些都能用!
换句话说:SwiftWebUI 是 SwiftUI API 在浏览器上的(接近完整)的实现。
再强调一遍免责声明:记住这是一个娱乐项目!可别把它用到生产环境里哦。它可以帮助你深入了解 SwiftUI 及其内部的工作机制。
一次学习,随处使用
SwiftUI 的宗旨不是“一次编写,随处运行”,而是“一次学习,随处使用”。可别指望随手抓起一个 iOS 平台上写得好好的 SwiftUI 应用,然后把代码扔进 SwiftWebUI 项目,就能原样在浏览器上完美呈现出来。这不是它们的设计目标。
这里的关键是在各个平台复用同一套代码逻辑和框架。本文所关注的就是复用到 Web 平台上。
接下来我们开始深入研究,并编写一个简单的 SwiftWebUI 应用。本着“一次学习,随处使用”的精神,首先要看一遍这两个 WWDC 讲座:SwiftUI简介和SwiftUI要点。还有一个讲座内容比较深,超出了本文涉及的范畴(并且讲座中涉及的概念大都被 SwiftWebUI 支持了):经过SwiftUI的数据流。
系统需求
macOS Catalina
目前,SwiftWebUI 需要运行在 macOS Catalina 系统上。还好大家可以很容易地在单独的 APFS 卷上安装 Catalina。另外 SwiftUI 需要调用大量 Swift 5.1 中的新功能,所以还得安装 Xcode 11 才行。
为什么需要 Catalina? SwiftUI 使用新的 Swift 5.1 运行时功能(例如不透明的结果类型)。这些功能在 Mojave 附带的 Swift 5 运行时中不可用。 (另一个原因是使用仅在 Catalina 中可用的 Combine,尽管可以使用 OpenCombine 修复该部分)
tuxOS
SwiftWebUI 现在可以在 Linux 上使用 OpenCombine 运行(也可以在没有它的情况下运行,但有些东西不起作用,例如 NavigationView)。
需要 Swift 5.1 快照。我们还提供了一个包含 5.1 快照的 Docker 镜像:
Mojave 系统
想在 Mojave 与 Xcode 11 的组合上运行也能做到。你需要创建一个 iOS 13 模拟器项目,在里面运行所有内容。
第一个应用
创建 SwiftWebUI 项目
启动 Xcode 11,选择“File> New> Project …”或按 Cmd-Shift-N:

选择“macOS / Command Line Tool”项目模板:

取个好名字,这里用“AvocadoToast”:

然后我们将 SwiftWebUI 添加为 Swift Package Manager 依赖项。该选项隐藏在“File / Swift Packages”菜单组中:

输入https://github.com/SwiftWebUI/SwiftWebUI.git作为包的 URL

在“Branch”中选 master,始终获取最新更新(你也可以使用 revision 或 develop 分支):

最后将 SwiftWebUI 库添加到你的 tool target:

搞定了。现在你就有了一个可以 import SwiftWebUI 的工具项目。(Xcode 可能需要花些时间来获取和构建依赖项。)
SwiftWebUI Hello World
开始使用 SwiftWebUI 吧。打开 main.swift 文件并将其内容替换为:
在 Xcode 中编译并运行应用,打开 Safari 并访问http://localhost:1337/

背后发生了什么事情呢?首先是导入 SwiftWebUI 模块(可别搞错了,导入 macOS SwiftUI 就不对了😀)
然后我们调用 SwiftWebUI.serve,它要么接受一个闭包返回一个视图,要么直接出一个视图,这里就是一个Text视图(又名“UILabel”,可以显示普通或格式化的文本)。
幕后
在程序内部,serve函数创建了一个非常简单的SwiftNIOHTTP 服务器,侦听端口 1337。当浏览器访问该服务器时,它会创建一个会话并将我们的(Text)视图传递给该会话。
最后 SwiftWebUI 在服务器上从视图中创建一个“Shadow DOM”,将其呈现为 HTML 并将结果发送到服务器。这个“Shadow DOM”(和搭配的状态对象)存储在这个会话中。
这里就是 SwiftWebUI 应用与 watchOS 或 iOS SwiftUI 应用之间的区别所在。一个 SwiftWebUI 应用不仅服务一个用户,而是为一组用户提供服务。
添加一些交互
接下来我们给代码做些改进。在项目中创建一个新的 Swift 文件并调用 MainPage.swift。然后为其添加一个简单的 SwiftUI 视图定义:
根据我们的自定义视图来调整一下 main.swift:
然后就不用管 main.swift 了,所有工作都能在自定义的视图中完成。下面添加一些交互:
我们的视图得到了一个名为counter的持久状态变量(具体介绍见前面第一个 wwdc 讲座)。还有一个小函数来触发计数器。
然后我们使用SwiftUI tapAction修饰符将事件处理程序附加到 Text 上。最后我们在标签中显示当前值:

幕后
这里程序又是怎么工作的呢?当浏览器点击我们的端点时,SwiftWebUI 在其中创建了会话和我们的“Shadow DOM”。然后它将描述我们视图的 HTML 发送到浏览器。然后 tapAction 向 HTML 添加了一个 onclick 处理程序。SwiftWebUI 还向浏览器发送(少量,没那么多!)JavaScript,负责处理点击操作并将其转发到我们的 Swift 服务器。
然后轮到 SwiftUI 上场了。SwiftWebUI 将 click 事件与我们的“Shadow DOM”中的事件处理程序相关联并调用 countUp 函数。该函数修改了计数器状态变量,使视图的呈现无效。接着 SwiftWebUI 对“Shadow DOM”中的更改执行 diff 命令。然后这些更改被发送回浏览器。
这些“更改”被作为 JSON 数组发送出去,我们页面中的小 JavaScript 程序可以处理这些数组。如果整个子树发生了变化(例如,如果用户跳转到一个全新的视图),则更改可以是更大的 HTML 片段,应用于 innerHTML 或 outerHTML。
但通常情况下这些更改都不大,诸如 add class、set HTML attribute 等(比如浏览器 DOM 调整之类)。
吐司面包 Avocado Toast
基础打得很牢固。下面该引入更多的交互了。下面的内容是基于“SwiftUI 要点”讲座中演示 SwiftUI 用的“Avocado Toast 应用”。这个应用是关于美味的吐司面包的。
我们写的 HTML/CSS 样式不是很完美也不够漂亮。你也知道我们不是网页设计师,需要大家帮助。欢迎提交贡献!
完整应用下载链接:https://github.com/SwiftWebUI/AvocadoToast。
吐司面包订单
讲座中的相关内容大约从 6 分钟开始,我们把其中的代码添加到新的 OrderForm.swift 文件中:
测试一下,在 main.swift 中将 SwiftWebUI.serve()指向新的 OrderForm 视图。
浏览器中是这个样子:

SemanticUI(https://semantic-ui.com/)用来在 SwiftWebUI 中设置一些样式。这一步并不是非用它不可,它只是用来做一些好看的小部件的。
注意:这里只使用 CSS/fonts,不用 JavaScript 组件。
插点内容:SwiftUI 布局
在 SwiftUI 要点讲座中大约 16 分钟的时候,他们会讲 SwiftUI 布局和视图修饰符排序:
结果如下,请注意修饰符的排序:

SwiftWebUI 试图复制常见的 SwiftUI 布局,但还没有完全成功。毕竟它必须考虑浏览器提供的布局系统。欢迎 flexbox 专家提供帮助!
吐司面包订单历史
再回来看应用。讲座 19 分 50 秒开始介绍列表(https://developer.apple.com/documentation/swiftui/list)视图,用于显示牛油果土司面包的订单历史记录。它在 Web 端长成这个样子:

List 视图遍历已完成订单的数组,为每个订单创建一个子视图(OrderCell),并传入列表中的当前项。
以下是我们使用的代码:
SwiftWebUI 列表视图效率非常低,它总是呈现整个子集。没有单元复用,啥都没有。在 Web 应用中有多种方法可以处理这种情况,例如使用分页或更多客户端逻辑。
讲座中用到的示例数据如下:
吐司面包选择器
讲座第 43 分钟开始讲解 Picker 控件及它与枚举一起使用的方法。首先是各种吐司选项的枚举:
可以把它们加入 Order 结构中:
然后使用不同的 Picker 类型显示它们。循环枚举值的代码非常简洁:
结果:

这里也需要改进一下 CSS……
吐司面包“最终版”应用
其实我们的结果和原版略有不同,也不是什么完整的版本。它看起来没那么完美,但毕竟这只是一个演示嘛

HTML 和 SemanticUI
SwiftWebUI 中的对应 UIViewRepresentable(https://developer.apple.com/documentation/swiftui/uiviewrepresentable)的等效组件负责发出原始 HTML。
这里提供了两种变体,HTML 按原样输出字符串,或者通过 HTML 转义内容:
一般来说,你可以使用此原语构建任何 HTML。
HTMLContainer 的级别更高一些。例如,这是我们 Stepper 控件的实现:
HTMLContainer 是“反应性的”,即如果类、样式或属性发生变化(而不是重新呈现所有内容),它将发出常规 DOM 更改。
SemanticUI
SwiftWebUI 还有一些预设的 SemanticUI 控件:
呈现为:

请注意,SwiftWebUI 还支持一些 SFSymbols 图像名称(通过 Image(systemName:))。这些都是基于 SemanticUI 对 Font Awesome 的支持的(https://semantic-ui.com/elements/icon.html)。
还有 SUISegment、SUIFlag 和 SUICARD:
呈现为:

添加此类视图非常简单,非常有趣。可以使用 SwiftUI 视图快速构建相当复杂和美观的布局。
Image.unsplash 使用 Unsplash API(http://source.unsplash.com)构建图像查询。只需输入一些参数,诸如图像尺寸和可选范围即可。
注意:有时 Unsplash 服务不怎么好用。
总结
上面就是我们的演示了,希望你能喜欢!但要再次重复免责声明:记住这是一个娱乐项目!可别把它用到生产环境里哦。它可以帮助你深入了解 SwiftUI 及其内部的工作机制。
我们认为它是一个很好的玩具,可能也是一个有价值的工具。
查看原文:http://www.alwaysrightinstitute.com/swiftwebui/
评论