产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

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:4233617

评论

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

极客时间架构师训练营 1 期 - 第 13 周总结

Kaven

架构师训练营 - 第十三周总结

一个节点

极客大学架构师训练营

使用 Docker 部署 canal,并将消息推送到 RabbitMQ

AlwaysBeta

MySQL Docker RabbitMQ canal

架构师训练营第一期第十三周总结

Leo乐

极客大学架构师训练营

架构师训练营第四周作业

zamkai

架构师训练营 week9 学习总结

花果山

极客大学架构师训练营

架构师训练营第十三周课后作业

Gosling

极客大学架构师训练营

海底光缆是如何铺设出来的?

第十三周作业

极客大学架构师训练营

架构师训练营 - 第十三周作业

一个节点

极客大学架构师训练营

架构师训练营 week9 课后作业

花果山

极客大学架构师训练营

分布式服务框架的选择-《企业IT架构转型之道-阿里巴巴中台战略思想与架构实战》

Man

分布式架构 中台架构

架构师训练营第一期第十三周作业

Leo乐

极客大学架构师训练营

盘点2020 | 带领团队学习成长,干货总结

架构精进之路

学习 盘点2020

架構師訓練營 week13 總結

ilake

第四周学习总结

简简单单

架构师训练营第 9 周学习总结

菜青虫

极客大学架构师训练营

LeetCode题解:18. 四数之和,双指针,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

第九周课后练习

晴空万里

极客大学架构师训练营

架构师训练营第13周总结

邓昀垚

极客时间架构师培训 1 期 - 第 13 周作业

Kaven

架構師訓練營 week13 作業

ilake

架构师训练营第 1 期 - 第十三周总结

Todd-Lee

极客大学架构师训练营

架构师训练营第13周作业

邓昀垚

架构师训练营第九周作业1

韩儿

架构师训练营第九周作业2

韩儿

秒杀活动要点分析

落朽

大数据 2 第十三周作业「架构师训练营第 1 期」

天天向善

第四周系统架构作业

简简单单

架构师训练营第十三周学习总结

Gosling

极客大学架构师训练营

架构师训练营第 9 周课后练习

菜青虫

极客大学架构师训练营

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