如果今天的报价来自第一个登上月球的人尼尔·阿姆斯特朗,那将是合适的。早在2000年,他就说:“科学是关于什么;工程是关于可能的事情。” 我不了解你,但是我发现这给了我很大的启发:每次创建一个新的Xcode项目时,我们都有一块空白的板块可以使用,而这正是我们想要的。

今天,我们正在学习构建Moonshot的技术,但是与所有正在学习的技术一样,它们构成了你更广泛的知识的一部分,供你在未来几年中随心所欲地进行混合和重新混合。

今天,你有五个专题工作过,在你将了解GeometryReader,ScrollView,NavigationLink,等等。

使用GeometryReader调整图像大小以适合屏幕

当我们Image在SwiftUI中创建视图时,它将根据其内容的尺寸自动调整自身大小。因此,如果图片为1000x500,则Image视图也将为1000x500。有时这是你想要的,但是大多数情况下,你将希望以较小的尺寸显示图像,而我想向你展示如何做到这一点,以及如何使用以下方法使图像适合用户的屏幕宽度一种称为的新视图类型GeometryReader。

首先,向你的项目中添加某种图像。没关系,只要它比屏幕宽即可。我称我为“ Example”,但显然你应该在下面的代码中替换你的图片名称。

现在,让我们在屏幕上绘制该图像:

1
2
3
4
5
6
7
struct ContentView: View {
var body: some View {
VStack {
Image("Example")
}
}
}

即使在预览中,你也可以看到对于可用空间来说太大了。图像frame()与其他视图具有相同的修饰符,因此你可以尝试按如下所示将其缩小:

1
2
Image("Example")
.frame(width: 300, height: 300)

但是,这将行不通–你的图片仍将显示为原尺寸。如果你想知道为什么,请仔细查看预览窗口:你会看到图像已放大,但是中间有一个300x300的蓝色框。的图像视图的帧已被正确设定,但内容的图像仍显示为原始大小。

尝试将图像更改为此:

1
2
3
Image("Example")
.frame(width: 300, height: 300)
.clipped()

现在,你将更清楚地看到事情:我们的图像视图确实是300x300,但这并不是我们想要的。

如果你也想调整图像内容的大小,则需要使用如下resizable()修饰符:

1
2
3
Image("Example")
.resizable()
.frame(width: 300, height: 300)

更好,但仅此而已。是的,现在可以正确调整图像的大小,但是可能看起来像是被压扁了。我的图像不是方形的,因此,由于它已被调整为方形,因此看起来有些失真。

要解决此问题,我们需要要求图像按比例调整自身大小,这可以使用aspectRatio()修饰符来完成。这样我们可以提供确切的宽高比以及应如何应用它,但是如果跳过宽高比本身,SwiftUI将自动使用原始的宽高比。

当谈到“应该如何应用”部分,SwiftUI称此为内容的模式,给我们两个选择:.fit意味着整个图像放在容器内,即使这意味着离开视图的某些部分清空,并且.fill手段视图将没有空白部分,即使这意味着我们的某些图像位于容器外部。

尝试它们两者,以自己了解差异。这是.fit应用的模式:

1
2
3
4
Image("Example")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)

这是.fill应用模式:

1
2
3
4
Image("Example")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 300)

如果我们要使用固定尺寸的图像,那么所有这些功能都很好用,但是通常你希望图像能够自动放大以一维或二维填充屏幕。也就是说,你真正要说的不是“ 硬编码300的宽度”,而是“使此图像充满屏幕的宽度”。

SwiftUI为此提供了专用类型,称为GeometryReader,并且功能非常强大。是的,我知道许多SwiftUI功能强大,但老实说:你可以做的事GeometryReader会让你震惊。

我们将GeometryReader在项目15中进行更详细的介绍,但现在,我们将其用于一项工作:确保图像填充其容器视图的整个宽度。

GeometryReader是一个视图,就像我们使用过的其他视图一样,除了创建视图时,我们将获得一个GeometryProxy对象以供使用。这使我们可以查询环境:容器有多大?我们的立场是什么?是否有安全区域插图?等等。

我们可以使用此几何代理设置图像的宽度,如下所示:

1
2
3
4
5
6
7
8
VStack {
GeometryReader { geo in
Image("Example")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width, height: 300)
}
}

现在,无论我们使用什么设备,图像都将占据我们屏幕的整个宽度。

对于我们的最后一个技巧,让我们height从图像中删除,如下所示:

1
2
3
4
5
6
7
8
VStack {
GeometryReader { geo in
Image("Example")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: geo.size.width)
}
}

我们已经为SwiftUI提供了足够的信息,可以自动计算出高度:它知道原始宽度,知道我们的目标宽度,并且知道我们的内容模式,因此它了解图像的目标高度如何与图像的高度成比例。目标宽度。

ScrollView如何让我们处理滚动数据

你已经看到了如何List和Form我们让创建数据的滚动表,但因为当我们要滚动次任意日期-即,只是一些意见,我们已经通过手工创建的-我们需要转向SwiftUI的ScrollView。

滚动视图可以水平,垂直或双向滚动,你还可以控制系统是否应在滚动视图旁边显示滚动指示器–这些滚动条看起来很小,可以使用户感觉内容的大小。当我们将视图放置在滚动视图中时,它们会自动确定内容的大小,以便用户可以从一个边缘滚动到另一边缘。

例如,我们可以创建一个包含100个文本视图的滚动列表,如下所示:

1
2
3
4
5
6
7
8
ScrollView(.vertical) {
VStack(spacing: 10) {
ForEach(0..<100) {
Text("Item \($0)")
.font(.title)
}
}
}

如果你在模拟器中重新运行该滚动条,则会看到你可以自由拖动滚动视图,并且如果滚动到底部,你还将看到ScrollView将安全区域视为List与Form–其内容位于home指示器下方,但它们增加了一些额外的填充,因此最终视图是完全可见的。

你可能还会注意到,直接在中心点击会很烦人–整个区域都可以滚动是很常见的。要获得这种行为,我们应该VStack占用更多空间,同时保持默认的中心对齐不变,如下所示:

1
2
3
4
5
6
7
8
9
ScrollView(.vertical) {
VStack(spacing: 10) {
ForEach(0..<100) {
Text("Item \($0)")
.font(.title)
}
}
.frame(maxWidth: .infinity)
}

现在,你可以在屏幕上的任意位置点击并拖动,这更加方便了用户。

这一切看起来确实很简单,而且确实ScrollView比UIScrollView我们必须与UIKit一起使用的旧版本要容易得多。但是,你需要注意一个重要的注意事项:当我们将视图添加到滚动视图时,它们会立即创建。

为了说明这一点,我们可以围绕常规文本视图创建一个简单的包装,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
struct CustomText: View {
var text: String

var body: some View {
Text(text)
}

init(_ text: String) {
print("Creating a new CustomText")
self.text = text
}
}

现在我们可以在我们的内部使用它ForEach:

1
2
3
4
ForEach(0..<100) {
CustomText("Item \($0)")
.font(.title)
}

结果看起来相同,但是现在当你运行该应用程序时,你会在Xcode的日志中看到打印了一百次的“正在创建新的CustomText” – SwiftUI不会等到你向下滚动才能看到它们,它会立即创建它们。

你可以使用尝试相同的实验List,如下所示:

1
2
3
4
5
6
List {
ForEach(0..<100) {
CustomText("Item \($0)")
.font(.title)
}
}

该代码运行时,你会发现它的行为是懒惰的:它CustomText仅在真正需要时才创建实例。

使用NavigationLink将新视图推入堆栈

SwiftUI NavigationView在视图顶部显示了一个导航栏,但它还执行其他操作:它使我们可以将视图推入视图堆栈。实际上,这实际上是iOS导航的最基本形式–点击“ Wi-Fi”或“常规”时,你可以在“设置”中看到它;或者点击某人的名字,则可以在“消息”中看到它。

该视图堆栈系统与我们之前使用的图纸非常不同。是的,两者都显示出某种新观点,但是它们的呈现方式存在差异,这会影响用户对它们的思考方式。

让我们先看一些代码,以便你自己看看。如果我们将默认的文本视图与导航视图一起包装并为其指定标题,则会得到以下信息:

