阅读 5870

SwiftUI or Flutter ?

前言

相信每一个见到 SwiftUI 的开发者,都会立刻将这门船新的 UI 框架和 Flutter 联系到一起。是的,它们身上有太多太多相似的地方,相似的声明语法、实时热更新、跨平台(SwiftUI 仅仅跨 Apple 平台)等等,让羡慕了前端技术爆发的移动开发圈子也热闹了一回。那么 SwiftUI 和 Flutter 到底有什么相似和不同?它们又各有什么优缺点?以及最后,单就技术方向而言,谁才是未来跨平台方案的赢家呢?

语言

现代计算机语言越发趋于相似是一个不争的事实,因为每个语言的基本目标都相当的一致:简洁、灵活、安全、高性能。同时,各种语言的优异特性也都在被相互借鉴,更近一步减少了各个语言之间的鸿沟。

Swift 和 Dart 分别作为这两个 UI 框架的唯一官方语言,在语法层面的差别其实非常小,网上也有大量的文章来对比这两种语言的优劣性。我的使用体验和大部分人相似,就目前而言,Swift 和 Dart 各有优劣。

  1. Swift 比 Dart 更加简洁。Swift 本身在语法层面已经比 Dart 要简洁很多,比如无需在句末添加;分号等。这一点在直接编写 SwiftUI 和 Flutter 上会显得尤为明显。不过这个问题并不全是语言层面带来的问题,也跟这两个 UI 框架的设计有关。

    ForEach(userData.landmarks) { landmark in
    	NavigationButton( destination: LandmarkDetail(landmark: landmark)) {
        LandmarkRow(landmark: landmark)
      }
    }
    复制代码
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          final landmark = landmarks[index];
          return LandmarkCell(
            landmark: landmark,
            onTap: () {
              Navigator.push(
                context,
                CupertinoPageRoute(
                  builder: (context) => LandmarkDetail(
                    landmark: landmark,
                  ),
                ),
              );
            },
          );
        },
        childCount: landmarks.length,
      ),
    ),
    复制代码

    两者都实现了一个可以点击的列表页,都调用了一个自定义 Cell 。

  2. Swift 比 Dart 更加严格,从某种角度上讲会更加安全,当然安全这种事情追根揭底还是得靠人。

  3. Swift 在发布的第5个年头竟然还没有 await/async ,虽然早有提案但一直没有落实下来。不过 Dart 这边也没有可选型。只能说两者互相吸收的还不够。

  4. Dart 的黑科技:同时支持 AOT 和 JIT,这也赋予了 Flutter Web 级的热重载特性,是 Flutter 对决 SwiftUI 的有力武器。

  5. Swift 和 Dart 都是开源语言。Apple 对 Swift 的开放是史无前例的,目前 Swift 已经可以脱离 macOS 运行,同时在社区中也出现了诸如后端框架 Vapor 等一系列有意思的东西,更是成为了 Tensorflow 的官方语言之一。这一切大大拓展了 Swift 的使用场景。不过,Dart 在可玩性上明显走的要比 Swift 更快。 用其开发的后端框架已经被一些公司投入到了生产环境,Google 的 Web 框架 AngularDart(非 Flutter Web)也早已在一些服务上稳定运行了许久。更不用说借助 Flutter 跑遍 iOS Android macOS Windows Web,简直可以用所向睥睨来形容。

一个语言的设计好坏的确会影响一个框架的受欢迎程度,我已经听过无数 Flutter 开发人员吐槽 Dark 无止境的嵌套问题。虽然 Dark 的可玩度更高,但就单 UI 框架而言, 我个人认为 Swift 的使用感受要明显好于 Dart。

语法

SwiftUI 和 Flutter 都无一例外的使用了声明式语法和各自的 DSL 来描述 UI ,它的好处是,你能一眼就从代码的结构中看出实际 UI 的结构,并且避免反复的逻辑代码。SwiftUI 用数据绑定和状态管理替代了原有的复杂控制逻辑,而 DSL 又使得代码在结构上和 UI 的层级结构高度一致。

VStack {
    MapView()
        .edgesIgnoringSafeArea(.top)
        .frame(height: 300)

    CircleImage()
        .offset(y: -130)
        .padding(.bottom, -130)

    VStack(alignment: .leading) {
        Text("Turtle Rock")
            .font(.title)
        HStack(alignment: .top) {
            Text("Joshua Tree National Park")
                .font(.subheadline)
            Spacer()
            Text("California")
                .font(.subheadline)
        }
    }
    .padding()

    Spacer()
}
复制代码

