-
Notifications
You must be signed in to change notification settings - Fork 317
Creating and Combining Views

此部分将指引你构建一个发现和分享您喜爱地方的 iOS app ——
Landmarks。首先我们来构建显示地标详细信息的视图。
Landmarks使用stacks将image、text等组件进行组合和分层,以此来给视图布局。如果想给视图添加地图,我们需要引入标准MapKit组件。在我们调整设计时,Xcode 可以作出实时反馈,以便我们看到这些调整是如何转换为代码的。下载项目文件并按照以下步骤操作。
- 预计完成时间:40 分钟
- 初始项目文件:下载
创建一个使用 SwiftUI 的 Xcode 项目,先浏览一下画布,预览区和 SwiftUI 的模版代码。
要在 Xcode 中使用画布,需要确保你的 Mac 系统为 macOS Catalina 10.15。

1.1 打开 Xcode ,在 Xcode 的启动窗口中单击 Create a new Xcode project ,或选择 File > New > Project 。

1.2 选择 iOS 平台, Single View App 模板,然后单击 Next 。

1.3 输入 Landmarks 作为项目名,勾选 Use SwiftUI 复选框,然后单击 Next 。选择一个位置保存此项目。

1.4 在项目导航栏中,选中 ContentView.swift 。
SwiftUI view 文件默认声明了两个结构体。第一个结构体遵循 View 协议,描述视图的内容和布局。第二个结构体声明该视图的预览。
ContentView.swift
import SwiftUI
//
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
//
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
1.5 在画布中,单击 Resume 来显示预览。
Tip:如果画布没有出现,可以选择
Editor>Editor and Canvas来显示。

1.6 在 body 属性中,将 Hello World 更改为自己的问候语。
更改代码的同时,预览也会实时更新。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
//
Text("Hello SwiftUI!")
//
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
我们可以更改代码,或者使用检查器帮助我们编写代码,来自定义视图的显示。
在构建 Landmarks 的过程中,我们可以使用任何方式来实现:编写源码、修改画布、或者通过检查器,无论使用哪种工具,代码都会保持更新。

接下来,我们使用检查器来自定义文字视图。
2.1 在预览中,按住 Command 并单击问候语来显示编辑窗口,然后选择 Inspect 。
编辑窗口会显示可以修改的不同属性,具体取决于其视图类型。

2.2 用检查器将文本改为 Turtle Rock ,这是在 app 中显示的第一个地标的名字。

2.3 将 Font 修饰符改为 Title 。
这个修改会让文本使用系统字体,之后它就能正确适应用户的偏好字体大小和设置。

为了自定义 SwiftUI 视图,我们可以调用称为 修饰符(modifier)的方法。修饰符会包装视图来更改其显示或其他属性。每个修饰符都会返回一个新视图,因此常常链式调用多个修饰符。
2.4 在代码中添加 foregroundColor(.green) 修饰符,将文本的颜色更改为绿色。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Turtle Rock")
.font(.title)
//
.foregroundColor(.green)
//
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
视图是代码的真实反馈,所以当我们使用检查器修改或删除修饰符时,Xcode 也会立即更新我们的代码。
2.5 这次我们在代码编辑区按住 Command ,单击 Text 的声明来打开检查器,然后选择 Inspect 。单击 Color 菜单并且选择 Inherited ,这样文字又变回了黑色。

2.6 注意,Xcode 会自动针对修改来更新代码,例如删除了 foregroundColor(.green) 修饰符。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Turtle Rock")
.font(.title)
//
//
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
在上一节创建标题视图后,我们来添加用来显示地标的详细信息的文字视图,比如公园的名称和所在的州。
在创建 SwiftUI 视图时,我们可以在视图的 body 属性中描述其内容、布局和行为。由于 body 属性仅返回单个视图,所以我们可以使用 Stack 来组合和嵌入多个视图,让它们以水平、垂直或从后到前的顺序组合在一起。
在本节中,我们使用水平的 stack 来显示公园的详细信息,再用垂直的 stack 将标题放在详细信息的上面。

