写点什么

SwiftWebUI 框架:带你深入理解 SwiftUI 原理

  • 2019-07-15
  • 本文字数:6957 字

    阅读完需:约 23 分钟

SwiftWebUI框架:带你深入理解SwiftUI原理

6 月初,苹果在 WWDC 2019 大会上发布了 SwiftUI。这是一个单一的“跨平台”“声明性”框架,可以用来构建 tvOS、macOS、watchOS 和 iOS/iPad OS 平台的 UI。而本文要介绍的SwiftWebUI则能将 SwiftUI 带到 Web 平台上。


免责声明:记住这是一个娱乐项目!可别把它用到生产环境里哦。它可以帮助你深入了解 SwiftUI 及其内部的工作机制。

SwiftWebUI

那么 SwiftWebUI 到底是什么?简单来说,你可以用它编写能显示在浏览器中的SwiftUI视图。详情请查看:


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) } }}
复制代码


结果如下:



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 镜像:


helje5 / swift

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 文件并将其内容替换为:


import SwiftWebUI
SwiftWebUI.serve(Text("Holy Cow!"))
复制代码


在 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 视图定义:


import SwiftWebUI
struct MainPage: View { var body: some View { Text("Holy Cow!") }}
复制代码


根据我们的自定义视图来调整一下 main.swift:


SwiftWebUI.serve(MainPage())
复制代码


然后就不用管 main.swift 了,所有工作都能在自定义的视图中完成。下面添加一些交互:


struct MainPage: View {  @State var counter = 3    func countUp() { counter += 1 }    var body: some View {    Text("Count is: \(counter)")      .tapAction(self.countUp)  }}
复制代码


我们的视图得到了一个名为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 文件中:


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 视图。


浏览器中是这个样子:



