Yoga 教程-使用跨平台布局引擎

2018 - 04 - 16

Posted by Archimboldi翻译

本文翻译自 raywenderlich.com 上,Christine Abernathy 写的教程 Yoga Tutorial: Using a Cross-Platform Layout Engine

Yoga 是一个基于 Flexbox 的跨平台布局引擎,能使布局工作更容易。你可以使用 Yoga 作为一个通用的布局系统,来代替 iOS 上的 Auto Layout 或 web 上的 Cascading Style Sheets (CSS)。

最初是 Facebook 在 2014 年推出的一个 CSS 布局的开源库,2016 年改版并更名为 Yoga。Yoga 支持多个平台,包括 Java、C#、C 和 Swift。

库开发者可以集成 Yoga 到他们的布局系统,就如 Facebook 已经集成进了它的两个开源项目:React Native 和 Litho。然而,Yoga 也是一个 iOS 开发者可以直接用来布局视图的框架。

在这份教程里,你将学习 Yoga 的核心概念,然后通过构建 FlexAndChill app 来练习并扩展它们。

即便你将使用 Yoga 布局引擎,在阅读这份教程之前,熟悉 Auto Layout 也是有好处的。你也想学习在你的项目中使用 CocoaPods 引入 Yoga 的知识。

拆包 Flexbox

Flexbox 也称为 CSS Flexible Box,被创建用来处理 web 上的复杂布局。一个关键特征是在给定方向上高效布局内容,并能“灵活”处理自身大小来适应一些空间。

Flexbox 由 flex 容器组成,每个含有一个或多个 flex 项目:

flexbox_theory

Flexbox 定义 flex 项目如何在一个 flex 容器里布置。Flex 容器之外和 flex 项目内部的内容会照常渲染。Flex 项目沿容器内的单一方向布置(尽管它们可以任意包裹)。这将设置项目的主轴。相反的方向被称为横轴。

flexbox_theory

Flexbox 允许你指定项目在主轴与横轴上的定位和间隔。对齐内容(justify-content)指定项目沿容器的主轴对齐。下面的示例显示容器的 flex 方向为行时的项目展示位置:

flexbox_theory

  • flex-start:项目被定位在容器的开端。
  • flex-end:项目被定位在容器的末端。
  • center:项目被定位在容器的中间。
  • space-between:项目在容器内被空白空间均匀间隔开,第一个项目在开端位置,最后一个项目在末端位置。
  • space-around:项目周围以同等空间均匀间隔。

对齐项目(align-items)指定项目沿容器的横轴对齐。这个例子显示容器的 flex 方向为时(这意味着横轴垂直运行)的项目展示位置:

flexbox_theory

项目在容器的开端、中间和末端垂直对齐。

这些初步的 Flexbox 属性应该让你感受了 Flexbox 的工作原理。还有更多属性可供你使用。有些控制项目依据可用容器空间来拉伸或收缩的方式。另一些可以设置填充(padding)、边距(margin),甚至大小(size)。

Flexbox 样例

一个完美试用 Flexbox 概念的地方是 jsFiddle,一个在线的 JavaScript,HTML 和 CSS 运行环境。

前往这里启动 JSFiddle,并看看。你应该看到 4 个窗格:

jsFiddle_init

三个编辑框里的代码驱动输出到你看见的右下方的窗格。启动的例子展示了一个白色的盒子。

注意在 CSS 编辑器里定义了 yoga 的类选择器。这些代表了 Yoga 实现的 CSS 默认值。一些值不同于 Flexbox w3 规范的默认值。例如,Yoga 默认 flex 方向是纵向,并且项目被定位在容器的开端。任何 HTML 元素的样式经由 class=”yoga” 将开启 “Yoga” 模式。

检查 HTML 源代码:

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

这个 div 的基本样式是 yoga。另外的样式属性设置了大小,背景色并覆盖了默认的 flex 方向,所以项目将沿行排列。

在 HTML 编辑器里,添加下面的代码到 div 闭标签之前:

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

这添加了一个 80 像素宽,红色盒子的 yoga 样式到 div 容器里。

点击顶部菜单的 Run。你应该看到如下输出

jsFiddle_run

添加下面的子元素到根 div,在红色盒子的 div 之后:

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

这添加了一个 80 像素宽的蓝色盒子。

点击 Run。更新后的输出显示了蓝色盒子堆叠在红色盒子的右边:

jsFiddle_run

使用下面的代码替换蓝色盒子的 div

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

额外的 flex-grow 属性允许盒子拉伸并填充任何可用空间。

点击 Run 查看更新后的输出,蓝色盒子被拉伸:jsFiddle_run

使用下面的代码替换整个 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>  

这介绍了填充子项目,添加红色盒子的右边距,设置蓝色盒子的高度,并让蓝色盒子与容器的中心对齐。

点击 Run 查看输出的结果:

jsFiddle_run

你可以在这里查看最终的 jsFiddle。请随意使用其它布局属性和值玩耍。

Yoga vs. Flexbox

即使 Yoga 是基于 Flexbox 的,它们也有一些不同。

Yoga 并没有实现全部 CSS Flexbox。它省略了非布局属性,如设置颜色。Yoga 改进了一些 Flexbox 的属性来提供更好的从右到左的支持。最后,Yoga 增加了一个新的比例(AspectRatio)属性来处理在布置某些元素如图片时常见的需求。

介绍 YogaKit

虽然你可能想要留在美妙的互联网上,但这是一份 Swift 教程。不要害怕,Yoga API 将使你沐浴在 Flexbox 熟悉度的余晖中。你将可以在 Swift app 布局中应用你学到的 Flexbox。

Yoga 使用 C 编写,主要关注于优化性能和简便集成到其它平台。对于开发 iOS app,你将使用 YogaKit 工作,这是一个由 C 实现的封装包。

回顾 Flexbox 在 web 里的样例,布局是通过样式属性来配置的。而 YogaKit,布局配置是交由 YGLayout 对象来完成。YGLayout 包含的属性有 flex 方向,对齐内容,对齐项目,填充和边距。

YogaKit 曝露 YGLayout 作为 UIView 上的一个 Category。这个 Category 添加 configureLayout(block:) 方法到 UIView。将 YGLayout 参数传进闭合块里,并使用这些信息来配置视图的布局属性。

通过使用所需的 Yoga 属性配置每个参与的视图来构建你的布局。一旦完成,你在根视图的 YGLayout 上调用 applyLayout(preservingOrigin:)。这会计算并应用布局到根视图和子视图。

你的第一个布局

使用 Single View Application 模版创建一个新的 Swift iPhone 工程,命名为 YogaTryout。

你将创建自己的 UI 编程方式,所以不需要使用 Storyboard。

打开 Info.plist,删除默认文件名为 Main 的 storyboard 属性。接着设置启动界面(Launch screen)文件名的值为空字符串。

打开 AppDelegate.swift,在 application(_:didFinishLaunchingWithOptions:) 返回之前添加以下代码:

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

编译(build)并运行(run)app。你会看到一个空白的白色屏幕。

关闭 Xcode 项目。

如果你还没有安装CocoaPods,打开 Terminal,并输入以下代码安装 CocoaPods:

sudo gem install cocoapods  

在 Terminal 里,跳转到 YogaTryout.xcodeproj 所在的本地文件夹。创建一个名为 Podfile 的文件,并设置以下内容:

platform :ios, '10.3'  

use_frameworks!  

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

在 Terminal 里运行如下命令,安装 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 工作。

Open YogaTryout.xcworkspace then build and run. You should still see a blank white screen.

打开 YogaTryout.xcworkspace,接着编译(build)并运行(run)。你依然看到一个空白的白色屏幕。

打开 ViewController.swift,添加下面的引入:

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. 创建一个视图,并设置背景色。
  2. 设置布局配置闭包。
  3. 在视图布局期间启用 Yoga 样式。
  4. 设置各个布局属性,包括 flex 方向,框架大小和边距。
  5. 计算并应用布局到 contentView。

在 iPhone 7 Plus 上编译(build)并运行(run)app。你会看到一个灰色盒子:

tryout_1

你也许会抓头,想知道为什么你不能使用期望的框架大小和设置背景色来简单实例化一个 UIView。耐心点,我的孩子。当你添加子项目到这个初始容器时,魔法开始了。

在 viewDidLoad() 里,在应用布局到 contentView 之前添加以下代码:

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)。你会看到如下:tryout_2

现在,给 contentView 的布局配置块里添加以下声明:

layout.padding = 10  

这设置了全部子项目的填充值。

给 child1 的布局配置块里添加以下代码:

layout.marginRight = 10  

这设置了红色盒子的右边距。

最后,给 child2 的布局配置块里添加以下代码:

layout.height = 20  
layout.alignSelf = .center  

这设置了蓝色盒子的高度和居中对齐于父容器。

编译(build)并运行(run)。你会看到如下:

tryout_3

什么可以让你实现将整个灰色盒子水平居中呢?好的,你可以在 contentView 的父视图 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  
}  

这在根视图上启用了 Yoga,并基于视图的范围(bound)配置了布局的宽度和高度。alignItems 配置了子项目水平居中。记住 alignItems 指定容器的子项目沿横轴对齐。容器的默认 flex 方向是纵向。所以横轴是水平方向。

移除 contentView 布局配置里的 layout.marginLeft 声明。你将通过它的父容器居中这个项目,所以不再需要了。

最后,替换:

contentView.yoga.applyLayout(preservingOrigin: true)  

为如下代码:

view.yoga.applyLayout(preservingOrigin: true)  

这会计算并应用布局到 self.view 和它的子视图,包括 contentView。

编译(build)并运行(run)。注意灰色盒子现在水平居中:tryout_4

让灰色盒子在屏幕上垂直居中很简单。添加下面的布局配置块到 self.view:

layout.justifyContent = .center  

移除 contentView 布局配置里的 layout.marginTop 声明。在父容器控制了垂直居中时,就不再需要了。

编译(build)并运行(run)。你会看到灰色盒子既水平又垂直居中:tryout_5

旋转设备到横屏模式。哦-哦,不再居中了:tryout_landscape_1

幸好,有一个方法可以得到屏幕改变方向的通知,以帮助解决它。

在类最后添加下面的方法:

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. 依据新方向的(屏幕)大小更新布局配置。请注意,只有受影响的属性才会更新。
  2. 重新计算并应用布局。

旋转设备回竖屏模式。编译(build)并运行(run)。旋转设备到横屏模式。灰色盒子现在应该好好的居中了:tryout_landscape_2

如果你想比较你的代码,可以在这里下载最终的 tryout 项目

诚然,你大概会在你的呼吸里嘀咕,你可以使用 Interface Builder,不到 3 分钟就构建好了这个布局,包括妥善处理旋转:layout_interface_builder

basic-angry-1

当你的布局开始变得比你想的更加复杂,如需要适应嵌入式堆栈视图时,你会想给 Yoga 一个新鲜的看法。

另一方面,你可能早已放弃使用 Interface Builder 编写布局方式,如布局锚定或可视化格式语言。如果那些为你工作,无需改变。在心里记住可视化格式语言不支持宽高比,而 Yoga 支持。

一旦你理解了 Flexbox,Yoga 就是如此易于掌握。这儿有许多资源,你可以在 iOS 上使用 Yoga 构建它们之前快速尝试 Flexbox 布局。

高级布局

你构建白色、红色和蓝色盒子的喜悦大概已经磨损了。是时候摇一摇。在接下来的部分,你将带着你新构造的 Yoga 技能来创建类似下面的视图:flexandchill_final

下载并浏览开始的工程。它已经包含了 YogaKit 的依赖。其它主要的类有:

  • ViewController:显示主视图。你将主要在这个类里工作。
  • ShowTableViewCell:用来显示列表视图里的插曲。
  • Show: 节目的模型(Model)对象。

编译(build)并运行(run)app。你会看到一个黑色的屏幕。

这是一个所需布局的线框分解,来帮助计划事情:flexandchill_frames

让我们快速剖析图中各个盒子的布局:

  1. 展示节目的图片。
  2. 展示这一项目所属系列的摘要信息,横向排列。
  3. 展示节目的标题信息,横向排列。
  4. 展示节目的描述信息,纵向排列。
  5. 展示可以进行的操作。主容器横向排列。每个子项目都是一个容器,纵向排列。
  6. 展示项目的标签,横向排列。
  7. 展示一个列表,用来填充其余的空间。

随着构建每一片布局,你将对额外的 Yoga 属性获得更多好感,和如何微调一个布局。

打开 ViewController.swift,添加以下代码到 viewDidLoad(),在节目刚刚从 plist 加载之后:

这设置了要展示的节目。

宽高比(Aspect Ratio)

Yoga 介绍了一个宽高比(aspectRatio)属性用来帮助对已知宽高比的项目布置视图。宽高比代表了宽度与高度的比例。

添加以下代码在 contentView 加入到它的父视图之后:

// 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. 梳理出图片大小。
  4. 配置布局和设置基于图片大小的宽高比。

编译(build)并运行(run)app。你会看到图片垂直拉伸,但保持了图片的宽高比:flexandchill_1-1

FlexGrow

迄今你见过了 flexGrow 应用在容器里的项目上。你在前一个例子里通过设置 flexGrow 属性为 1,拉伸了蓝色盒子。

如果不止一个子项目设置 flexGrow 属性,那么会优先基于它们需要的空间布置子项目。然后使用每个子元素的 flexGrow 分配剩余的空间。

在系列概述视图里,你将布置子项目以便中间部分占据其余两部分的两倍空间。

添加以下代码到 episodeImageView 加入到它的父视图之后:

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)  

这添加了一个人气标签,并设置了它的 flexGrow 属性为 1。

编译(build)并运行(run)app 查看人气信息:flexandchill_2-1

添加以下代码在 summaryView 加入到它的父视图之前:

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

这为概述标签子项目设置了一个新的容器视图。注意 flexGrow 设置为 2。因此,summaryInfoView 将占据比 summaryPopularityLabel 多两倍的额外空间。

添加以下代码在前一段代码块之后:

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)  

这里循环使用概述标签来展示节目。每个标签都是 summaryInfoView 容器的一个子元素。那个容器的布局指定了标签放置在开端,中间和末端。

编译(build)并运行(run)app 查看节目标签:flexandchill_3-1

调整布局以得到正确的空间,你将再添加一个项目到 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)app 查看属性调整过的布局:flexandchill_4-1

更多样例

继续构建布局,查看更多间距和定位的例子。

添加以下代码到 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)app 查看标题:flexandchill_5-1

接着添加下面的代码:

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)  

这里创建了一个使用水平间距的容器视图,然后添加了一个子元素用来展示节目的详情。

现在,添加下面的代码:

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)app 查看完整描述:flexandchill_6-1

接下来,你将添加节目的操作视图。

在 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  
}  

这里使用一张图片和一个标签设置了一个水平居中对齐的容器视图。

现在,添加以下代码到 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)  

这里创建了一个容器,持有两个使用 showActionViewFor(imageName:text) 创建的项目。

编译(build)并运行(run)app 查看操作视图。flexandchill_7-1

是时候布置一些标签了。

在 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. 创建一个容器,设置内部项目水平居中对齐。
  2. 根据标签选中与否来计算期望的字体信息。
  3. 创建一个视图来标明标签的选中。
  4. 创建一个用来表示标签标题的 label。

添加下面的代码到 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)  

这设置了标签容器视图,并将标签项目添加到容器。

编译(build)并运行(run)app 看看你的新标签:flexandchill_8-1

在这个简单的 app 里,标签选项没有功能。如果你有兴趣稍后添加,大多数勾子都已到位。

就快完成了。你只需将列表视图加入到最后。

添加以下代码到 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)  

这段代码创建并配置了一个列表视图。布局配置信息设置 flexGrow 属性为 1,允许列表视图拉伸以填充其余的空间。

编译(build)并运行(run)app。你应该看到在视图里有一个剧集列表:flexandchill_final

继续学习之路

恭喜!如果你已经做到了这一点,你几乎是一个 Yoga 专家。推出你的垫子,抓住额外的特殊拉伸裤,屏住呼吸。你可以在这里下载最终的教程工程。

查看 Yoga 文档以获得更多未涵盖属性的细节,例如对从右到左的支持。

Flexbox 规范是一个获得更多 Flexbox 背景知识的好资源。Flexbox 学习 资源是一个十分便利的,浏览不同 Flexbox 属性的向导。

我希望你享受阅读这份 Yoga 教程。如果你有关于这份手册的任何建议或问题,请加入下面的论坛进行讨论!

Table of Contents