[译] SwiftUI 官方教程 (二)

6,853 阅读11分钟

由于 API 变动,此文章部分内容已失效,最新完整中文教程及代码请查看 github.com/WillieWangW…

微信技术群

SwiftUI 代表未来构建 App 的方向,欢迎加群一起交流技术,解决问题。

加群现在需要申请了,可以先加我微信,备注 "SwiftUI",我会拉你进群。

创建和组合 View

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

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

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

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

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

SwiftUI 的 app 模板来创建一个新的 Xcode 项目,并且浏览一下这个 canvas。

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

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

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

1.4 在 Project navigator 中,选中 ContentView.swift

默认情况下, SwiftUI view 文件声明了两个结构体。第一个结构体遵循 View 协议,描述 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 在 canvas 中,单击 Resume 来显示预览。

Tip:如果没有 canvas ,选择 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. 自定义 Text View

为了自定义 view 的显示,我们可以自己更改代码,或者使用 inspector 来帮助我们编写代码。

在构建 Landmarks 的过程中,我们可以使用任何编辑器来工作:编写源码、修改 canvas、或者通过 inspectors ,无论使用哪种工具,代码都会保持更新。

接下来,我们使用 inspector 来自定义 text view

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

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

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

2.3 将 Font 修改为 Title

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

Edit the code by hand to add the .color(.green) modifier; this changes the text’s color to green.

To customize a SwiftUI view, you call methods called modifiers. Modifiers wrap a view to change its display or other properties. Each modifier returns a new view, so it’s common to chain multiple modifiers, stacked vertically.

2.4 在代码中添加 .color(.green) ,将文本的颜色更改为绿色。

如果想自定义 SwiftUI 的 view,我们可以调用一类叫做 modifiers 的方法。这类方法通过包装一个 view 来改变它的显示或者其他属性。每个 modifiers 方法会返回一个新的 view,因此我们可以链式调用多个 modifiers 方法。

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Turtle Rock")
            .font(.title)
            .color(.green)
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

view 的真实来源是其实是代码,当我们使用 inspector 修改或删除 modifiers 时,Xcode 会立即更新我们的代码。

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

2.6 注意,Xcode 会自动针对修改来更新代码,例如删除了 .color(.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. 用 Stacks 组合 View

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

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

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

我们可以使用 Xcode 的编辑功能将 view 嵌入到一个容器里,也可以使用 inspector 或者 help 找到更多帮助。

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

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

3.2 单击 Xco​​de 右上角的加号按钮 (+) 打开 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()
    }
}

调整地点 view 以满足布局需求。

3.4 将地点 view 的 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)3
        }
    }
}

struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

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

3.6 在 canvas 中按住 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 ParkCalifornia ,这样它们就会共享整个屏幕宽度。

spacer 能展开它包含的 view ,使它们共用其父 view 的所有空间,而不是仅通过其内容定义其大小。

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. 自定义 Image View

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

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

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

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

接下来,创建一个新的 SwiftUI view 来自定义 image view。

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

现在准备工作已完成。

4.3 使用 Image(_:) 初始化方法将 text view 替换为 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 可以当做一个蒙版的形状,也可以通过 strokefill 形成 view。

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

4.6 接来下,添加一个半径为 10 point 的阴影。

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 ,完成 image view。

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

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

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

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

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:) 用来创建一个 MKMapViewupdateUIView(_:context:) 用来配置 view 并响应修改。

5.3 用 makeUIView(context:) 方法替换 body 属性,该方法创建并返回一个空的 MKMapView

MapView.swift

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
	
	typealias UIViewType = MKMapView

    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
        return MKMapView(frame: .zero)
    }
}

struct MapView_Preview: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

5.4 实现 updateUIView(_:context:) 方法,给 map view 设置坐标,使其在 Turtle Rock 上居中。

MapView.swift

import SwiftUI
import MapKit

struct MapView : UIViewRepresentable {
    
    typealias UIViewType = MKMapView
    
    func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
        return MKMapView(frame: .zero)
    }
    
    func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
        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)
        uiView.setRegion(region, animated: true)
    }
}

struct MapView_Preview: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

当预览处于 static mode 时仅显示 SwiftUI view 。因为 MKMapView 是一个 UIView 的子类,所以需要切换到实时模式才能看到地图。

5.5 单击 Live Preview 可将预览切换为实时模式,有时也会用到 Try AgainResume 按钮。

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

6. 编写详情 View

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

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

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 参数,view 会自动调整其内容的宽度。此节中, 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 按钮,在组合 view 中查看渲染的地图。

在此过程中,我们可以继续编辑 view。

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 为了将 image view 布局在 map view 的顶部,我们需要给图片设置 -130 points 的偏移量,并从底部填充 -130 points

图片向上移动后,就为文本腾出了空间。

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) 添加到 map view 中。

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