千呼万唤始出来☑️:SwiftWebUI
作者:The Always Right Institute,原文链接,原文日期:2019-06-30
译者:Ji4n1ng;校对:numbbbbb,WAMaker;定稿:Pancf
六月初,Apple 在 WWDC 2019 上发布了 SwiftUI。SwiftUI 是一个“跨平台的”、“声明式”框架,用于构建 tvOS、macOS、watchOS 和 iOS 上的用户界面。SwiftWebUI 则将它带到了 Web 平台上✔️。
免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。
SwiftWebUI
那么究竟什么是 SwiftWebUI?它允许你编写可以在 Web 浏览器中显示的 SwiftUI 的 视图:
import SwiftWebUI |
结果是:
![]()
与其他一些工作不同,SwiftWebUI 不仅仅是将 SwiftUI 视图渲染为 HTML,而且还在浏览器和 Swift 服务器中托管的代码之间建立了一个连接,这样就可以实现各种交互功能——按钮、选择器、步进器、列表、导航等,这些都可以做到!
换句话说:SwiftWebUI 是针对浏览器的 SwiftUI API(很多部分但不是所有)的一种实现。
再次进行免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。
学习一次,随处使用
SwiftUI 的既定目标不是“编写一次,随处运行”,而是“学习一次,随处使用”。不要期望在 iOS 上开发了一个漂亮的 SwiftUI 应用程序,然后将它的代码放入 SwiftWebUI 项目中,并让它在浏览器中呈现完全相同的内容。这不是我们的重点。
关键是能够重用 SwiftUI 的原理并使其在不同平台之间共享。在这种情况下,SwiftWebUI 就达到目的了✔️。
但是先让我们深入了解一下细节,并编写一个简单的 SwiftWebUI 应用程序。本着“学习一次,随处使用”的精神,首先观看这两个 WWDC 演讲:介绍 SwiftUI 和 SwiftUI 要点。本文不会过多的深入数据流有关的内容,但这篇演讲同样推荐观看(这些概念在 SwiftWebUI 中被广泛支持):SwiftUI 中的数据流。
要求
到目前为止,SwiftWebUI 需要安装 macOS Catalina 来运行(“Swift ABI”🤦♀️)。幸运的是,将 Catalina 安装在单独的 APFS 卷 上非常容易。并且需要安装 Xcode 11 才能获得在 SwiftUI 中大量使用的 Swift 5.1 新功能。明白了吗?很好!
Linux 呢?这个项目确实准备在 Linux 上运行,但尚未完成。唯一还没完成的事情是对 Combine PassthroughSubject 的简单实现以及围绕它的一些基础设施。准备:NoCombine。欢迎来提 PR!
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 选项,以便于总能获得最新和最好的版本(也可以使用修订版或 develop 分支):
![]()
最后,将 SwiftWebUI 库添加到你的工具的 target 中:
![]()
这就完成了创建。你现在有了一个可以导入 SwiftWebUI 的工具项目。(Xcode 可能需要一些时间来获取和构建依赖。)
SwiftWebUI Hello World
让我们开始使用 SwiftWebUI。打开 main.swift 文件,将其内容替换为:
import SwiftWebUI |
在 Xcode 中编译并运行该应用程序,打开 Safari,然后访问 http://localhost:1337/:
![]()
这里发生了什么:首先导入 SwiftWebUI 模块(不要意外导入 macOS SwiftUI 😀)。
然后我们调用了 SwiftWebUI.serve,它要么接受一个返回视图的闭包,要么就直接是一个视图——如下所示:一个 Text 视图(也称为“UILabel”,它可以显示纯文本或格式化的文本)。
幕后发生的事情
在内部,serve 函数创建一个非常简单的 SwiftNIO HTTP 服务器,它将会监听 1337 端口。当浏览器访问该服务器时,它会创建一个 session(会话)并将(Text)视图传递给该会话。
最后,SwiftWebUI 在服务器上根据这个视图来创建一个“Shadow DOM”,将其渲染为 HTML 并将结果发送到浏览器。“Shadow DOM”(和状态对象保持在一起)存储在会话中。
这是 SwiftWebUI 应用程序与 watchOS 或 iOS SwiftUI 应用程序之间的区别。单个 SwiftWebUI 应用程序为一组用户提供服务,而不仅仅是一个用户。
添加一些交互
第一步,更好地组织代码。在项目中创建一个新的 Swift 文件,并将其命名为 MainPage.swift。然后向其中添加一个简单的 SwiftUI 视图的定义:
import SwiftWebUI |
修改 main.swift 来让 SwiftWebUI 作用于我们的定制视图:
SwiftWebUI.serve(MainPage()) |
现在,可以把 main.swift 放到一边,在自定义视图中完成所有工作。添加一些交互:
struct MainPage: View { |
视图 有了一个名为 counter 的持久 状态 变量(不知道这是什么?再看一下 SwiftUI 的介绍)。还有一个可以使计数器加一的小函数。
然后,使用 SwiftUI tapAction 修饰符将事件处理程序附加到 Text。最后,在标签中显示当前值:
![]()
🧙魔法🧙
幕后发生的事情
这是如何运作的?当浏览器访问端点时,SwiftWebUI 在其中创建了会话和“Shadow DOM”。然后将描述视图的 HTML 发送到浏览器。tapAction 通过向 HTML 添加 onclick 处理程序来工作。SwiftWebUI 还向浏览器发送 JavaScript(少量,没有大的 JavaScript 框架!),处理点击并将其转发到 Swift 服务器。
然后 SwiftUI 的魔法开始生效。SwiftWebUI 将 click 事件与“Shadow DOM”中的事件处理程序相关联,并调用 countUp 函数。该函数通过修改 counter 状态 变量,使视图的渲染无效。SwiftWebUI 开始工作,并对“Shadow DOM”中的变更进行差异比较。然后将这些变更发送回浏览器。
“变更”作为 JSON 数组发送,页面中的小型 JavaScript 可以处理这些数组。如果整个子树发生了变化(例如,如果用户导航到一个全新的视图),则变更可以是应用于
innerHTML或outerHTML的更大的 HTML 片段。但通常情况下,这些变更都很小,例如
添加类,设置 HTML 属性等(即浏览器 DOM 修改)。
🥑🍞 Avocado Toast
太好了,基础的部分可以正常工作了。让我们引入更多的交互。以下是基于 SwiftUI 要点 演讲中演示 SwiftUI 的“Avocado Toast App”。没看过吗?你应该看看,讲的是美味的吐司。
HTML / CSS 样式不漂亮也不完美。你知道,我们不是网页设计师,而且需要帮助。欢迎来提交 PR!
想要跳过细节,观看应用程序的 GIF 并在 GitHub 上下载:🥑🍞。
🥑🍞订单
谈话从这(~6:00)开始,可以将这些代码添加到新的 OrderForm.swift 文件中:
struct Order { |
在 main.swift 中直接用 SwiftWebUI.serve() 测试新的 OrderForm 视图。
这就是浏览器中的样子:
![]()
SemanticUI 用于在 SwiftWebUI 中设置一些样式。SemanticUI 并不是必须的,这里只是用它的控件来美化界面。
注意:仅使用 CSS 和字体,而不是 JavaScript 组件。
幕间休息:一些 SwiftUI 布局
在 SwiftUI 要点 演讲的 16:00 左右,他们将介绍 SwiftUI 布局和视图修改器排序:
var body: some View { |
结果如下,请注意修饰符的排序是如何相关的:
![]()
SwiftWebUI 尝试复制常见的 SwiftUI 布局,但还没有完全成功。毕竟它必须处理浏览器提供的布局系统。需要帮助,欢迎弹性盒布局相关的专家!
🥑🍞订单历史
回到应用程序,演讲(~19:50)介绍了 列表 视图,用于显示 Avocado toast 订单历史记录。这就是它在 Web 上的外观:
![]()
列表 视图遍历已完成订单的数组,并为每个订单创建一个子视图(OrderCell),并传入列表中的当前项。
这是我们使用的代码:
struct OrderHistory: View { |
SwiftWebUI 列表视图效率很低,它总是呈现整个子集合。没有单元格重用,什么都没有😎。在一个网络应用程序中有各种各样的方法来处理这个问题,例如使用分页或更多客户端逻辑。
你不必手动输入演讲中的样本数据,我们为你提供了这些数据:
let previousOrders : [ CompletedOrder ] = [ |
🥑🍞涂抹酱选择器
选择器控件以及如何将它与枚举一起使用将在(~43:00)进行演示。首先是各种吐司选项的枚举:
enum AvocadoStyle { |
可以将这些代码添加到 Order 结构体中:
struct Order { |
然后使用不同的选择器类型来显示它们。如何循环枚举值非常简单:
Form { |
结果是:
![]()
同样,这需要一些对 CSS 的热爱来让它看起来更好看…
完成后的🥑🍞应用
不,我们与原版略有不同,也没有真正完成应用。它看起来并不那么棒,但毕竟只是一个演示示例😎。
![]()
完成后的应用程序可在GitHub:AvocadoToast 上获取。
HTML 和 SemanticUI
UIViewRepresentable 在 SwiftWebUI 中对应的实现,是直接使用原始 HTML。
它提供了两种变体,一种是 HTML 按原样输出字符串,另一种是通过 HTML 转义内容:
struct MyHTMLView: View { |
使用这个原语,基本上可以构建所需的任何 HTML。
还有一种更高级的用法是 HTMLContainer,SwiftWebUI 内部也用到了它。例如,这是步进器控件的实现:
var body: some View { |
HTMLContainer 是“响应式的”,即如果类、样式或属性发生变化,它将触发(emit)常规 DOM 变更(而不是重新渲染整个内容)。
SemanticUI
SwiftWebUI 还附带了一些预先设置的 SemanticUI 控件:
VStack { |
……渲染为如下内容:
![]()
请注意,SwiftWebUI 还支持一些 SFSymbols 图像名称(通过
Image(systemName:)来使用)。这些都得到了 SemanticUI 对 Font Awesome 的支持。
还有 SUISegment,SUIFlag 和 SUICARD:
SUICards { |
……渲染为这些内容:
![]()
添加此类视图非常简单,也非常有趣。可以使用 WOComponent 的 SwiftUI 视图来快速构建相当复杂和美观的布局。
Image.unsplash根据http://source.unsplash.com上运行的 Unsplash API 来构建图像的查询。只需给它一些查询词、大小和可选范围。注意:有时,特定的 Unsplash 服务似乎有点慢且不可靠。
总结
这就是我们的演示示例。我们希望你能喜欢!但要再次进行免责声明:这是一个玩具项目!不要用于生产。使用 SwiftWebUI 是为了了解更多关于 SwiftUI 本身及其内部工作原理的信息。
我们认为它是一个很好的玩具,可能也是一个有价值的工具,以便于更多地了解 SwiftUI 的内部工作原理。
技术随记
这些只是关于该技术的各个方面的一些笔记。可以跳过,这个不是那么的有趣😎。
问题
SwiftWebUI 有很多问题,有些是在 GitHub 上提出的:Issues。欢迎来提更多问题。
相当多的 HTML 布局的东西有问题(例如 ScrollView 并不总是滚动的),还有一些像 Shapes 这样的正在讨论方案的功能也有问题(可能通过 SVG 和 CSS 很容易做到)。
哦,还有一个例子是 If-ViewBuilder 不能正常工作。不明白为什么:
var body: some View { |
需要帮忙!欢迎来提交 PR!
与原来的 SwiftUI 相比
本文的实现非常简单且效率低下。在现实情况下,必须以更高的速率来处理状态修改事件,以 60Hz 的帧速率做所有的动画等等。
我们侧重于使基本操作正确,例如状态和绑定如何工作,视图如何以及何时更新等等。很可能本文的实现在某些方面并不正确,可能是因为 Apple 忘了将原始资源作为 Xcode 11 的一部分发送给我们。
WebSockets
我们目前使用 AJAX 将浏览器连接到服务器。使用 WebSockets 有多种优势:
- 保证了事件的顺序(AJAX 请求可能不同步到达)
- 非用户发起的服务器端 DOM 更新(定时器、推送)
- 会话超时指示器
这会让实现一个聊天客户端的演示示例变得非常容易。
添加 WebSockets 实际上非常简单,因为事件已经作为 JSON 发送了。我们只需要客户端和服务器端的垫片(shims)。所有这些都已经在 swift-nio-irc-webclient 中试用过了,只需要移植一下。
SPA
SwiftWebUI 的当前版本是一个连接到有状态后端服务器的 SPA(单页面应用程序)。
还有其他方法可以做到这一点,例如,当用户通过正常的链接遍历应用程序时,保持树的状态。又名 WebObjects。;-)
一般来说,最好能更好地控制 DOM ID 生成、链接生成以及路由等等。这和 SwiftObjects 所提供的方式类似。
但是最终用户将不得不放弃很多本可以“学习一次,随处使用”的功能,因为 SwiftUI 操作处理程序通常是围绕着捕捉任意状态的事实来构建的。
我们将会期待基于 Swift 的服务器端框架提出什么更好的东西来👽。
WASM
一旦我们找到合适的 Swift WASM(WebAssembly),SwiftWebUI 就会更有用处。期待 WASM!
WebIDs
有些像 ForEach 这样的 SwiftUI 视图需要 Identifiable 对象,其中的 id 可以是任何 Hashable。这在 DOM 中不太好,因为我们需要基于字符串的 ID 来识别节点。
这是通过将 ID 映射到全局映射中的字符串来解决的。这在技术上是无界的(一个类引用的特定问题)。
总结:对于 web 代码,最好使用字符串或整型来标识个体。
表单
表单需要做得更好:Issue。
SemanticUI 有一些很好的表单布局,我们可能参照这些布局重写子树。有待商榷。
面向 Swift 的 WebObjects 6
花了点时间在文章中嵌入了下面这个可点击的 Twitter 控件。(译者注:由于某些原因,这里没办法像原文一样嵌入 Twitter 控件,只能放链接。)
苹果确实给了我们一个“Swift 风格”的 WebObjects 6!
下一篇:直面 Web 和一些 Swift 化的 EOF(又名 CoreData 又名 ZeeQL)。
链接
- GitHub:SwiftWebUI
- SwiftUI
- 介绍 SwiftUI(204)
- SwiftUI 要点 (216)
- SwiftUI 中的数据流(226)
- SwiftUI 框架 API
- SwiftObjects
- SemanticUI
- SwiftNIO
联系方式
嘿,我们希望你能喜欢这篇文章,并且也希望得到你的反馈!
Twitter(任何一个都可以):@helje5,@ar_institute。
电子邮件:wrong@alwaysrightinstitute.com
Slack:在 SwiftDE、swift-server、noze、ios-developers 上找到我们。
写于 2019 年 6 月 30 日
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg。