#center#80%

创建和组合视图

此部分将指引你构建一个发现和分享您喜爱地方的 iOS应用 —— Landmarks 。首先我们来构建显示地标详细信息的视图。

Landmarks 使用 stacksimagetext 等组件进行组合和分层,以此来给视图布局。如果想给视图添加地图,我们需要引入标准 MapKit 组件。在我们调整设计时,Xcode 可以作出实时反馈,以便我们看到这些调整是如何转换为代码的。

下载项目文件并按照以下步骤操作。

* 预计完成时间:40 分钟
* 初始项目文件:下载

1. 创建一个新项目并且浏览画布

创建一个使用 SwiftUI 的 Xcode 项目,先浏览一下画布,预览区和 SwiftUI 的模版代码。

要在 Xcode 中使用画布,需要确保你的 Mac 系统为 macOS Catalina 10.15。

#center#80%

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

#center#80%

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

#center#80%

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

#center#80%

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()
    }
}

#center#30%

1.5 在画布中,单击 Resume 来显示预览。

Tip:如果画布没有出现,可以选择 Editor > Editor and Canvas 来显示。

#center#80%

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()
    }
}

#center#30%

2. 自定义文字视图

我们可以更改代码,或者使用检查器帮助我们编写代码,来自定义视图的显示。

在构建 Landmarks 的过程中,我们可以使用任何方式来实现:编写源码、修改画布、或者通过检查器,无论使用哪种工具,代码都会保持更新。

#center#80%

接下来,我们使用检查器来自定义文字视图。

2.1 在预览中,按住 Command 并单击问候语来显示编辑窗口,然后选择 Inspect

编辑窗口会显示可以修改的不同属性,具体取决于其视图类型。

#center#80%

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

#center#80%

2.3 将 Font 修饰符改为 Title

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

#center#80%

为了自定义 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()
    }
}

#center#30%

视图是代码的真实反馈,所以当我们使用检查器修改或删除修饰符时,Xcode 也会立即更新我们的代码。

2.5 这次我们在代码编辑区按住 Command ,单击 Text 的声明来打开检查器,然后选择 Inspect 。单击 Color 菜单并且选择 Inherited ,这样文字又变回了黑色。

#center#80%

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()
    }
}

#center#30%

3. 用 Stack 组合视图

在上一节创建标题视图后,我们来添加用来显示地标的详细信息的文字视图,比如公园的名称和所在的州。

在创建 SwiftUI 视图时,我们可以在视图的 body 属性中描述其内容、布局和行为。由于 body 属性仅返回单个视图,所以我们可以使用 Stack 来组合和嵌入多个视图,让它们以水平、垂直或从后到前的顺序组合在一起。

在本节中,我们使用水平的 stack 来显示公园的详细信息,再用垂直的 stack 将标题放在详细信息的上面。

#center#80%

我们可以使用 Xcode 的结构编辑功能将一个视图嵌入到一个容器里,也可以使用检查器或 help 找到更多帮助。

3.1 按住 Command 并单击文字视图的初始化方法,在编辑窗口中选择 Embed in VStack

#center#80%

接下来,我们从 Library 中拖一个 Text view 添加到 stack 中。

3.2 单击 Xco​​de 右上角的加号按钮 (+) 打开 Library ,然后拖一个 Text view ,放在代码中 Turtle Rock 的后面。

#center#80%

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()
    }
}

#center#30%

调整地点视图以达到布局需求。

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()
    }
}

#center#30%

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()
    }
}

#center#30%

接下来,我们在地点的右侧添加另一个文字视图来显示公园所在的州。

3.6 在画布中按住 Command ,单击 Joshua Tree National Park ,然后选择 Embed in HStack

#center#80%

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()
    }
}

#center#30%

3.8 在水平 stack 中添加一个 Spacer 来分割及固定 Joshua Tree National ParkCalifornia ,这样它们就会共享整个屏幕宽度。

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()
    }
}

#center#30%

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()
    }
}

#center#30%

4. 自定义图片视图

搞定名称和位置视图后,我们来给地标添加图片。

这不需要添加很多代码,只需要创建一个自定义视图,然后给图片加上遮罩、边框和阴影即可。

#center#80%

首先将图片添加到项目的 asset catalog 中。

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

#center#80%

接下来,创建一个新的 SwiftUI 视图来自定义图片视图。

4.2 选择 File > New > File 打开模板选择器。在 User Interface 中,选中 SwiftUI View ,然后单击 Next 。将文件命名为 CircleImage.swift ,然后单击 Create

#center#80%

现在准备工作已完成。

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()
    }
}

#center#30%

4.4 调用 .clipShape(Circle()) ,将图像裁剪成圆形。

Circle 可以当做一个蒙版的形状,也可以通过 strokefill 绘制视图。

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()
    }
}

#center#30%

4.5 创建另一个 gray strokecircle ,然后将其作为 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()
    }
}

#center#30%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

5. 同时使用 UIKit 和 SwiftUI

至此,我们已准备好创建地图视图了,接下来使用 MapKit 中的 MKMapView 类来渲染地图。

SwiftUI 中使用 UIView 子类,需要将其他视图包装在遵循 UIViewRepresentable 协议的 SwiftUI 视图中。 SwiftUI 包含了和 WatchKitAppKit 视图类似的协议。

#center#80%

首先,我们创建一个可以呈现 MKMapView 的自定义视图。

5.1 选择 File > New > File ,选择 iOS 平台,选择 SwiftUI View 模板,然后单击 Next 。将新文件命名为 MapView.swift ,然后单击 Create

#center#80%

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:) 用来创建一个 MKMapViewupdateUIView(_: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 AgainResume 按钮。

片刻之后,你会看到 Joshua Tree National Park 的地图,这是 Turtle Rock 的故乡。

#center#80%

6. 编写详情视图

现在我们完成了所需的所有组件:名称、地点、圆形图片和地图。

继续使用目前的工具,将这些组件组合起来变成符合最终设计的详情视图。

#center#80%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

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()
    }
}

#center#30%

Made with in Shangrao,China By 老雷

Copyright © devler.cn 1987 - Present

赣ICP备19009883号-1