SemanticUI(https://semantic-ui.com/)用来在 SwiftWebUI 中设置一些样式。这一步并不是非用它不可,它只是用来做一些好看的小部件的。


注意:这里只使用 CSS/fonts,不用 JavaScript 组件。

插点内容:SwiftUI 布局

在 SwiftUI 要点讲座中大约 16 分钟的时候,他们会讲 SwiftUI 布局和视图修饰符排序:


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 秒开始介绍列表(https://developer.apple.com/documentation/swiftui/list)视图,用于显示牛油果土司面包的订单历史记录。它在 Web 端长成这个样子:



List 视图遍历已完成订单的数组,为每个订单创建一个子视图(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 列表视图效率非常低,它总是呈现整个子集。没有单元复用,啥都没有。在 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")]
复制代码


吐司面包选择器


讲座第 43 分钟开始讲解 Picker 控件及它与枚举一起使用的方法。首先是各种吐司选项的枚举:


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

吐司面包“最终版”应用

其实我们的结果和原版略有不同,也不是什么完整的版本。它看起来没那么完美,但毕竟这只是一个演示嘛


HTML 和 SemanticUI

SwiftWebUI 中的对应 UIViewRepresentable(https://developer.apple.com/documentation/swiftui/uiviewrepresentable)的等效组件负责发出原始 HTML。


这里提供了两种变体,HTML 按原样输出字符串,或者通过 HTML 转义内容:


struct MyHTMLView: View {  var body: some View {    VStack {      HTML("<blink>Blinken Lights</blink>")      HTML("42 > 1337", escape: true)    }  }}
复制代码


一般来说,你可以使用此原语构建任何 HTML。


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 是“反应性的”,即如果类、样式或属性发生变化(而不是重新呈现所有内容),它将发出常规 DOM 更改。

SemanticUI

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 的支持的(https://semantic-ui.com/elements/icon.html)。


还有 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!.")  }}
复制代码


呈现为:



添加此类视图非常简单,非常有趣。可以使用 SwiftUI 视图快速构建相当复杂和美观的布局。


Image.unsplash 使用 Unsplash API(http://source.unsplash.com)构建图像查询。只需输入一些参数,诸如图像尺寸和可选范围即可。


注意:有时 Unsplash 服务不怎么好用。

总结

上面就是我们的演示了,希望你能喜欢!但要再次重复免责声明:记住这是一个娱乐项目!可别把它用到生产环境里哦。它可以帮助你深入了解 SwiftUI 及其内部的工作机制。


我们认为它是一个很好的玩具,可能也是一个有价值的工具。


查看原文:http://www.alwaysrightinstitute.com/swiftwebui/


2019-07-15 19:4233674

评论

发布
暂无评论
发现更多内容

Parallels Desktop 18 for Mac (Pd18虚拟机) v18.3.2永久激活版

Mac相关知识分享

pd虚拟机

即时通讯哪个好?五大私有化即时通讯软件推荐

WorkPlus

《世界总体在变好,却还是有烂人》

充实的orzi

科大讯飞T30 Ultra和T20 Pro区别对比

妙龙

学习机 科大读飞

科大讯飞T30 UItra AI学习机 怎么样 值得买吗

妙龙

科大讯飞 学习机

蓝易云 - Ubuntu之apt-get系列--apt-get安装软件的方法/教程

百度搜索:蓝易云

蓝易云 - DB2 HADR+TSA运维,TSA添加资源组的命令

百度搜索:蓝易云

中文版谷歌访问助手 for Mac(谷歌浏览器插件)下载安装教程

理理

谷歌访问助手插件 谷歌浏览器扩展

Agisoft Metashape Professional for mac(三维建模重建软件)激活版

Mac相关知识分享

Steinberg Dorico Pro for Mac(乐谱编写软件) v5.1.51中文激活版

Mac相关知识分享

音乐制作软件 乐谱制作

HollySys PLC笔记 查看LE5109L的外观

万里无云万里天

PLC 工业控制 HollySys PLC

im即时通讯平台,WorkPlus稳定安全可靠的即时通讯服务

WorkPlus

即时通讯系统选型:如何为企业选择最佳的私有化即时通讯工具

WorkPlus

Elasticsearch 磁盘空间异常:一次成功的故障排除案例分享

极限实验室

elasticsearch easysearch

C ++ IDE智能代码编辑器:CLion 2023 (Win&Mac)激活版

你的猪会飞吗

CLion 2023 CLion 2024破解版 CLion激活码 CLion破解版

downie 4怎么下载?苹果mac专业的视频下载工具downie4下载安装 含集成版许可证

理理

Downie 4许可证 Downie 4 下载 Downie 4 Mac版 Downie 4视频下载器

汉化版Microsoft Remote Desktop微软远程桌面使用教程

理理

HollySys PLC笔记 安装AutoThink

万里无云万里天

PLC 工业控制 HollySys PLC

蓝易云 - postgresql-常用数学函数

百度搜索:蓝易云

蓝易云 - 跨境电商企业应该如何选择服务器?

百度搜索:蓝易云

【ACL2024】阿里云人工智能平台PAI多篇论文入选ACL2024

阿里云大数据AI技术

人工智能 阿里云 acl 论文 PAI

文献管理软件:EndNote X9 (Win&Mac) 特别版

你的猪会飞吗

Mac软件 mac破解软件下载

科大讯飞T30 UItra AI学习机和科大讯飞p30对比评测

妙龙

科大讯飞 学习机

科大讯飞AI学习机x3pro和科大讯飞T30 UItra对比评测

妙龙

科大讯飞 学习机

蓝易云 - Linux部署kettle并设置定时任务

百度搜索:蓝易云

科大讯飞t30ultra学习机和t20选哪个

妙龙

科大讯飞 学习机

科大讯飞T30 UItra 和科大讯飞S30学习机选哪个

妙龙

科大讯飞 学习机

photoshop 2021 滤镜如何使用?ps滤镜库下载及安装【永久使用】

理理

ps2021破解版 photoshop 2021 滤镜 neural filters逆天滤镜 ps照片滤镜 photoshop 2021 安装包

横扫鸿蒙弹窗乱象,SmartDialog出世

小呆呆666

flutter ios android 前端 HarmonyOS

科大讯飞学习机T30 UItra和科大讯飞学习机LUMIE10区别对比

妙龙

科大讯飞 学习机

【永久使用版】Parallels Desktop 18虚拟机 for mac下载激活教程

理理

Parallels Desktop 18 Mac虚拟机 PD18破解版 Parallels 永久使用版

SwiftWebUI框架:带你深入理解SwiftUI原理_语言 & 开发_Always Right Institute_InfoQ精选文章