-
Notifications
You must be signed in to change notification settings - Fork 314
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() } }