上面是 WWDC 上出现的一个 SwiftUI Demo LandMark。即便你没有学习过 SwiftUI 的语法,稍稍阅读一下上面的代码,你也能发现,它和图片中的 UI 是一一对应的。这就是声明式语法的优点。不过在具体写代码的时候,Flutter 和 Swift 的感受就差太多了,最明显的就是:Flutter 要比 SwiftUI 复杂太多了。上面的效果用 Flutter 实现要写多少代码呢?多到我都没法粘贴过来,否则我的文章有一半都会是 Dart 代码。不过这里有一个人仿照着 Demo 写了一份 Flutter 版的 LandMarks ,大家可以参考一下。

为什么同样是声明式语法,Fluter 要比 SwiftUI 复杂这么多呢。这其实体现了 Apple 和 Google 在设计这两个 UI 框架时的不同思路,甚至代表了这两家公司的不同文化。对 iOS 和 Android 稍有了解的开发者都知道,iOS 开发要比 Android 开发轻松很多。单就配置开发环境来说,iOS 就可以比 Android 提早1天开始写代码。更不容说复杂的框架、混乱的平台兼容性、生涩难懂的文档。

Apple 对待开发者,就像是在对待新手一样,极尽所能的去简化开发过程,减少开发中的工作量,甚至还要让开发变成一件优雅的事情,这从 Xcode 的外观功能设计到系统 API 再到 Swift 语言都是如此。而 Google 在对待开发者时,更像是对待极客一样,将大量的工作和创作交给开发人员,让它们自由的创造有意思的东西。但相应的,它的门槛也很高,而且自由带来的碎片化和不可控性也会导致开发效率的降低。

在设计 SwiftUI 的 API 时,Apple 隐藏了大量的内部细节,让开发者调用尽可能少的代码就能实现相应的效果。开发者不需要重写初始化方法,不需要使用 return,不需要要关心 context 上下文,也不需要关心它应该是一个 StatefulWidget 还是 StatelessWidget, 更不需要关心用的是 Material Design 还是 iOS Style。能做的 Apple 都帮你在内部做好了,你需要做的,只是告诉编译器,你需要什么控件,它们是什么样的,它们应该在什么地方,仅此而已。如果说 Flutter 给移动开发带来的声明式语法让所有人眼前一亮的话,那么 SwiftUI 的出现则是告诉所有人(包括 Flutter ),什么才是真正的声明式语法。

实时更新

SwiftUI 最为重磅的功能,就是万众期待的热加载,也就是实时更新。Web 端超高的开发效率一直是移动端梦寐以求的技能,RN 的出现让所有移动开发者第一次看到了这种可能,虽然它如今也有着自己的问题。Flutter 作为 Web 技术的一种延伸,继承了大量的 Web 技术特性,热加载似乎也就成为了手到擒来的技能。而 SwiftUI 要想实现近似 Web 那样的实时刷新,所要面对的困难要复杂的多。从我的实际体验下来,也充分说明了这一点。

SwiftUI 的实时预览分为静态预览和动态预览。默认情况下展示的是静态预览,它的速度快,而且支持代码和可视化两种编写方式。不过它没有任何响应事件,无法滚动和跳转页面。如果需要动态调试,则需要切换到动态预览。动态预览需要经过一段编译时间,然后可以以完全动态的形式实时响应 UI 的变化,而且可以在真机上实时调试。但动态预览的限制还比较多,无法做到 Flutter 那种只需编译一次,之后整个 App 都能实现动态更新。

为什么会有静态预览和动态预览呢?我相信如果不是出于一些问题的考虑(性能问题、难以处理的 Bug),Apple 是断然不会将这个问题复杂化的。只能说,目前残疾版的热加载是 Apple 能给我们的最好效果。正因此,目前 SwiftUI 所谓的实时刷新,距离 Web 以及 Flutter,还是小儿科。静态 UI 调试其实完全可以通过 StoryBoard 来实现,而真正实用的动态预览又有一定的局限性。不过 SwiftUI 尚处在测试阶段,暂时还无法对其性能作出最终的定论。

性能

