从前一章中,我相信你现在应该了解如何使用堆叠建立一个复杂的UI。当然,在你能够熟练 SwiftUI 的运用之前,需要许多的练习才行。因此,在我们深入研究 ScrollView, 学习如何让视图滚动之前,我们先进行一个挑战,来作为本章的开始。你的任务是建立一个卡片视图(card view ),如图5.1 所示。
通过使用堆叠、图片与文字视图,你应该能够建立 UI。虽然我会逐步示范如何实现, 但请先花一些时间来思考如何完成这个任务,以及找出自己的解决方案。
当你完成卡片视图的建立之后,我将与你讨论 ScrollView
,并使用卡片视图建立一个可滚动的介面,图 5.2 即是完成后的 UI。
如果你还没有开启 Xcode,请开启它并使用 App 模板 (于 iOS 下)。来建立一个新项目。在下一个屏幕画面中,设定项目名称为“SwiftUIScrollView”(或是任何你喜欢的名称),并填入所需要的值。请确认已选取“Interface”选项中的“SwiftUI”。
到目前为止,我们在 ContentView.swift
档中撰写使用者介面的代码,代码撰写在这里完全没有任何问题,不过我要介绍一个整理代码的较佳方式。为了实现卡片视图, 我们另外为它建立一个单独文件,在项目导航器中,于 SwiftUIScrollView
按右键,并选择“New File...”,如图 5.3 所示。
如图 5.4 所示,在“User Interface”区块,选取“SwiftUI View”模板,然后点选“Next”来建立文件。将文件名称命名为 CardView
,并将其储存在项目数据夹中。
CardView.swift
中的代码与 ContentView.swift
中的代码很相似。同样的,你可以在画布中预览 UI,如图5.5 所示。
现在,我们准备要撰写卡片视图的代码。但是,首先你需要准备图档,并将其汇入素材目录。如果你不想要准备自己的图片,则可以至 https://www.appcoda.com/resources/swiftui/SwiftUIScrollViewImages.zip 下载示例图片档,将图片档解压缩后,选取 Assets
,并所有图片其拖曳至素材目录。
现在切回 CardView.swift
档。若你再看一下图 5.1,这个卡片视图是由两个部分组成, 视图上部是图片,而视图下部是文字叙述。
让我们从图片开始。我将使图片可调整大小,以缩放来填满屏幕,同时保持长宽比。你可以撰写代码如下:
struct CardView: View {
var body: some View {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
如果你忘记了这两个修饰器是什么,请返回并阅读有关 Image
物件的章节。接下来,我们来实现文字叙述部分,你可以撰写代码如下:
VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
显然的,你需要使用 Text
来建立文字视图。由于我们在叙述中实际上有三个垂直排列的文字视图,因此我们使用一个 VStack
来嵌入它们。对于 VStack
,我们指定对齐方式为 .leading
,这会将文字视图对齐堆叠视图的左侧。
这些文字的修饰器皆在有关 Text
物件的章节讨论过。如果你对任何修饰器有疑问的话,可以回去参考。但是,这里会特别提到有关 .primary
与 .secondary
颜色。
虽然你可在 foregroundColor
修饰器指定标准颜色,像是 .black
与 .purple
,但 iOS 提供一套系统颜色,其中包含主色( primary color )、辅色( secondary color )、第三级色( tertiary color )等变化,通过使用此颜色变化,你的 App 可以轻松支持浅色模式与深色模式。举例而言,文字视图的主色默认设定为浅色模式的黑色。当 App 切换到深色模式, 主色将被调整为白色,这是由 iOS 自动调整,因此你无须另外编写写支持深色模式的代码,我们将在后面的章节中深入探讨深色模式。
为了将图片与这些文字视图垂直排列,我们使用 VStack
来嵌入它们,目前的布局如图 5.7 所示。
还没有完成,尚有几件事情需要实现。首先,如果文字叙述区块要与图片的边缘对齐,该如何做呢?
依照我们所学,我们可以在一个 HStack
嵌入文字视图的 VStack
,然后我们将使用一个留白( Spacer
)来将VStack
往左推,我们来看看是否可行。
如果你已经修改代码,如图 5.8 所示,这个文字视图的VStack 会对齐屏幕的左侧。
最好是在 HStack
周围加入一些间距( padding )。插入 padding
修饰器如下,如图 5.9 所示 :
最后是边框部分。我们在前面的章节中讨论过如何绘制圆角边框。我们可以使用 overlay
修饰器,并使用RoundedRectangle
来画出边框。以下是完整的代码:
struct CardView: View {
var body: some View {
VStack {
Image("swiftui-button")
.resizable()
.aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading) {
Text("SwiftUI")
.font(.headline)
.foregroundColor(.secondary)
Text("Drawing a Border with Rounded Corners")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by Simon Ng".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color(.sRGB, red: 150/255, green: 150/255, blue: 150/255, opacity: 0.1), lineWidth: 1)
)
.padding([.top, .horizontal])
}
}
除了边框之外,我们也在顶部、左侧、右侧分别加入了一些间距。现在,你应该已经建立好卡片视图的布局,如图 5.10 所示。
虽然目前卡片视图看起来没问题,但我们将图片与文字写死( Hard Code)在程序中, 为了让它更具弹性,我们要重构代码。首先,在 CardView
声明 image、category、heading 与author 这些变量:
var image: String
var category: String
var heading: String
var author: String
接下来,将 Image
与 Text
视图的值以下列变量替代:
VStack {
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading) {
Text(category)
.font(.headline)
.foregroundColor(.secondary)
Text(heading)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(3)
Text("Written by \(author)".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
}
修改完成后,你将在 CardView_Previews
结构中看到一个错误,如图 5.11 所示。这是因为我们在 CardView
导入了一些变量,当使用它时,必须指定参数给它。
因此,以下列代码来取代:
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
}
}
错误将可被修正,现在你已经建立了一个能接受不同图片及文字的弹性 CardView
。
再看一下图5.2,这就是我们要实现的使用者介面。首先,你可能认为我们可以使用一个 VStack
来嵌入四个卡片视图。你可以切换到 ContentView.swift
,于 body 内插入以下的程序:
VStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
}
如果你这样做的话,这些卡片视图将被挤压,以填满屏幕,因为 VStack
是不可滚动的,如图 5.12 所示。
要让内容可以滚动,SwiftUI 提供一个名为 ScrollView
的视图。当内容嵌入在一个 ScrollView
时,它变得可以滚动,因此你需要做的是在一个 ScrollView
内加入一个 VStack
,以使视图可以滚动。在预览画布中,你可以拖曳这些视图来滚动内容。
你的任务是加入标题( header )至目前的滚动视图( scroll view )中,结果如图 5.14 所示。如果你完全暸解了 Vstack
与 Hstack
,你应该有能力建立这个布局。
默认上,ScrollView
允许你以垂直方向滚动内容。另外,它还支持水平方向的可滚动内容。我们来了解如何进行一些修改,以将目前的布局转换为轮播(carousel )UI。
修改 ContentView
如下:
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
// 作业#1的代码
HStack {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
.frame(width: 300)
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
.frame(width: 300)
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
.frame(width: 300)
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
.frame(width: 300)
}
}
}
}
我们在上列的代码中做了三个修改:
.horizontal
值,以在 ScrollView
中使用一个水平滚动视图。VStack
修改为 HStack
。修改代码之后,你将看到卡片视图以水平排列且可以滚动,如图 5.15 所示。
在滚动视图时,屏幕底部附近有一个滚动指示器。这个指示器默认是显示的。如果你想要隐藏它,你可以将 ScrollView
的代码修改如下:
ScrollView(.horizontal, showsIndicators: false)
通过指定 showIndicators
为 false
,iOS 将不再显示该指示器。
如果你再次阅读一下代码,所有的 CardViews
是以 .frame
修饰器来限制其宽度为 300 点,是否有其他简化的方式,并移除重复的代码呢?SwiftUI 框架提供了开发者群组视图(Group
view)的功能,可以将相关内容群组起来。更重要的是,你可以将修饰器加至群组,所有嵌入群组内的视图皆能够同步做产生效果。
举例而言,你可以将 HStack
内的程序重写如下来完成同样的结果:
HStack {
Group {
CardView(image: "swiftui-button", category: "SwiftUI", heading: "Drawing a Border with Rounded Corners", author: "Simon Ng")
CardView(image: "macos-programming", category: "macOS", heading: "Building a Simple Editing App", author: "Gabriel Theodoropoulos")
CardView(image: "flutter-app", category: "Flutter", heading: "Building a Complex Layout with Flutter", author: "Lawrence Tan")
CardView(image: "natural-language-api", category: "iOS", heading: "What's New in Natural Language API", author: "Sai Kambampati")
}
.frame(width: 300)
}
如图 5.15 所示,第一张卡片的标题被截断了,该如何修正这个问题呢? SwiftUI 中可以使用 .minimumScaleFactor
修饰器来自动缩小文字。你可以切换至 CardView.swift,并于 Text(标题)
加上以下这个修饰器:
.minimumScaleFactor(0.5)
SwiftUI 会自动缩小文字来相容可用的空间。这边的值设定了视图所允许的最小缩放量。以这个例子来看,SwiftUI 能够将文字尽量缩至原来大小的 50%。
最后有一个作业,修改目前的代码,并如图 5.16 所示来重新排列。请注意,当使用者滚动卡片视图时,用户应该可以看到标题和日期。
在本章所准备的示例档中,有完整的项目与作业解答可以下载: