SwiftUI框架解析(一) —— 基于SwiftUI的一个简单示例(一)

版本记录

版本号 时间
V1.0 2019.06.14 星期五

前言

SwiftUI是2019年WWDC新推出的UI相关的API,相信大家都已经知道并想体验一下,下面我们就去一起了解和学习了。

开始

首先看下写作环境

Swift 5, iOS 13, Xcode 11

在这个SwiftUI教程中,您将学习如何通过声明和修改视图来布局UI,以及如何使用状态变量来更新UI。 您将使用Xcode的新预览和实时预览,体验保持代码和WYSIWYG布局同步的乐趣。

自Apple于2014年宣布推出Swift以来,SwiftUI是最激动人心的消息。这是迈向Apple实现每个人都可以编码,简化基础知识的目标迈出了一大步,因此您可以将更多时间花在满足用户需求的自定义功能上。一些开发人员开玩笑说他们可能被SwiftUI Sherlocked了!

SwiftUI允许您忽略Interface Builder(IB)storyboards,而无需编写详细的逐步说明来布置UI。 IBXcodeXcode 4之前是单独的应用程序,每次编辑IBActionIBOutlet的名称或从代码中删除它时,接缝仍会显示,并且您的应用程序崩溃,因为IB没有看到代码更改。或者你已经对你必须在你的代码中使用的segues或表格视图单元格的字符串类型标识符感兴趣,但Xcode无法检查你,因为它们是字符串。而且,虽然在WYSIWYG编辑器中设计新UI可能更快更容易,但在用代码编写时,复制或编辑UI会更有效率。

SwiftUI来了可以做好这点!您可以与其代码并排预览SwiftUI视图 - 对一侧的更改将更新另一侧,因此它们始终保持同步。没有任何标识符字符串出错。它是代码,但比你为UIKit编写的要少得多,因此它更容易理解,编辑和调试。

SwiftUI不会取代UIKit - 比如SwiftObjective-C,你可以在同一个应用程序中使用它们。您将无法在macOS上运行SwiftUI iOS应用程序 - 这就是Catalyst。但是,SwiftUI API在不同平台上是一致的,因此在多个平台上使用相同的源代码开发相同应用程序将更容易。

在本教程中,您将使用SwiftUIiOS Apprentice构建我们著名的BullsEye游戏的变体。您将学习如何通过声明和修改视图来布局UI,以及如何使用状态变量来更新UI。您将使用Xcode的一些新工具,尤其是预览和实时预览,并体验保持代码和WYSIWYG布局同步的乐趣。

注意:本教程假设您习惯使用Xcode开发iOS应用程序。 你需要Xcode 11 beta。 要查看SwiftUI预览,您需要macOS 10.15 beta。 因此,使用beta版软件也需要一定的舒适度。

如果您没有备用Mac,则可以 install Catalina beta on a separate APFS volume

下载项目,并在RGBullsEye-Starter文件夹中构建并运行UIKit应用程序。 此游戏使用三个滑块sliders - RGB颜色空间中的红色,绿色和蓝色值 - 以匹配目标颜色。

我为RWDevCon 2016编写了这个应用程序,并在本教程中将代码更新为Swift 5。 它运行在Xcode 10Xcode 11 beta中。 在本教程中,您将使用SwiftUI创建此游戏的基本版本。

Xcode 11 beta中,创建一个新的Xcode项目(Shift-Command-N),选择iOS▸Single View App,将项目命名为RGBullsEye,然后选中Use SwiftUI复选框:

将项目保存在RGBullsEye-Starter文件夹之外的某个位置。


Entering the New World of SwiftUI

在项目导航器中,打开RGBullsEye组以查看您的内容:旧的AppDelegate.swift现在拆分为AppDelegate.swiftSceneDelegate.swiftSceneDelegate具有window

SceneDelegate不是特定于SwiftUI,但这一行是:

window.rootViewController = UIHostingController(rootView: ContentView())

UIHostingControllerSwiftUI视图ContentView创建一个视图控制器。

注意:UIHostingController使您可以将SwiftUI视图集成到现有应用程序中。 您可以在storyboard中添加一个Hosting View Controller,并从UIViewController创建一个segue。 然后按住Ctrl键从segue拖动到视图控制器代码中以创建IBSegueAction,您可以在其中指定hosting controllerrootView值 - SwiftUI视图。

当应用程序启动时,window会显示ContentView的一个实例,该实例在ContentView.swift中定义。 它是一个符合View协议的struct