Google 宣称 Flutter 的目标之一就是流畅(60Hz),目前来看,它确实在大部分场景做到了。不过根据我的测试(此时尚处于 beta 版),像页面转场、控件动画这样的场景,跑到 60 Hz 还是有一定压力。我一直都有在关注 Flutter 项目,我们内部也在尝试将部分模块使用 Flutter 重写。但我至今体验下来的情况,目前的 Flutter, 只能说很接近原生平台的流畅度。不过即便 Flutter 能够达到系统原生的流畅度,它也始终无法回避性能损耗的问题。iOS 的原生 UI 框架(包括 SwiftUI)都是搭建在 Metal 之上的,Metal 对于 GPU 的使用效率远要高于 OpenGL 。其带来的结果在我的测试中也表明了,同样的页面渲染和动画,Flutter 对于 CPU 和 GPU 的利用率要高于 iOS 原生 UI 框架。而高性能消耗,对设备就意味着高发热和低续航。这在移动设备上尤为严重。

反观 SwiftIU,Apple 似乎照搬了系统所有的原生控件,原则上不会有性能上的改观,但千万不要被你的眼睛骗了,Apple 在你看不见的地方,下了一盘大棋。

上面两张图分别是 SwiftUI 和 iOS原生渲染的 Label,它们都运行在 iOS 平台,都长得一摸一样,但它们背后是两个完全不一样的东西。我们发现,SwiftUI 中的文字已经不是 iOS 系统的原生控件了,而是一个新的类型,名为 Text。它其实是一个继承自 UIView 的新控件,全名是 DisplayList.ViewUpdater.Platform.CGDrawingView 。观察右侧的属性窗口,发现两个控件有着完全不同的属性。UILabel 有着复杂的属性,用于控制样式、交互等。而 Text 则要精简的多的多。

有传言说,SwiftUI 的部分控件已经抛弃了系统原生 UI 框架,转而直接使用了更加底层,同时也在 Apple 生态系统中更加通用的 Core Animation、Core Graphics、Core Text 进行渲染。是否直接使用了底层框架进行高效渲染我暂时还无得而知,但目前可以肯定的是,Apple 正在将 SwiftUI 剥离出原有的 UI 框架,丢掉沉重的历史包袱。这势必会带来诸多益处,比如更高的自由度、更好的性能,以及真正意义上的:跨平台。

跨平台?

可能很多人都会说,SwiftUI 和 Flutter 根本没法一起比较,SwiftUI 不是真正的跨平台。没错,SwiftUI 目前所跨的仅仅是 Apple 自家的生态圈,包含了 iOS(iPadOS)macOS watchOS tvOS,而隔壁 Flutter 早已玩腻了移动端,开始向 Windows macOS 以及 Web进发。不过,你不可否认,SwiftUI 确实做到了在完全不同的系统平台上实现了 Write once,use anywhere。而且,隐藏在这背后的思路还完全不一样。

SwiftUI 从根本上不是在构建 UI,而是在描述 UI 。这两者有什么区别?构建 UI 时,你会明确每一个控件的类型,甚至精确到平台。比如在原生 UI 框架中,你需要一个输入框 ,你就要根据不同的平台,选择具体是使用 UITextFiled(iOS) 还是 NSTextFiled(macOS),而这两个输入框 有着不同的属性、外观和特性。在 Flutter 中,你不需要考虑不同平台的控件类型,但你需要考虑控件的风格,官方提供了符合 Material Design 的 Android 控件和 iOS Style 的 iOS 控件,它们的许多特性也不一致,这一切给 UI 构建带来了更加复杂的逻辑和工作量。

而 SwiftUI 更像是 Web 的模版,它只描述 UI ,而控件具体长什么样子,有什么特性,会根据编译的平台因地制宜的实现,而且是自动的。这就像是给 Web 套上了一个模版,一个主题。

左侧的代码描述了一个表单,其中包含了一些简单的控件,包括PickerToggleStepperButton。如果我们把代码原封不动的放到 macOS 项目中,它会变成下面这个样子。

你会发现,同样的代码,展示出来的 UI 却完全不一样,但 UI 表达的内容确实完全一致的。更令人细思极恐的是,Picker 这个"单项列表选择" 控件,在iOS上被翻译成了一个选择页面,点击 cell 会 push 出所有可选择的选项,点击选项会返回上一页面并将选中的内容展示到对应的 cell 上。而在 macOS 上,Picker 则会被翻译成我们桌面操作系统上常见的下拉列表。这种转变是不可思议的,我只能用 Wow、Awesome、Amazing 来形容它。因为它完全符合平台设计语言和用户使用习惯的,同时又极大的降低了开发和适配难度。然而 SwiftUI 能做的还远远不止这些。

