瑜伽教程: 一个跨平台的布局引擎

原文:Yoga Tutorial: Using a Cross-Platform Layout Engine
作者:Christine Abernathy
译者:kmyhy

瑜伽(Yoga) 是一个跨平台的布局引擎,基于 Flexbox,它让布局变得更简单。可以用它替代 iOS 的自动布局和 web 的 CSS,也可以将它当成一种通用的布局系统使用。

Yoga 最初源自 Facebook 在 2014 年的一个开源的 css 布局开源库,在 2016 年经过修改,更名为 Yoga。Yoga 支持多个平台,包括 Java、C#、C 和 Swift。

框架开发者可以将 Yoga 集成到自己的布局系统中,就像 Facebook 在开源的 React Native 和 Litho 中所做的一样。当然,Yoga 也开放李一个框架,允许 iOS 开发者直接用于对视图进行布局。

在本教程中,你会用到主要的 Yoga 概念,并在构建 FlexAndChill app 中实践和扩展它们。

尽管使用的是 Yoga 布局引擎,但在阅读本文之前,熟悉自动布局仍然是有帮助的。你还需要了解如何在项目中使用 CocoaPods 来引入 Yoga。

理解弹性盒子

弹性盒子,也叫做 CSS 弹性盒子,用于解决 web 中的复杂布局问题。这种布局的一个重要特性就是内容的布局在某个方向上填充,同时大小会“弹性地”适应某个空间。

弹性盒子由多个弹性容器组成,每个容器都有 1 个或多个 flex item:

瑜伽教程: 一个跨平台的布局引擎_第1张图片

弹性盒子定义了当 flex item 在弹性容器中如何布局。弹性容器之外的内容和 flex item 之内的内容照常渲染。

容器中的 flex item 在单一方向上进行布局(当然容器还可以再次被装在容器中)。这个方向就是 item 的主轴。而另外一个方向则是交叉轴。

瑜伽教程: 一个跨平台的布局引擎_第2张图片

弹性盒子允许你指定 item 如何放置以及在主轴和交叉轴上的空间分布。justify-content 用于指定 item 在容器主轴方向上的对齐。下图显示了当容器主轴方向为行的时候 item 的位置:

瑜伽教程: 一个跨平台的布局引擎_第3张图片

  • flex-start: item 沿容器的开头放置。
  • flex-end: item 沿容器的结尾开始放置。
  • center: item 放在容器的中间。
  • space-between: item 等间距分布,第一个 item 方在开始位置,最后一个 item 放在结尾位置
  • space-around: item 等间距分布,并在它们的四周也等间距。

align-items 指定 item 在容器交叉轴方向上的对齐方式。下图显示了当容器的主轴为行时,也就是交叉轴为垂直方向时,item 的分布:

item 分别对齐于容器的上、中、下。

这些属性帮你对弹性盒子有一个大致的认识。你还会用到其它属性。有的属性控制了 item 相对于容器空间的拉伸和压缩。其它属性还包括设置边距、内白和大小。

弹性盒子的例子

要测试弹性盒子的一个绝佳地方是 jsFiddle,一个在线测试 JavaScript、HTML 和 CSS 的地方。

进入 starter JSFiddle,你应该看到 4 个面板:

瑜伽教程: 一个跨平台的布局引擎_第4张图片

在 3 个编辑器中的代码会在右下角的面板中看到结果。这个 starter 的例子会显示一个白色的框。

注意在 CSS 编辑器中定义的 yoga 类选择器。这个定义了 yoga 类的 CSS 实现。某些值和 Flexbox w3 规范不一致。例如,Yoga 指定 flex direction 默认为 column,items 从容器的开始位置摆放。任何使用了 class=”yoga” 的 HTML 元素都会进入 Yoga 模式。

看一下 HTML 代码:

<div class="yoga" 
  style="width: 400px; height: 100px; background-color: white; flex-direction:row;">
div>