struct ContentView: View {
  var body: some View {
    Text("Hello World")
  }
}

这是SwiftUI声明ContentView的主体body包含显示Hello WorldText视图。

DEBUG块中,ContentView_Previews生成一个包含ContentView实例的视图。

#if DEBUG
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
#endif

您可以在此处为预览指定样本数据。 但是这个预览在哪里好像听过?

代码旁边有一个很大的空白区域,位于顶部:

单击Resume,稍等片刻以查看预览:

注意:如果没有看到Resume按钮,请单击editor options按钮,然后选择Editor and Canvas

如果您仍然没有看到Resume按钮,请确保您正在运行macOS 10.15 beta


Outlining Your UI

您没有看到的一个文件是Main.storyboard - 您将在SwiftUI代码中创建您的UI,密切关注预览以查看它的外观。 但不要担心 - 您不会编写数百行代码来创建视图!

SwiftUI是声明性的:您声明了UI的外观,SwiftUI将您的声明转换为可以完成工作的高效代码。 Apple鼓励您根据需要创建任意数量的视图,以使代码易于阅读和维护。 特别推荐使用可重用的参数化视图 - 就像将代码提取到函数中一样,您将在本教程的后面创建一个。

RGBullsEye的UI有很多子视图,所以你首先要创建一个outline,使用Text视图作为占位符。

首先用以下代码替换Text(“Hello World”)

Text("Target Color Block")

如有必要,请单击Resume以刷新预览。

现在Command-Click预览中的Text视图,然后选择Embed in HStack

请注意您的代码更新以匹配:

HStack {
  Text("Target Color Block")
}

你在这里使用horizontal stack是因为你想要显示目标并且并排猜测颜色块。

复制并粘贴Text语句,然后对其进行编辑,使HStack如下所示。 请注意,您不要使用逗号分隔两个语句 - 只需在各自的行中写入:

HStack {
  Text("Target Color Block")
  Text("Guess Color Block")
}

它在预览中:

现在准备通过在VStack中嵌入HStack来添加滑块占位符 - 这次,在代码中按命令单击HStack

选择Embed in VStack,出现新代码,但预览不会更改 - 您需要在颜色块下面添加视图。

HStack闭包下面打开一个新行,单击工具栏中的+按钮打开Library,然后将Vertical Stack拖到新行中:

正如您所期望的那样,代码和预览更新:

注意:迷你地图不会出现在我的屏幕截图中,因为我隐藏了它:Editor ▸ Hide Minimap

现在完成大纲,看起来像这样:

VStack {
  HStack {
    Text("Target Color Block")
    Text("Guess Color Block")
  }

  Text("Hit me button")

  VStack {
    Text("Red slider")
    Text("Green slider")
    Text("Blue slider")
  }
}

新的VStack将包含三个滑块sliders,颜色块和滑块之间会有一个按钮。

注意:Xcode 11是测试版软件,在编写本教程时,很难弄清楚我做错了什么。它有一些无用的错误消息,包括Unable to infer complex closure return type; add explicit type to disambiguate, ‘(LocalizedStringKey) -> Text’ is not convertible to ‘(LocalizedStringKey, String?, Bundle?, StaticString?) -> Text’ and Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type, which basically meant “huh??”。这些消息很少出现在我刚刚添加的代码附近。如果您看到的消息似乎没有帮助,或者指向以前完全正常的代码,请尝试注释掉刚才添加的内容。还要检查拼写并确保您的大括号和括号匹配。最后,在beta期间重新启动Xcode总是一个好主意!


Filling in Your Outline

现在,练习你的新SwiftUI-fu技能,开始填充HStack颜色块,所以它看起来像这样:

HStack {
  // Target color block
  VStack {
    Rectangle()
    Text("Match this color")
  }
  // Guess color block
  VStack {
    Rectangle()
    HStack {
      Text("R: xxx")
      Text("G: xxx")
      Text("B: xxx")
    }
  }
}

每个颜色块都有一个Rectangle。 目标颜色块在其Rectangle下方有一个Text视图,而guess color block有三个Text视图 - 在本教程的后面,您将替换每个xxx以显示当前滑块值。

不要担心黑色矩形占据场景 - 它们会为滑块腾出空间,你现在就可以设置它们的前景颜色了。


Using @State Variables

您可以在SwiftUI中使用normal常量和变量,但是如果UI在其值发生更改时应该更新,则将变量指定为@State变量。 这个游戏都是关于颜色的,所以影响guess rectangle颜色的所有变量都是@State变量。

struct ContentView的顶部,在body闭包上方添加这些行:

let rTarget = Double.random(in: 0..<1)
let gTarget = Double.random(in: 0..<1)
let bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double

R,G和B值介于0和1之间。将目标值初始化为随机值。 您也可以将猜测值初始化为0.5,但如果您没有初始化某些变量,我会将它们保留为未初始化以显示您必须执行的操作。

向下滚动到DEBUG块,该块实例化要在预览中显示的ContentView。 初始化程序现在需要猜测值的参数值。 将ContentView()更改为:

ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)

创建滑块时,它们将以居中显示在预览中。

您还必须修改SceneDelegate中的初始值,在scene(_:willConnectTo:options:),将ContentView()替换成下面这行:

window.rootViewController = UIHostingController(rootView:
  ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5))

当应用加载其根场景时,滑块将居中。

现在将前景色修改器添加到目标Rectangle

Rectangle()
  .foregroundColor(Color(red: rTarget, green: gTarget, blue: bTarget, opacity: 1.0))

修改器.foregroundColor创建一个新的Rectangle视图,现在使用由随机生成的RGB值指定的前景颜色。

同样,修改guess Rectangle

Rectangle()
  .foregroundColor(Color(red: rGuess, green: gGuess, blue: bGuess, opacity: 1.0))

当R,G和B值均为0.5时,您会得到灰色。

单击Resume,稍等片刻以进行预览更新。

注意:预览会定期刷新,以及单击Resume或“实时预览”按钮时,所以不要惊讶地看到目标颜色经常变化。


Making Reusable Views

我向几个人展示了这个游戏,他们发现它很容易让人上瘾 - 特别是平面设计师。 然后他们会要求我实现其他颜色空间之一,比如YUV。 但RGB是本教程的不错选择,因为滑块基本相同,因此您将定义一个滑块视图,然后将其重用于其他两个滑块。

首先,假装你没有考虑重用,只需创建红色滑块。 在滑块VStack中,用这个HStack替换Text(“Red slider”)占位符:

HStack {
  Text("0").color(.red)
  Slider(value: $rGuess, from: 0.0, through: 1.0)
  Text("255").color(.red)
}

您可以修改Text视图以将文本颜色更改为红色。 并且您使用值 - thumb的位置 - 在fromthrough值之间的范围内初始化Slider

但是什么是$? 你刚刚适应了吗 对于期权,现在是$

对于这样一个小符号来说,它实际上非常酷而且非常强大。rGuess本身就是值 - 只读。 $ rGuess是一个读写绑定 - 你需要它,在用户更改滑块的值时更新猜测矩形的前景色。

要查看差异,请在猜测矩形下方的三个Text视图中设置值:

HStack {
  Text("R: \(Int(rGuess * 255.0))")
  Text("G: \(Int(gGuess * 255.0))")
  Text("B: \(Int(bGuess * 255.0))")
}

在这里,您只使用值而不是更改它们,因此您不需要$前缀。

注意:您和我知道滑块从0变为1,但255结束标签和0到255 RGB值适用于您的用户,他们可能会觉得更喜欢在0到255之间的RGB值,如十六进制颜色的表示。

等待预览刷新,看到你的第一个滑块:

为了腾出空间,颜色块略有缩小,但是滑块看起来仍然有点局促 - 末端标签太靠近窗口边缘 - 所以在HStack中添加一些padding(另一个修改器):

HStack {
  Text("0").color(.red)
  Slider(value: $rGuess, from: 0.0, through: 1.0)
  Text("255").color(.red)
}.padding()

这就好多了

现在,如果您要复制粘贴编辑此HStack以创建绿色滑块,则将.red更改为.green,将$ rGuess更改为$ gGuess。 这就是参数的所在。

Command-Click红色滑块HStack,然后选择Extract Subview

这与Refactor ▸ Extract to Function的工作方式相同,但适用于SwiftUI视图。

不要担心出现的所有错误消息 - 当您编辑完新的子视图后,它们会消失。

命名ExtractedView ColorSlider,然后在body闭合之前在顶部添加这些行:

@Binding var value: Double
var textColor: Color

然后用$ value替换$ rGuess,用textColor替换.red

Text("0").color(textColor)
Slider(value: $value, from: 0.0, through: 1.0)
Text("255").color(textColor)

然后返回到VStack中对ColorSlider()的调用,并添加您的参数:

ColorSlider(value: $rGuess, textColor: .red)

检查预览是否显示红色滑块,然后复制粘贴编辑此行以将Text占位符替换为其他两个滑块:

ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)

单击Resume以更新预览:

注意:您可能已经注意到您经常单击Resume。 如果您不想从键盘上把手拿开,Option-Command-P将是您学习的最有用的键盘快捷键之一!

这就是整个应用程序的完成!

现在有趣的事情:在预览设备的右下角,点击实时预览(live preview)按钮:

实时预览可让您与预览进行交互,就像您的应用程序在模拟器中运行一样 - 太棒了!

等待预览微调器(Preview spinner)停止,如有必要,请单击Try Again

现在移动那些滑块以匹配颜色!

精彩! 您是否喜欢Goodnight Developers video from the WWDC Keynote中的程序员? 太令人满意了!


Presenting an Alert

在使用滑块获得良好的颜色匹配后,您的用户可以点击Hit Me按钮,就像在原始的BullsEye游戏中一样。 就像在BullsEye中一样,会出现一个Alert,显示分数。

首先,向ContentView添加一个方法来计算分数。 在@State变量和body之间,添加以下方法:

func computeScore() -> Int {
  let rDiff = rGuess - rTarget
  let gDiff = gGuess - gTarget
  let bDiff = bGuess - bTarget
  let diff = sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff)
  return Int((1.0 - diff) * 100.0 + 0.5)
}

diff值只是三维空间中两点之间的距离 - 用户的错误。 要获得分数,请从1减去diff,然后将其缩放到100以外的值。较小的diff会产生较高的分数。

接下来,使用Button视图替换Text(“Hit me button”)占位符:

Button(action: {

}) {
  Text("Hit Me!")
}

Button有一个动作和一个标签,就像一个UIButton。 您希望发生的操作是提供Alert视图。 但是如果你只是在Button动作中创建一个Alert,它将不会做任何事情。

而是将Alert创建为ContentView的子视图之一,并添加Bool类型的@State变量。 然后,在希望显示Alert时将此变量的值设置为true - 在这种情况下,在Button操作中。 当用户移除Alert时,该值将重置为false

所以添加这个@State变量,初始化为false

@State var showAlert = false

然后将此行添加为Button操作:

self.showAlert = true

你需要self,因为showAlert在一个闭包内。

最后,向Button添加一个presentation修饰符,这样你的Button视图如下所示:

Button(action: {
  self.showAlert = true
}) {
  Text("Hit Me!")
}
.presentation($showAlert) {
  Alert(title: Text("Your Score"), message: Text("\(computeScore())"))
}

您传递绑定$ showAlert,因为当用户移除alert时,其值将更改。

SwiftUI具有用于Alert视图的简单初始化器,就像许多开发人员在UIAlertViewController扩展中为自己创建的初始化器一样。 这个有一个默认的OK按钮,所以你甚至不需要将它作为参数包含在内。

关闭live preview,单击Resume以刷新预览,然后启用实时预览live preview,并尝试匹配目标颜色:

嘿,当你有实时预览时,谁需要iOS模拟器? 尽管如果您在模拟器中运行应用程序,可以将其旋转为横向:

本教程几乎没有涉及SwiftUI的表面,但您现在已经了解了如何使用一些新的Xcode工具来布局和预览视图,以及如何使用@State变量来更新UI。更不用说那个惊人的Alert了!

您现在已做好充分准备,深入了解Apple的丰富资源 - 其教程和WWDC会议。教程tutorials和WWDC会议通过不同的示例项目。例如,Introducing SwiftUI#204)为会议构建了一个列表应用程序 - 它比教程的Landmarks应用程序更简单。 SwiftUI Essentials#216)向您展示了如何使用Form容器视图轻松获取iOS表单的外观。

为了简化本教程,我没有为RGB颜色创建数据模型。但大多数应用程序将其数据建模为结构体或类。如果需要SwiftUI来跟踪模型实例中的更改,则必须通过实现发布更改事件的didChange属性来符合BindableObject。看看Apple的示例项目,特别是Data Flow Through SwiftUI#226

为了使自己更容易进入SwiftUI,您可以将SwiftUI视图添加到现有应用程序,或者在新的SwiftUI应用程序中使用现有视图 - 观看Integrating SwiftUI#231)以查看这样做的快捷方式。

另外,浏览SwiftUI documentation以查看其他可用内容 - 有很多内容!

后记

本篇主要讲述了SwiftUI相关,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容