[SwiftUI 100 天] 登月计划 - part1 ScrollView

1,894 阅读7分钟
译自 Moonshot: Introduction
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

Moonshot 介绍

在这个项目中我们将构建一个让用户了解 NASA 的阿波罗航天计划相关任务和宇航员的 app 。你不仅会更精通 Codable, 重要的是可以接触到滚动视图,导航,以及更有趣的布局。

是的,你仍会做一些 List,Text 等视图的实践,但也会开始解决一些在 SwiftUI 中很重要的问题 —— 如何让一个图像正确适应空间?如何用计算属性整理代码?如何组合更小的视图到更大的视图以便保持项目结构的条理?

要做的事情很多,所以我们开始吧:用 Single View App 模板创建一个新的 iOS app ,取名叫“Moonshot” 。按照惯例,在开始项目之前,让我们先近距离接触一些你需要掌握的新技能...

译自 Resizing images to fit the screen using GeometryReader

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

当我们在 SwiftUI 中创建一个 Image 视图时,它会根据内容的尺寸自动排列自身。因此,如果图片是 1000x500 ,那么 Image 视图也会是 1000x500 。这个机制有时候正是你想要的,但多数情况下你会想要图像以一个更小的尺寸显示,而我将在这个项目中向你演示如何做到这一点,同时也会介绍如何利用一个叫 GeometryReader 的新类型,帮助我们把图像适配到设备屏幕的宽度。

首先,添加某个图片到你的工程。图片内容无关紧要,只要比屏幕宽就行。我的图片名叫 “Example” ,显然你可以在代码中替换成你自己的图片名称。

让我们把图像绘制到屏幕:

struct ContentView: View {
    var body: some View {
        VStack {
            Image("Example")
        }
    }
}

即便是在预览中,你也能看出图像对于可用的显示空间来说太大了。和其他视图一样, Image 也有相同的 frame() modifier ,所以你可以尝试把它缩小,像这样:

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

但是,上面的代码不会管用 —— 你的图像还是以全尺寸显示。如果你想知道为什么,可以再仔细看一看预览窗口:你会发现图像是全尺寸的,但图像中央有一个 300x300 的蓝色盒子。也就是说,图像的 frame 已经正确设置了,但图像内容却还是原始尺寸。

尝试把图像代码改成下面这样:

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

现在事情看起来更明显了:我们的图像视图的确是 300x300 ,但这不是我们想要的:

如果你希望图像的内容也调整大小,我们需要用resizable() modifier ,像这样:

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

这样一来好些了,但仅仅只是好一点。图像尺寸虽然正确,但很可能被挤压了。我的图片不是正方形的,所以当它被调整大小塞进一个正方形时,看起来是变形的。

为了解决这个问题,我们需要让图像按比例缩放。实现这一点要用到 aspectRatio() modifier 。它让我们提供一个要应用的比例作为参数,但如果我们忽略这个参数,SwiftUI 会自动使用原始的高宽比。

当谈到 “应该如何应用(比例)” 的部分,SwiftUI 把它叫做 content mode ,并且给了我们两个选项: .fit 表示图像会把自己整个装进容器,即使会让视图部分留白。 而 .fill 表示视图不会留白,也就意味着图像的一部分会跑出容器。

你可以自行尝试,看一下两者的区别。首先是应用 .fit 模式:

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

然后应用 .fill 模式:

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

当我们只要固定尺寸的图像时,上面的方案可以很好满足。但是我们经常需要图像能自动放大一边或者两边以填满屏幕。就是说,不再硬编码宽度为 300 ,而是 ”让这个图像填满屏幕宽度“ 。

SwiftUI 给了我们一个专门的类型,叫 GeometryReader,它非常地强大。是的,我知道 SwiftUI 里有很多东西都很强大,但是老实说:你能用 GeometryReader 做到的事情会让你大吃一惊。

我们还会在后面的项目中介绍更多 GeometryReader 的细节,不过眼下我们将用它来完成一件事:确保图像填满容器视图的完整宽度。

就像我们用过的其他视图,GeometryReader 也是一个视图,除了构建时它传给我们一个 GeometryProxy 对象。这个对象可以用来查询环境信息:容器有多大?我们的视图位置在哪里?是否有安全区 insets ?等等。

我们可以这个几何代理来设置图像的宽度,像这样:

VStack {
    GeometryReader { geo in
        Image("Example")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geo.size.width, height: 300)
    }
}

这样一来,无论我们用的是什么设备,图像都将填满整个屏幕宽度。

作为最后一个小技巧,我们可以从图像上移除 height 设置,像这样:

VStack {
    GeometryReader { geo in
        Image("Example")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geo.size.width)
    }
}

因为我们已经给到 SwiftUI 足够的信息,它能够自动搞明白高度:它知道原始宽度,知道目标宽度,也知道 content mode ,所以它能理解目标高度应该如何针对目标宽度成比例设置。

译自 How ScrollView lets us work with scrolling data

ScrollView 的工作方式

你已经知道 ListForm 可以让我们创建可滚动的数据表格,但是当我们想要滚动任意的视图 —— 比如,某些我们手动创建的视图 —— 我们需要求助于 SwiftUI 的 ScrollView

ScrollView 可以横向滚动,竖向滚动,或者两个方向都滚动,你还可以控制系统是否显示滚动指示器 —— 滚动指示器是一种让用户可以感知内容大小的小滚动条。 当我们把视图放进滚动视图中时,SwiftUI 能够自动算出内容的大小,以方便用户从一头滚动到另一头。

举个例子,我们可以创建一个有 100 个文本视图的滚动列表,像这样:

ScrollView(.vertical) {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \($0)")
                .font(.title)
        }
    }
}

在模拟器中运行,你会发现你可以在滚动视图上自由地拖曳,滚向底部,你还会发现 ScrollView 处理安全区域的方式正如 ListForm—— 它们的内容会处于屏幕底部横线的下方,但由于额外的 padding ,最终的视图都是完整可见的。

你可能还会留意到,只能扣中间区域才能滚动,这有点恼人 —— 一般要整个区域都可以滚动。为了实现这个行为,我们要让 VStack 占据更多的空间,同时保留默认居中的方式不变,像这样:

ScrollView(.vertical) {
    VStack(spacing: 10) {
        ForEach(0..<100) {
            Text("Item \($0)")
                .font(.title)
        }
    }
    .frame(maxWidth: .infinity)
}

现在你可以在屏幕上的任何地方点击并拖拽滚动,这样就容易使用多了。

看起来很简单对吧? ScrollView 的确明显比原来的 UIScrollView 容易使用。不过,你需要意识到一个关键点:当我们添加视图到一个滚动视图时,它们是被立即创建的。

为了演示这一点,我们在一个常规的文本视图上套一个包装,像这样:

struct CustomText: View {
    var text: String

    var body: some View {
        Text(text)
    }

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

然后把它用在 ForEach中:

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

结果看起来是一样,但当你运行 app ,你会在 Xcode 的日志里看到 100 行的 “Creating a new CustomText” 打印 —— SwiftUI 不会等你向下滚动将要看到视图的时候创建,它只会立即创建它们。

你可以 List上尝试相同的实验,像这样:

List {
    ForEach(0..<100) {
        CustomText("Item \($0)")
            .font(.title)
    }
}

运行这份代码,你会发现它是懒加载的:只有在真正用到的时候才创建 CustomText 实例。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~