1
2
3
4
5
6
7
8
9
10
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello World")
}
.navigationBarTitle("SwiftUI")
}
}
}

该文本视图只是静态文本。它不是带有任何附加操作的按钮。我们将做到这一点,以便当用户点击“ Hello World”时,向他们展示一个新视图,并使用NavigationLink以下方法完成:为该目标提供一个目的地以及可以点击的内容,其余的将由它负责。

我喜欢SwiftUI的众多优点之一就是,我们可以将其NavigationLink与任何一种目标视图一起使用。是的,我们可以设计要推送到的自定义视图,但是我们也可以直接推送到某些文本。

要尝试此操作,请将你的视图更改为:

1
2
3
4
5
6
7
8
NavigationView {
VStack {
NavigationLink(destination: Text("Detail View")) {
Text("Hello World")
}
}
.navigationBarTitle("SwiftUI")
}

现在运行代码,看看你的想法。你会看到“ Hello World”现在看起来像一个按钮,点击它会在右侧显示一个新视图,即“详细信息视图”。更好的是,你会看到“ SwiftUI”标题会向下移动以变为后退按钮,你可以点击该按钮或从左边缘轻扫以返回。

所以,无论是sheet()和NavigationLink允许我们显示从当前一个新的观点,但这样他们这样做是不同的,你应该仔细选择他们:

NavigationLink 用于显示有关用户选择的详细信息,就像你正在深入探讨主题一样。
sheet() 用于显示不相关的内容,例如设置或撰写窗口。
你看到的最常见的地方NavigationLink是列表,SwiftUI的功能非常出色。

尝试将你的代码修改为此:

1
2
3
4
5
6
7
8
NavigationView {
List(0..<100) { row in
NavigationLink(destination: Text("Detail \(row)")) {
Text("Row \(row)")
}
}
.navigationBarTitle("SwiftUI")
}

现在,当你运行该应用程序时,你将看到100个可以点击以显示详细视图的列表行,但是你还将在右侧看到灰色的显示指示器。这是一种标准的iOS方法,告诉用户在点击该行时另一个屏幕将从右侧滑动,而SwiftUI足够聪明,可以在此处自动添加它。如果这些行不是导航链接-如果你注释掉该NavigationLink行及其右括号,你会看到指示消失。

处理分层可编码数据

该Codable协议使解码平面数据变得很简单:如果你解码的是一个类型的单个实例,或者这些实例的数组或字典,则一切正常。但是,在这个项目中,我们将解码稍微复杂的JSON:另一个数组内部将使用不同的数据类型来解码一个数组。

如果要解码这种分层数据,关键是为每个级别创建单独的类型。只要数据与你要求的层次结构匹配,Codable就可以解码所有内容,而无需我们做进一步的工作。

为了演示这一点,请将此按钮放入你的内容视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
Button("Decode JSON") {
let input = """
{
"name": "Taylor Swift",
"address": {
"street": "555, Taylor Swift Avenue",
"city": "Nashville"
}
}
"""

// more code to come
}

这会在代码中创建一个JSON字符串。如果你不太熟悉JSON,则最好查看与之匹配的Swift结构-你可以将它们直接放入按钮操作中,也可以放在ContentView结构外部,这无关紧要:

1
2
3
4
5
6
7
8
9
struct User: Codable {
var name: String
var address: Address
}

struct Address: Codable {
var street: String
var city: String
}

希望你现在可以看到JSON包含的内容:一个用户有一个名称字符串和一个地址,而地址是一个街道字符串和一个城市字符串。

现在最好的是:我们可以将JSON字符串转换为Data类型(可以使用的类型Codable),然后将其解码为User实例:

1
2
3
4
5
let data = Data(input.utf8)
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: data) {
print(user.address.street)
}

如果运行该程序并点击按钮,你应该会看到打印出的地址-尽管为了避免疑问,我应该说这不是她的真实地址!

Codable经历的级别数量没有限制-重要的是定义的结构与JSON字符串匹配。

这使我们到了该项目的概述的结尾,因此请继续将ContentView.swift重置为其原始状态。

参考资料

查看下一天的SwiftUI学习笔记

关于100days英文课程