我们可以使用 Xcode 的结构编辑功能将一个视图嵌入到一个容器里,也可以使用检查器或 help 找到更多帮助。
3.1 按住 Command 并单击文字视图的初始化方法,在编辑窗口中选择 Embed in VStack 。

接下来,我们从 Library 中拖一个 Text view 添加到 stack 中。
3.2 单击 Xcode 右上角的加号按钮 (+) 打开 Library ,然后拖一个 Text view ,放在代码中 Turtle Rock 的后面。

3.3 将 Placeholder 改成 Joshua Tree National Park 。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Turtle Rock")
.font(.title)
//
Text("Joshua Tree National Park")
//
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
调整地点视图以达到布局需求。
3.4 将地点视图的 font 设置成 subheadline 。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Turtle Rock")
.font(.title)
Text("Joshua Tree National Park")
//
.font(.subheadline)
//
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
3.5 编辑 VStack 的初始化方法,将 view 以 leading 方式对齐。
默认情况下, stacks 会将内容沿其轴居中,并设置适合上下文的间距。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
//
VStack(alignment: .leading) {
//
Text("Turtle Rock")
.font(.title)
Text("Joshua Tree National Park")
.font(.subheadline)
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
接下来,我们在地点的右侧添加另一个文字视图来显示公园所在的州。
3.6 在画布中按住 Command ,单击 Joshua Tree National Park ,然后选择 Embed in HStack 。

3.7 在地点后新加一个 text view,将 Placeholder 修改成 California ,然后将 font 设置成 subheadline 。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
.font(.subheadline)
//
Text("California")
.font(.subheadline)
//
}
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
3.8 在水平 stack 中添加一个 Spacer 来分割及固定 Joshua Tree National Park 和 California ,这样它们就会共享整个屏幕宽度。
spacer 能撑开 stack 所包含的视图,使它们共用其父视图的所有空间,而不是仅通过其内容定义其大小。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
.font(.subheadline)
//
Spacer()
//
Text("California")
.font(.subheadline)
}
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
3.9 最后,用 padding() 修饰符给地标的名称和信息留出一些空间。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
//
.padding()
//
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
搞定名称和位置视图后,我们来给地标添加图片。
这不需要添加很多代码,只需要创建一个自定义视图,然后给图片加上遮罩、边框和阴影即可。

首先将图片添加到项目的 asset catalog 中。
4.1 在项目的 Resources 文件夹中找到 turtlerock.png ,将它拖到 asset catalog 的编辑器中。 Xcode 会给图片创建一个 image set 。

接下来,创建一个新的 SwiftUI 视图来自定义图片视图。
4.2 选择 File > New > File 打开模板选择器。在 User Interface 中,选中 SwiftUI View ,然后单击 Next 。将文件命名为 CircleImage.swift ,然后单击 Create 。

现在准备工作已完成。
4.3 使用 Image(_:) 初始化方法将文字视图替换为 Turtle Rock 的图片。
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
//
Image("turtlerock")
//
}
}
struct CircleImage_Preview: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
4.4 调用 .clipShape(Circle()) ,将图像裁剪成圆形。
Circle 可以当做一个蒙版的形状,也可以通过 stroke 或 fill 绘制视图。
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
Image("turtlerock")
//
.clipShape(Circle())
//
}
}
struct CircleImage_Preview: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
4.5 创建另一个 gray stroke 的 circle ,然后将其作为 overlay 添加到图片上,形成图片的边框。
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
Image("turtlerock")
.clipShape(Circle())
//
.overlay(
Circle().stroke(Color.gray, lineWidth: 4))
//
}
}
struct CircleImage_Preview: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
4.6 接来下,添加一个半径为 10 点的阴影。
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
Image("turtlerock")
.clipShape(Circle())
.overlay(
Circle().stroke(Color.gray, lineWidth: 4))
//
.shadow(radius: 10)
//
}
}
struct CircleImage_Preview: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
4.7 将边框的颜色改为 white ,完成图片视图。
CircleImage.swift
import SwiftUI
struct CircleImage: View {
var body: some View {
Image("turtlerock")
.clipShape(Circle())
.overlay(
//
Circle().stroke(Color.white, lineWidth: 4))
//
.shadow(radius: 10)
}
}
struct CircleImage_Preview: PreviewProvider {
static var previews: some View {
CircleImage()
}
}
至此,我们已准备好创建地图视图了,接下来使用 MapKit 中的 MKMapView 类来渲染地图。
在 SwiftUI 中使用 UIView 子类,需要将其他视图包装在遵循 UIViewRepresentable 协议的 SwiftUI 视图中。 SwiftUI 包含了和 WatchKit 、 AppKit 视图类似的协议。

首先,我们创建一个可以呈现 MKMapView 的自定义视图。
5.1 选择 File > New > File ,选择 iOS 平台,选择 SwiftUI View 模板,然后单击 Next 。将新文件命名为 MapView.swift ,然后单击 Create 。

5.2 给 MapKit 添加 import 语句,声明 MapView 类型遵循 UIViewRepresentable 。
可以忽略 Xcode 的错误,接下来的几步会解决这些问题。
MapView.swift
import SwiftUI
//
import MapKit
struct MapView: UIViewRepresentable {
//
var body: some View {
Text("Hello World")
}
}
struct MapView_Preview: PreviewProvider {
static var previews: some View {
MapView()
}
}UIViewRepresentable 协议需要实现两个方法: makeUIView(context:) 用来创建一个 MKMapView, updateUIView(_:context:) 用来配置视图并响应修改。
5.3 用 makeUIView(context:) 方法替换 body 属性,该方法创建并返回一个空的 MKMapView。
MapView.swift
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
//
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
//
}
}
struct MapView_Preview: PreviewProvider {
static var previews: some View {
MapView()
}
}5.4 实现 updateUIView(_:context:) 方法,给地图视图设置坐标,使其在 Turtle Rock 上居中。
MapView.swift
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
//
func updateUIView(_ view: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(
latitude: 34.011286, longitude: -116.166868)
let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
//
}
struct MapView_Preview: PreviewProvider {
static var previews: some View {
MapView()
}
}当预览处于 static mode 时仅显示 SwiftUI 视图 。因为 MKMapView 是一个 UIView 的子类,所以需要切换到实时模式才能看到地图。
5.5 单击 Live Preview 可将预览切换为实时模式,有时也会用到 Try Again 或 Resume 按钮。
片刻之后,你会看到 Joshua Tree National Park 的地图,这是 Turtle Rock 的故乡。

现在我们完成了所需的所有组件:名称、地点、圆形图片和地图。
继续使用目前的工具,将这些组件组合起来变成符合最终设计的详情视图。

6.1 在项目导航中,选中 ContentView.swift 文件。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}6.2 把之前的的 VStack 嵌入到另一个新 的 VStack 中。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
//
VStack {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
//
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6.3 将自定义的 MapView 添加到 stack 顶部,使用 frame(width:height:) 方法来设置 MapView 的大小。
如果仅指定了 height 参数,视图会自动调整其内容的宽度。此节中, MapView 会展开并填充所有可用空间。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
MapView()
.frame(height: 300)
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6.4 单击 Live Preview 按钮,查看渲染的地图。
在此过程中,我们可以继续编辑视图。
6.5 将 CircleImage 添加到 stack 中。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
MapView()
.frame(height: 300)
//
CircleImage()
//
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6.6 为了将图片视图盖在地图视图上面,我们需要给图片设置 -130 点的偏移量,并从底部填充 -130 点。
图片向上移动后,就为文本腾出了空间。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
MapView()
.frame(height: 300)
CircleImage()
//
.offset(y: -130)
.padding(.bottom, -130)
//
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6.7 在外部 VStack 的底部添加一个 spacer ,将内容推到屏幕顶端。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
MapView()
.frame(height: 300)
CircleImage()
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
//
Spacer()
//
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
6.8 最后,为了将地图内容扩展到屏幕的上边缘,需要给地图视图添加 edgesIgnoringSafeArea(.top) 修饰符。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
MapView()
//
.edgesIgnoringSafeArea(.top)
//
.frame(height: 300)
CircleImage()
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
Spacer()
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SwiftUI 纲要 - 绘制与动画 - App 设计与布局 - 框架集成