这个 div 的基本样式是 yoga。另外 style 属性还设置了 size、background 属性并覆盖了默认的 flex direction 属性,因此 item 会排成行。

在 HTML 编辑器中,在关闭的 div 标签前添加如下代码:

<div class="yoga" style="background-color: #cc0000; width: 80px;">div>

在 div 容器上添加了一个 yoga 样式,且宽 80 的红色方块。

点击顶部菜单的 Run 按钮。你会看到如下结果:

瑜伽教程: 一个跨平台的布局引擎_第5张图片

在根 div 下新增子元素,就在红色方块的后面:

<div class="yoga" style="background-color: #0000cc; width: 80px;">div>

这会再添加一个 80 像素宽的蓝色方块。

点击 Run。输出结果会在红色方块右边在加上一个蓝色方块:

瑜伽教程: 一个跨平台的布局引擎_第6张图片

将蓝色方块的 div 代码修改成:

<div class="yoga" style="background-color: #0000cc; width: 80px; flex-grow: 1;">div>

添加了一个 flex-grow 属性,这样方块会拉伸并填充剩下的空间。

点击 Run,看看修改后结果如何:

瑜伽教程: 一个跨平台的布局引擎_第7张图片

将整个 HTML 代码修改成:

<div class="yoga" 
  style="width: 400px; height: 100px; background-color: white; flex-direction:row; padding: 10px;">
  <div class="yoga" style="background-color: #cc0000; width: 80px; margin-right: 10px;">div>
  <div class="yoga" style="background-color: #0000cc; width: 80px; flex-grow: 1; height: 25px; align-self: center;">div>
div>

这在子 item 上添加了一个 padding,在红色方块右边添加了一个右边距,为蓝色方块设置了高度,然后指定蓝色方块居中对齐于容器。

点 Run 查看效果如下:

瑜伽教程: 一个跨平台的布局引擎_第8张图片

你可以查看最终的jsFiddle。你可以试一下其它布局属性和值。

Yoga vs. 弹性盒子

虽然 Yoga 是基于弹性盒子的,但二者仍有一些不同。

Yoga 没有百分之百的实现了 CSS 弹性盒子。它忽略了非布局属性,比如颜色。Yoga 还修改了一些弹性盒子属性以提供更好的 Right-to-Left 支持。最后,Yoga 增加了一个 AspectRatio 属性,用于解决一些常见的需求,比如像图片这样的元素布局。

YogaKit 简介

虽然你很想停留在 www 的世界中再玩一会儿,但这是一篇 Swift 教程。别担心,Yoga API 会让你沐浴在弹性盒子的余晖当中。你可以利用你的弹性盒子知识在你的 Swift 项目中。

Yoga 是用 C 写的,主要是为了性能上的优化和易于于其它平台集成。为了开发 iOS app,你需要使用 YogaKit,它是 C 实现的一个封装。

回忆弹性盒子的 web 示例,布局通过样式属性来配置。通过 YogaKit,布局通过 YGLayout 对象来配置。YGLayout 包含了 flex direction 属性、justify content 属性、align items 属性和 padding 、margin 属性。

YogaKit 将 YGLayout 暴露作为 UIView 的一个分类。这个分类在 UIView 中增加了一个 configureLayout(block:) 方法。block 闭包带一个 YGLayout 参数,你可以用这些数据来配置 view 的布局属性。

通过将每个子 view 配置所需的 Yoga 属性来构建布局。然后,调用根 view 上的 YGLayout 的 applyLayout(preservingOrigin:) 方法。然后布局会被计算并应用到根 view 和 subview 上。

你的第一个布局

新建 Swift Single View Application 项目,名为 YogaTryout。

你将用代码来创建 UI,你不需要使用故事板。

打开 Info.plist 删除 Main storyboard file base name 属性。然后,设置 Launch screen interface file base name 为空字符串。最后删除 Main.storyboard 和 LaunchScreen.storyboard。

打开 AppDelegate.swift 在 application(_:didFinishLaunchingWithOptions:) 方法 return 之前添加:

window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.backgroundColor = .white
window?.makeKeyAndVisible()

Build & run。你会看到一个空白的屏幕。

关闭 Xcode 项目。

如果你没有安装 CocoaPods,请打开终端窗口,安装它:

sudo gem install cocoapods

在终端窗口中,进入 YogaTryout.xcodeproj 所在目录。新建文件 Podfile ,编辑其内容为:

platform :ios, '10.3'

use_frameworks!

target 'YogaTryout' do
  pod 'YogaKit', '~> 1.5'
end

在终端窗口运行命令安装 YogaKit 依赖:

pod install

会有类似如下输出:

Analyzing dependencies
Downloading dependencies
Installing Yoga (1.5.0)
Installing YogaKit (1.5.0)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `YogaTryout.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed.

从现在开始,我们将用 YogaTryout.xcworkspace 来打开项目了。

打开 YogaTryout.xcworkspace,Build & run。你看到的仍然是一个空白屏幕。

打开 ViewController.swift 添加 import 语句:

import YogaKit

这就导入了 YogaKit 框架。

在 viewDidLoad() 方法最后添加:

// 1
let contentView = UIView()
contentView.backgroundColor = .lightGray
// 2
contentView.configureLayout { (layout) in
  // 3
  layout.isEnabled = true
  // 4
  layout.flexDirection = .row
  layout.width = 320
  layout.height = 80
  layout.marginTop = 40
  layout.marginLeft = 10
}
view.addSubview(contentView)
// 5
contentView.yoga.applyLayout(preservingOrigin: true)

这段代码做了这些事情:

  1. 创建一个 view,并设置背景色。
  2. 创建了一个布局配置闭包。
  3. 在 view 的布局期间,开启 Yoga 样式。
  4. 设置各种布局属性,比如 flex direction、大小、margin 值。
  5. 计算布局,并应用到 contentView。

在 iPhone 7p 模拟器上 Build & run。你会看到一个灰色方块:

瑜伽教程: 一个跨平台的布局引擎_第9张图片

你可能奇怪,为什么不用指定的 frame 实例化一个 UIView 并设置它的背景色呢?哥们,请耐心点。当你添加内容到这个容器之后,你就会看到魔法出现了。

在 viewDidLoad() 方法应用布局之前添加:

let child1 = UIView()
child1.backgroundColor = .red
child1.configureLayout{ (layout)  in
  layout.isEnabled = true
  layout.width = 80
}
contentView.addSubview(child1)

这段代码添加一个 80 像素宽的红色方块到 contentView。

然后,在上述代码之后添加:

let child2 = UIView()
child2.backgroundColor = .blue
child2.configureLayout{ (layout)  in
  layout.isEnabled = true
  layout.width = 80
  layout.flexGrow = 1
}
contentView.addSubview(child2)

这次添加了一个蓝色方块到容器中,宽 80 但允许它拉伸并填充剩余空间。如果这看起来很眼熟,那是因为这和 jsFiddle 中的代码很像。

Build & run。你会看到:

瑜伽教程: 一个跨平台的布局引擎_第10张图片

然后,添加下列代码在 contentView 的布局配置闭包中:

layout.padding = 10

这会为所有子元素添加一个内白。

在 child1 的布局配置闭包中添加:

layout.marginRight = 10

这会为红色方块右边增加一个边距。

最后,在 child2 的配置布局闭包中添加:

layout.height = 20
layout.alignSelf = .center

这指定了蓝色方块的高度并让它在容器中居中对齐。

Build & run 你会看到:

瑜伽教程: 一个跨平台的布局引擎_第11张图片

如果你想将整个灰色方块水平居中呢?哈,你可以在 contentView 的父 view 即 self.view 上设置 Yoga。

在 viewDidLoad()方法的 super 之后添加:

view.configureLayout { (layout) in
  layout.isEnabled = true
  layout.width = YGValue(self.view.bounds.size.width)
  layout.height = YGValue(self.view.bounds.size.height)
  layout.alignItems = .center
}

这会开启根 view 的 Yoga 并根据 view 的 bounds 配置宽、高布局。alignItems 指定了子元素为垂直居中对齐。记住,alignItems 指定了容器的子元素在交叉轴上的对齐方式。容器默认的 flex direction 是 column,因此交叉轴就是水平方向。

将 contentView 的布局配置中的 layout.marginLeft 一句移除。不再需要这个值李,因为父容器已经让这个元素居中对齐了。

最后,将:

contentView.yoga.applyLayout(preservingOrigin: true)

替换为:

view.yoga.applyLayout(preservingOrigin: true)

这会计算布局并应用到 self.view 及其子 view,包括 contentView。

Build & run。注意灰色方块现在居中对齐了:

瑜伽教程: 一个跨平台的布局引擎_第12张图片

让灰色方块垂直居中对齐也是同样简单。在 self.view 的布局配置闭包中添加这句:

layout.justifyContent = .center

将 contentView 布局配置中的 layout.marginTop 一句删除。因为父容器已经让它垂直居中了。

Build & run。你会看到,灰色方块已经垂直和水平居中了:

瑜伽教程: 一个跨平台的布局引擎_第13张图片

将设备旋转为横屏模式。哇喔,它们不再居中对齐李:

瑜伽教程: 一个跨平台的布局引擎_第14张图片

幸运的是,有一种方法能够获得设备旋屏通知,能帮助我们解决这个问题。

在类末尾添加方法:

override func viewWillTransition(
  to size: CGSize,
  with coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransition(to: size, with: coordinator)
  // 1
  view.configureLayout{ (layout) in
    layout.width = YGValue(size.width)
    layout.height = YGValue(size.height)
  }
  // 2
  view.yoga.applyLayout(preservingOrigin: true)
}

这段代码做了这些事情:

  1. 用新屏幕方向的 size 来更新配置。注意,只有受影响的属性会被更新。
  2. 重新计算并应用布局。

将设备旋转会竖屏。Build & run。旋转为横屏。灰色方块将正确居中对齐:

瑜伽教程: 一个跨平台的布局引擎_第15张图片

如果你想和自己的代码进行对比,你可以从这里下载最终的 tryout 项目。

很好,你可能心里面嘀咕,如果你用 IB 构建这个布局话时间不会超过 3 分钟,包括正确地处理旋屏:

瑜伽教程: 一个跨平台的布局引擎_第16张图片

当你的布局变得超乎想象的复杂,或者嵌套的 stack view 让你大发其火的时候,你会对 Yoga 刮目相看的。

另外,你可能很久就没有再用 IB 了,而是使用代码布局,比如:布局锚点、可视化格式语言。如果这都能用,那么 Yoga 也没什么不可以。记住,可视化格式语言不支持 Yoga 的宽高比。

Yoga 也很容易理解,一旦你理解了弹性盒子的话。在 iOS 中使用 Yoga 之前,你可以有很多资源快速测试弹性盒子布局。

高级布局

你对制造白色、红色和蓝色方块的兴趣已经消耗殆尽了吧?是时候加快进度了。在接下来的这一节,你将用全新的 Yoga 技能创建下图的视图:

瑜伽教程: 一个跨平台的布局引擎_第17张图片

下载并打开开始项目。它已经添加李 YogaKit 依赖。其它主类包括:

  • ViewController: 显示主界面。你将先和这个类打交道。
  • ShowTableViewCell: 用于在 Table view 中显示剧集。
  • Show: 剧集的模型对象。

Build & run。你会看到一个黑色窗口。

为了便于规划布局,我用线框图画出我们将要实现的布局:

瑜伽教程: 一个跨平台的布局引擎_第18张图片

图中的每个框说明如下:

  1. 显示剧集的图片。
  2. 显示连续剧的统计信息,子元素横向排列。
  3. 显示本集的标题,子元素横向排列。
  4. 显示本集介绍,子元素竖向排列。
  5. 显示有效的动作。第一层容器横向排列。第二层容器竖向排列。
  6. 显示 tab 按钮,子元素横向排列。
  7. 显示一个 table view,填充剩余空间。

在构建每一部分布局的时候,你都会新的 Yoga 属性以及如何对布局细部进行微调有更好的理解。

打开 ViewController.swift 在 viewDidLoad() 的加载 plist 一句后添加如下代码:

let show = shows[showSelectedIndex]

这句读取了要显示的剧集。

纵横比

Yoga 中有一个 aspectRatio 属性,允许通过设置子元素的纵横比来布局视图。AspectRatio 即宽高比。

在 contentView 被添加到 subview 之后添加代码:

// 1
let episodeImageView = UIImageView(frame: .zero)
episodeImageView.backgroundColor = .gray
// 2
let image = UIImage(named: show.image)
episodeImageView.image = image
// 3
let imageWidth = image?.size.width ?? 1.0
let imageHeight = image?.size.height ?? 1.0
// 4
episodeImageView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexGrow = 1.0
  layout.aspectRatio = imageWidth / imageHeight
}
contentView.addSubview(episodeImageView)

代码解释如下:

  1. 创建一个 UIImageView
  2. 根据所选的剧集设置它的图片
  3. 读取图片的 size
  4. 配置布局,根据图片的 size 设置 aspectRatio

Build & run。你会看到图片在垂直方向上被拉伸李,但图片的纵横比保持不变:

瑜伽教程: 一个跨平台的布局引擎_第19张图片

FlexGrow

之前你就看到过将 flexGrow 应用到容器中子元素的写法。比如在上一个示例中,我们曾经通过将蓝色方块的 flextGrow 设置为 1 来拉伸它。

如果对不止一个子元素设置 flexGrow 属性,则这些子元素首先按照它们所需要的空间来布局。每个子元素的 flexGrow 被用于剩余空间的分布。

在连续剧统计视图,我们打算将中间的部分占据的空间是另外两个空间的 2 倍。

在 episodeImageView 添加到 subviews 之后添加代码:

let summaryView = UIView(frame: .zero)
summaryView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexDirection = .row
  layout.padding = self.padding
}

这里将子元素布局成一行,并添加一点内白。

继续添加代码:

let summaryPopularityLabel = UILabel(frame: .zero)
summaryPopularityLabel.text = String(repeating: "★", count: showPopularity)
summaryPopularityLabel.textColor = .red
summaryPopularityLabel.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexGrow = 1.0
}
summaryView.addSubview(summaryPopularityLabel)

contentView.addSubview(summaryView)

这里添加了一个人气标签,并将它的 flexGrou 设置为 1。

Build & run,效果如下:

瑜伽教程: 一个跨平台的布局引擎_第20张图片

在将 summaryView 添加到 subviews 之前添加代码:

let summaryInfoView = UIView(frame: .zero)
summaryInfoView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexGrow = 2.0
  layout.flexDirection = .row
  layout.justifyContent = .spaceBetween
}

这里创建了一个新的容器 view,用于 summary 标签的子元素。注意 flexGrow 属性被设成了 2。因此,summaryInfoView 会占据比 summaryPopularityLabel 多出 2 倍的剩余空间。

继续添加代码:

for text in [showYear, showRating, showLength] {
  let summaryInfoLabel = UILabel(frame: .zero)
  summaryInfoLabel.text = text
  summaryInfoLabel.font = UIFont.systemFont(ofSize: 14.0)
  summaryInfoLabel.textColor = .lightGray
  summaryInfoLabel.configureLayout { (layout) in
    layout.isEnabled = true
  }
  summaryInfoView.addSubview(summaryInfoLabel)
}
summaryView.addSubview(summaryInfoView)

这个循环遍历了剧集要显示的所有 summary 标签。每个标签都是 summaryInfoView 容器的子元素。容器的布局规定这些标签应当按照左、中、右的方式放置。

Build & run,这些标签会显示出来:

瑜伽教程: 一个跨平台的布局引擎_第21张图片

要布局右边的空间,你可以在 summaryView 中再加一子元素。添加代码:

let summaryInfoSpacerView = 
  UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 1))
summaryInfoSpacerView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexGrow = 1.0
}
summaryView.addSubview(summaryInfoSpacerView)

这创建了一个 flexGrow 为 1 的空白。summaryView 有 3 个子元素。第一个子元素和第三个子元素会占据剩余容器空间的 25%,第二个子元素会占据剩余空间的 50%。

Build & run,效果如下:

瑜伽教程: 一个跨平台的布局引擎_第22张图片

更多示例

继续构建布局,以学习更多关于空间和位置的例子。

在 summaryView 代码之后添加:

let titleView = UIView(frame: .zero)
titleView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexDirection = .row
  layout.padding = self.padding
}

let titleEpisodeLabel =
  showLabelFor(text: selectedShowSeriesLabel, 
               font: UIFont.boldSystemFont(ofSize: 16.0))
titleView.addSubview(titleEpisodeLabel)

let titleFullLabel = UILabel(frame: .zero)
titleFullLabel.text = show.title
titleFullLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
titleFullLabel.textColor = .lightGray
titleFullLabel.configureLayout { (layout) in
  layout.isEnabled = true
  layout.marginLeft = 20.0
  layout.marginBottom = 5.0
}
titleView.addSubview(titleFullLabel)
contentView.addSubview(titleView)

这段代码创建了 titleView 用作两个显示剧集名称的子元素的容器。

Build & run,你会看到标题:

瑜伽教程: 一个跨平台的布局引擎_第23张图片

继续添加代码:

let descriptionView = UIView(frame: .zero)
descriptionView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.paddingHorizontal = self.paddingHorizontal
}

let descriptionLabel = UILabel(frame: .zero)
descriptionLabel.font = UIFont.systemFont(ofSize: 14.0)
descriptionLabel.numberOfLines = 3
descriptionLabel.textColor = .lightGray
descriptionLabel.text = show.detail
descriptionLabel.configureLayout { (layout) in
  layout.isEnabled = true
  layout.marginBottom = 5.0
}
descriptionView.addSubview(descriptionLabel)

这里创建了一个容器 view,指定了水平方向上的内白,添加了一个子元素用于显示聚集的剧情。

然后添加如下代码:

let castText = "Cast: \(showCast)";
let castLabel = showLabelFor(text: castText,
                             font: UIFont.boldSystemFont(ofSize: 14.0))
descriptionView.addSubview(castLabel)

let creatorText = "Creators: \(showCreators)"
let creatorLabel = showLabelFor(text: creatorText,
                                font: UIFont.boldSystemFont(ofSize: 14.0))
descriptionView.addSubview(creatorLabel)

contentView.addSubview(descriptionView)

这里添加了两个子元素到 descriptionView,用于显示更多信息。

Build & run,你会看到:

瑜伽教程: 一个跨平台的布局引擎_第24张图片

接下来是添加操作区域。

在 ViewController 扩展中,添加一个私有的助手方法:

func showActionViewFor(imageName: String, text: String) -> UIView {
  let actionView = UIView(frame: .zero)
  actionView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.alignItems = .center
    layout.marginRight = 20.0
  }
  let actionButton = UIButton(type: .custom)
  actionButton.setImage(UIImage(named: imageName), for: .normal)
  actionButton.configureLayout{ (layout) in
    layout.isEnabled = true
    layout.padding = 10.0
  }
  actionView.addSubview(actionButton)
  let actionLabel = showLabelFor(text: text)
  actionView.addSubview(actionLabel)
  return actionView
}

这里创建了一个容器 view,包含一个 image 和一个 label,水平居中对齐。

然后,在 viewDidLoad() 方法的 descriptionView 的代码下边,添加:

let actionsView = UIView(frame: .zero)
actionsView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexDirection = .row
  layout.padding = self.padding
}

let addActionView =
  showActionViewFor(imageName: "add", text: "My List")
actionsView.addSubview(addActionView)

let shareActionView =
  showActionViewFor(imageName: "share", text: "Share")
actionsView.addSubview(shareActionView)

contentView.addSubview(actionsView)

这里创建了一个容器 view,包含两个用 showActionViewFor(imageName:text) 方法生成的子元素。

Build & run。

瑜伽教程: 一个跨平台的布局引擎_第25张图片

接下来布局 tab 按钮。

在 ViewController 扩展中新建方法:

func showTabBarFor(text: String, selected: Bool) -> UIView {
  // 1
  let tabView = UIView(frame: .zero)
  tabView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.alignItems = .center
    layout.marginRight = 20.0
  }
  // 2
  let tabLabelFont = selected ?
    UIFont.boldSystemFont(ofSize: 14.0) :
    UIFont.systemFont(ofSize: 14.0)
  let fontSize: CGSize = text.size(attributes: [NSFontAttributeName: tabLabelFont])
  // 3  
  let tabSelectionView =
    UIView(frame: CGRect(x: 0, y: 0, width: fontSize.width, height: 3))
  if selected {
    tabSelectionView.backgroundColor = .red
  }
  tabSelectionView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.marginBottom = 5.0
  }
  tabView.addSubview(tabSelectionView)
  // 4
  let tabLabel = showLabelFor(text: text, font: tabLabelFont)
  tabView.addSubview(tabLabel)

  return tabView
}

代码解释如下:

  1. 创建一个容器 view,其子元素水平居中对齐。
  2. 根据 tab 按钮是否被选中来设置字体。
  3. 创建一个 view,用于 tab 按钮被选中时。
  4. 创建一个 label 用于 tab 按钮的标题。

在将 actionsView 添加到 contentView 的代码后面添加如下代码(在 viewDidLoad 方法中):

let tabsView = UIView(frame: .zero)
tabsView.configureLayout { (layout) in
  layout.isEnabled = true
  layout.flexDirection = .row
  layout.padding = self.padding
}

let episodesTabView = showTabBarFor(text: "EPISODES", selected: true)
tabsView.addSubview(episodesTabView)
let moreTabView = showTabBarFor(text: "MORE LIKE THIS", selected: false)
tabsView.addSubview(moreTabView)

contentView.addSubview(tabsView)

这里创建李一个 tab 按钮的容器 view,并将 tab 按钮添加到容器。

Build & run,你会看到:

瑜伽教程: 一个跨平台的布局引擎_第26张图片

在本例中,点击 tab 按钮是没有作用的。如果你后面准备添加这个功能的话,大部分工作都已经就绪李。

基本上差不多李。你还需要添加一个 table view。

在 tabView 被添加到 contentView 之后添加如下代码:

let showsTableView = UITableView()
showsTableView.delegate = self
showsTableView.dataSource = self
showsTableView.backgroundColor = backgroundColor
showsTableView.register(ShowTableViewCell.self,
                        forCellReuseIdentifier: showCellIdentifier)
showsTableView.configureLayout{ (layout) in
  layout.isEnabled = true
  layout.flexGrow = 1.0
}
contentView.addSubview(showsTableView)

这里创建并布局了 table view。布局配置中,将 flexGrow 设为 1,允许 table view 拉伸并填充剩余空间。

Build & run,你会看到视图中的列表显示出来了:

瑜伽教程: 一个跨平台的布局引擎_第27张图片

结束

恭喜你!如果你坚持到了这里,表明你已经是一个 Yoga 高手了。铺起你的瑜伽垫,穿上弹力裤,深呼吸。你可以从这里下载最终完成的项目。

要了解更多属性,比如 Right-to-Left,请阅读Yoga 文档。

弹性盒子规范是一个关于弹性盒子的极好的参考。弹性盒子学习参考是一个查找各种弹性盒子属性的绝佳的指南。

希望你喜欢本教程。如果有任何想法或问题,请在下面留言。

你可能感兴趣的:(iPhone开发)