没错,不仅仅是 iOS 和 macOS,在 tvOS 和 watchOS 上,SwiftUI 也将以最符合平台设计语言和交互的方式去展示每一个控件。而且,你还将自动获得大量的系统特性,比如 Dynamic Type、Dark Model、阅读方向自适应等。

从这一点上来看,SwiftUI 和 React Native 有着更为相似的思路和技术。只不过 Facebook 无论对 iOS 还是 Android 都几乎没有任何话语权,因此在兼容性、一致性上都有一定问题。而为了达到这种兼容性,不仅 React Native 自身需要做大量的工作,开发人员也需要疲于应对各种问题,可以说是一种比较尴尬的中间技术。

而 SwiftUI 和 React Native 与 Flutter 的跨 UI 平台思想有着本质的不同。Flutter 完全抛弃了运行平台的原生 UI 框架,类似于在一张画布上一个像素一个像素"画"出每一个控件,类似于一个 2D 游戏引擎。它的目的也很明确:确保相同的代码在不同的操作系统、硬件设备、屏幕尺寸下展示完全相同的 UI 。这似乎是开发者想要的,因为我们受够了 RN 在不同平台上表现出的不可控差异。但即便抛去了不同操作系统设计语言的差异,不同尺寸屏幕下的 UI 至少应该是很不一样的。桌面平台 UI 和移动平台 UI,甚至手表上的 UI,应该根据用户的使用习惯和操控方式,制定完全不同的 UI 和交互。如果要在 Flutter 上实现多平台的适配,结果对任何一个程序员而言都可能是一场噩梦。你需要做大量的判断,并且可能需要使用多种控件来实现单一功能,最终还要面对大量的测试和 Bug。

不过有利就有弊。SwiftUI 近乎完美的跨平台方案是建立在较低自由度下的。通过上面的例子你也发现了,Demo 中使用的都是系统原生的控件样式。这并不是说我们无法自定义,只不过:

  1. 我们能够自定义的项目不够多
  2. 我们自定义的内容越多,系统为我们自动添加的特性失去的也就越多

比如我们想要修改列表背景的颜色,那么我们将会失去部分系统自动为我们添加的 Dark Model 特性,你需要做一些额外的工作来告诉 App 在黑/白模式下背景应该展示什么颜色。

但好在 Apple 已经开始尝试脱离系统原有的 UI 框架,这反而让我们更加方便自定义部分 UI 。以往我们要实现下面这个 Button 的样式,需要对 UIButton 的参数进行大量调整,因为默认的 UIButton 是文字在左,图片在右。如今,SwiftUI 脱离了系统 UI 框架组建后,我们可以通过极其简单的代码实现复杂的 UI 样式。

不过这和 Flutter 引以为傲的 "精确到像素" 还有一定差距,我们可能无法像 Flutter 那样完全掌控 UI 控件的所有细节。但就像画画一样,你失去了一定的自由,换来的是更快更高效的开发,这恰恰体现了 Apple 的企业风格。

然而 SwiftUI 跨平台的故事讲完了吗?我认为这恰恰只是个开始。SwiftUI 这个完全与平台、设备无关的,纯描述的 UI 框架,恰恰才是跨平台方案的正确方向。SwiftUI 能够用来描述 iOS macOS tvOS 甚至 watchOS 的 UI,为什么不能用来描述 Android,或者 Web?SwiftUI 已经拥有了 Flex 布局、combine 数据绑定、热加载等一系列特性,我们只需要照着这个思路,将 Android 或者 Web 的模版套用在 SwiftUI 上,就能同样得到符合平台设计语言和规范的 UI 。当然,这远没有我说的那么简单,但是不要忘了,Swift 是开源的,跨平台运行也不是问题。只要条件存在,一切皆有实现的可能。

然而到那时,Flutter 又会变成什么样子呢?至少在目前,Flutter 还是会成为大部分公司跨平台的首要方案之一,而 SwiftUI 嘛,大面积使用至少也是2~3年之后的事咯。

关注下面的标签,发现更多相似文章
评论