创建和组合视图
此部分将指引你构建一个发现和分享您喜爱地方的 iOS应用 ——Landmarks
。首先我们来构建显示地标详细信息的视图。Landmarks
使用stacks
将image
、text
等组件进行组合和分层,以此来给视图布局。如果想给视图添加地图,我们需要引入标准MapKit
组件。在我们调整设计时,Xcode 可以作出实时反馈,以便我们看到这些调整是如何转换为代码的。
下载项目文件并按照以下步骤操作。
* 预计完成时间:40 分钟
* 初始项目文件:下载
1. 创建一个新项目并且浏览画布
创建一个使用 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()
}
}
2. 自定义文字视图
我们可以更改代码,或者使用检查器帮助我们编写代码,来自定义视图的显示。
在构建 Landmarks
的过程中,我们可以使用任何方式来实现:编写源码、修改画布、或者通过检查器,无论使用哪种工具,代码都会保持更新。
接下来,我们使用检查器来自定义文字视图。
2.1 在预览中,按住 Command
并单击问候语来显示编辑窗口,然后选择 Inspect
。
编辑窗口会显示可以修改的不同属性,具体取决于其视图类型。
2.2 用检查器将文本改为 Turtle Rock
,这是在 yinBiao 中显示的第一个地标的名字。
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()
}
}
3. 用 Stack 组合视图
在上一节创建标题视图后,我们来添加用来显示地标的详细信息的文字视图,比如公园的名称和所在的州。
在创建 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()
}
}
4. 自定义图片视图
搞定名称和位置视图后,我们来给地标添加图片。
这不需要添加很多代码,只需要创建一个自定义视图,然后给图片加上遮罩、边框和阴影即可。
首先将图片添加到项目的 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()
}
}
5. 同时使用 UIKit 和 SwiftUI
至此,我们已准备好创建地图视图了,接下来使用 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. 编写详情视图
现在我们完成了所需的所有组件:名称、地点、圆形图片和地图。
继续使用目前的工具,将这些组件组合起来变成符合最终设计的详情视图。
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()
}
}