构建列表和导航-BuildingListsAndNavigation
在设置了基本的地标详细视图后,您需要提供一种方式让用户查看完整的地标列表,并查看每个位置的详细信息。
您将创建可以显示任何地标信息的视图,并动态生成一个滚动列表,用户可以点击该列表查看某个地标的详细视图。为了对用户界面进行微调,您将使用Xcode的画布在不同的设备尺寸下呈现多个预览。
下载项目文件开始构建这个项目,并按照以下步骤进行。
With the basic landmark detail view set up, you need to provide a way for users to see the full list of landmarks, and to view the details about each location.
You’ll create views that can show information about any landmark, and dynamically generate a scrolling list that a user can tap to see a detail view for a landmark. To fine-tune the UI, you’ll use Xcode’s canvas to render multiple previews at different device sizes.
Download the project files to begin building this project, and follow the steps below.
1.了解简单数据-Get to Know the Sample Data
在第一篇教程中,你将信息硬编码到所有的自定义视图中。在这里,您将学习如何将数据传递到您的自定义视图中进行显示。
下载入门项目并熟悉样本数据,即可开始学习。
In the first tutorial, you hard-coded information into all of your custom views. Here, you’ll learn to pass data into your custom views for display.
Get started by downloading the starter project and familiarizing yourself with the sample data.
- 第一步
在项目导航器中,选择Models>Landmark.swift。
Landmark.swift声明一个Landmark结构,该结构存储了应用程序需要显示的所有地标信息,并从landmarkData.json导入一个地标数据数组。
- 第二步
在项目导航器中,选择Resources>landmarkData.json。
您将在本教程的其余部分以及后续的所有内容中使用这个样本数据。
- 第三步
请注意,创建并组合视图-CreatingAndCombiningViews这一节中的 ContentView现在被命名为 LandmarkDetail。
在本教程和下面的每个教程中,您将创建更多的视图类型。
//
// Landmark.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable {
var id: Int
var name: String
fileprivate var imageName: String
fileprivate var coordinates: Coordinates
var state: String
var park: String
var category: Category
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
enum Category: String, CaseIterable, Codable, Hashable {
case featured = "Featured"
case lakes = "Lakes"
case rivers = "Rivers"
}
}
extension Landmark {
var image: Image {
ImageStore.shared.image(name: imageName)
}
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
Step 1
In the Project navigator, chooseModels>
Landmark.swift.
Landmark.swiftdeclares aLandmarkstructure that stores all of the landmark information the app needs to display, and imports an array of landmark data fromlandmarkData.json.Step 2
In the Project navigator, chooseResources>
landmarkData.json.You’ll use this sample data throughout the remainder of this tutorial, and for all that follow.
Step 3
Note that the
ContentViewtype fromCreating and Combining Viewsis now namedLandmarkDetail.You’ll create several more view types in this and each of the following tutorials.
2.创建行视图-Create the Row View
在本教程中,您将构建的第一个视图是一个用于显示每个地标详细信息的行。这个行视图将信息存储在它所显示的地标的属性中,因此一个视图可以显示任何地标。稍后,您将把多个行组合成一个地标列表。
The first view you’ll build in this tutorial is a row for displaying details about each landmark. This row view stores information in a property for the landmark it displays, so that one view can display any landmark. Later, you’ll combine multiple rows into a list of landmarks.
- 第一步
创建一个新的SwiftUI视图,命名为LandmarkRow.swift。
- 第二步
如果预览还不可见,选择 Editor > Canvas,然后点击Resume来显示画布。
- 第三步
添加landmark作为LandmarkRow的一个存储属性。
当你添加landmark属性时,预览停止工作,因为LandmarkRow类型在初始化时需要一个地标实例。
为了恢复预览,你需要修改预览提供者(preview provider)。
- 第四步
在 LandmarkRow_Previews 的 previews static属性中,将landmark参数添加到 LandmarkRow 初始化中,指定landmarkData数组的第一个元素。
预览显示的是文字,Hello World。
固定后,您就可以为该行建立布局了。
- 第五步
将现有的文本视图嵌入到HStack中。
- 第六步
修改文本视图,使用 landmark属性的 name。
- 第七步
在文本视图前添加图像,在文本视图后添加spacer,从而完成行
//
// LandmarkRow.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import SwiftUI
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image
.resizable()
.frame(width: 50, height: 50)
Text(landmark.name)
Spacer()
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
LandmarkRow(landmark: landmarkData[0])
}
}
//
// Landmark.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable {
var id: Int
var name: String
fileprivate var imageName: String
fileprivate var coordinates: Coordinates
var state: String
var park: String
var category: Category
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
enum Category: String, CaseIterable, Codable, Hashable {
case featured = "Featured"
case lakes = "Lakes"
case rivers = "Rivers"
}
}
extension Landmark {
var image: Image {
Image(imageName)
// ImageStore.shared.image(name: imageName)
}
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
//
// ModelData.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import Foundation
var landmarkData: [Landmark] = load("landmarkData.json")
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
Step 1
Create a new SwiftUI view, named
LandmarkRow.swift.Step 2
If the preview isn’t visible already, show the canvas by selectingEditor > Canvas, and then clickResume.
Step 3
Add
landmarkas a stored property ofLandmarkRow.When you add the
landmarkproperty, the preview stops working, because theLandmarkRowtype needs a landmark instance during initialization.To fix the preview, you’ll need to modify the preview provider.
Step 4
In the
previewsstatic property ofLandmarkRow_Previews, add the landmark parameter to theLandmarkRowinitializer, specifying the first element of thelandmarkDataarray.The preview displays the text,Hello World.
With that fixed, you can build the layout for the row.
Step 5
Embed the existing text view in an
HStack.Step 6
Modify the text view to use the
landmarkproperty’sname.Step 7
Complete the row by adding an image before the text view, and a spacer after it.
3.自定义行预览-Customize the Row Preview
Xcode的画布会自动识别并显示当前编辑器中符合 "PreviewProvider "协议的任何类型。预览提供者会返回一个或多个视图,并提供配置大小和设备的选项。
您可以自定义从预览提供者返回的内容,以准确地呈现对您最有帮助的预览。
Xcode’s canvas automatically recognizes and displays any type in the current editor that conforms to the
PreviewProviderprotocol. A preview provider returns one or more views, with options to configure the size and device.You can customize the returned content from a preview provider to render exactly the previews that are most helpful to you.
第一步
在LandmarkRow_Previews中,更新landmark参数为landmarkData数组中的第二个元素。
预览会立即改变,显示第二个样本地标而不是第一个。
第二步
使用previewLayout(_:)修改器来设置一个近似于列表中一行的大小。
您可以使用Group从一个预览提供者返回多个预览。
第三步
将返回的行包在Group中,再将第一行添加回来。
Group是一个用于分组视图内容的容器。Xcode将该组的子视图作为单独的预览显示在画布中。
第四步
为了简化代码,将previewLayout(_:)调用移到组的子声明的外面。
视图的子声明继承了视图的上下文设置,如预览配置。
您在预览提供者中编写的代码只改变Xcode在画布中显示的内容。
//
// LandmarkRow.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import SwiftUI
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image
.resizable()
.frame(width: 50, height: 50)
Text(landmark.name)
Spacer()
}
}
}
struct LandmarkRow_Previews: PreviewProvider {
static var previews: some View {
Group {
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
.previewLayout(.fixed(width: 300, height: 70))
}
}
Step 1
In
LandmarkRow_Previews, update thelandmarkparameter to be the second element in thelandmarkDataarray.The preview immediately changes to show the second sample landmark instead of the first.
Step 2
Use the
previewLayout(_:)modifier to set a size that approximates a row in a list.You can use a
Groupto return multiple previews from a preview provider.Step 3
Wrap the returned row in a
Group, and add the first row back again.
Groupis a container for grouping view content. Xcode renders the group’s child views as separate previews in the canvas.Step 4
To simplify the code, move the
previewLayout(_:)call to the outside of the group’s child declarations.A view’s children inherit the view’s contextual settings, such as preview configurations.
The code you write in a preview provider only changes what Xcode displays in the canvas.
4.创建地标列表-Create the List of Landmarks
当你使用SwiftUI的List类型时,你可以显示一个平台特定的视图列表。列表中的元素可以是静态的,比如到目前为止你已经创建的堆栈的子视图,也可以是动态生成的。你甚至可以混合静态和动态生成的视图。
When you use SwiftUI’s
Listtype, you can display a platform-specific list of views. The elements of the list can be static, like the child views of the stacks you’ve created so far, or dynamically generated. You can even mix static and dynamically generated views.
- 第1步
创建一个新的SwiftUI视图,命名为LandmarkList.swift。
- 第二步
将默认的 "Text "view替换为 "List",并提供 "LandmarkRow "实例,将前两个地标作为列表的子节点。
预览以适合iOS的列表样式显示两个地标。
//
// LandmarkList.swift
// Landmarks
//
// Created by EchoSun on 2020/12/22.
//
import SwiftUI
struct LandmarkList: View {
var body: some View {
List {
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
Step 1
Create a new SwiftUI view, named
LandmarkList.swift.Step 2
Replace the default
Textview with aList, and provideLandmarkRowinstances with the first two landmarks as the list’s children.The preview shows the two landmarks rendered in a list style that’s appropriate for iOS.
5.创建动态列表-Make the List Dynamic
你可以不单独指定一个列表的元素,而是直接从一个集合中生成行。
您可以通过传递您的数据集合和一个为集合中的每个元素提供视图的闭包来创建一个显示集合元素的列表。列表通过使用提供的闭包将集合中的每个元素转换为一个子视图。
Instead of specifying a list’s elements individually, you can generate rows directly from a collection.
You can create a list that displays the elements of collection by passing your collection of data and a closure that provides a view for each element in the collection. The list transforms each element in the collection into a child view by using the supplied closure.
- 步骤1
删除两个静态的地标行,而将landmarkData传递给Listinitializer。
Lists的工作原理是 identifiable data。你可以通过两种方式之一来使你的数据可识别:通过与你的数据一起传递一个键路径到一个唯一标识每个元素的属性,或者通过使你的数据类型符合Identifiable协议。
- 第二步
通过从闭包中返回一个LandmarkRow来完成动态生成的列表。
这将为 "landmarkData "数组中的每个元素创建一个 "LandmarkRow"。
接下来,你将通过在Landmark类型中添加Identifiableconformance 来简化List代码。
- 第三步
切换到 "Landmark.swift "并声明符合 "Identifiable "协议。
由于Landmark类型已经具有Identifiableprotocol所要求的id属性,所以没有更多的工作要做。
- 第四步
切换回LandmarkList.swift并删除id参数。
从现在开始,您可以直接使用Landmark元素的集合。
Step 1
Remove the two static landmark rows, and instead pass
landmarkDatato theListinitializer.Lists work with identifiable data. You can make your data identifiable in one of two ways: by passing along with your data a key path to a property that uniquely identifies each element, or by making your data type conform to the
Identifiableprotocol.Step 2
Complete the dynamically-generated list by returning a
LandmarkRowfrom the closure.This creates one
LandmarkRowfor each element in thelandmarkDataarray.Next, you’ll simplify the
Listcode by addingIdentifiableconformance to theLandmarktype.Step 3
Switch to
Landmark.swiftand declare conformance to theIdentifiableprotocol.Since the
Landmarktype already has theidproperty required byIdentifiableprotocol, there’s no more work to do.Step 4
Switch back to
LandmarkList.swiftand remove theidparameter.From now on, you’ll be able to use collections of
Landmarkelements directly.
6.设置列表和详细信息之间的导航-Set Up Navigation Between List and Detail
列表显示正确,但你还不能点击一个单独的地标来查看该地标的详细页面。
您可以通过将列表嵌入到一个 "导航视图 "中来添加导航功能,然后将每一行嵌套在一个 "导航链接 "中,以设置一个到目标视图的转换。
The list renders properly, but you can’t tap an individual landmark to see that landmark’s detail page yet.
You add navigation capabilities to a list by embedding it in a
NavigationView, and then nesting each row in aNavigationLinkto set up a transtition to a destination view.
- 第一步
将动态生成的地标列表嵌入到NavigationView中。
- 第二步:将动态生成的地标列表嵌入到导航视图中。
调用navigationBarTitle(_:)修改器方法来设置显示列表时导航栏的标题。
- 第三步
在列表的闭合中,将返回的行包裹在一个NavigationLink中,指定LandmarkDetailview作为目标。
- 第四步
您可以通过切换到实时模式,直接在预览中试用导航。点击 "实时预览 "按钮,并点击一个地标来访问详细页面。
Step 1
Embed the dynamically generated list of landmarks in a
NavigationView.Step 2
Call the
navigationBarTitle(_:)modifier method to set the title of the navigation bar when displaying the list.Step 3
Inside the list’s closure, wrap the returned row in a
NavigationLink, specifying theLandmarkDetailview as the destination.Step 4
You can try out the navigation directly in the preview by switching to live mode. Click the Live Preview button and tap a landmark to visit the detail page.
7.将数据传递至子视图-Pass Data into Child Views
"LandmarkDetail "视图仍然使用硬编码的细节来显示其地标。就像 "LandmarkRow "一样,"LandmarkDetail "类型和它所包含的视图需要使用 "landmark "属性作为其数据的来源。
从子视图开始,你将转换CircleImage,MapView,然后LandmarkDetail来显示传入的数据,而不是硬编码每行。
The
LandmarkDetailview still uses hard-coded details to show its landmark. Just likeLandmarkRow, theLandmarkDetailtype and the views it comprises need to use alandmarkproperty as the source for their data.Starting with the child views, you’ll convert
CircleImage,MapView, and thenLandmarkDetailto display data that’s passed in, rather than hard-coding each row.
- 第一步
在CircleImage.swift中,为CircleImage添加一个存储图像属性。
这是使用SwiftUI构建视图时的常见模式。你的自定义视图通常会包裹和封装一系列特定视图的修改器。
- 第二步
更新预览提供者以传递龟石的图像。
- 第三步
在MapView.swift中,给MapView添加一个坐标属性,并将代码转换为使用该属性,而不是硬编码经纬度。
- 第四步
更新预览提供者以传递数据数组中第一个地标的坐标。
- 第五步
更新预览提供者,传递数据阵列中第一个地标的坐标。
在LandmarkDetail.swift中,为LandmarkDetail类型添加Landmark属性。
- 第六步
更新预览,使用landmarkData中的第一个地标。
- 第七步
将所需数据传递给您的自定义类型。
- 第八步
最后,调用navigationBarTitle(_:displayMode:)修改器,在显示详细视图时,给导航栏一个标题。
- 第九步
在SceneDelegate.swift中,将应用程序的根视图切换为LandmarkList。
当在模拟器中独立运行时,您的应用程序将以SceneDelegate中定义的根视图启动,而不是预览。
- 第十步:将应用程序的根视图切换为 "LandmarkList"。
在LandmarkList.swift中,将当前的地标传递给目标LandmarkDetail。
- 第十一步:将当前的地标传给目的地
LandmarkDetail。
切换到实时预览,当您从列表中导航时,可以看到详细视图显示正确的地标。
Step 1
In
CircleImage.swift, add a storedimageproperty toCircleImage.This is a common pattern when building views using SwiftUI. Your custom views will often wrap and encapsulate a series of modifiers for a particular view.
Step 2
Update the preview provider to pass the image of Turtle Rock.
Step 3
In
MapView.swift, add acoordinateproperty toMapViewand convert the code to use that property instead of hard-coding the latitude and longitude.Step 4
Update the preview provider to pass the coordinate of the first landmark in the data array.
Step 5
In
LandmarkDetail.swift, add aLandmarkproperty to theLandmarkDetailtype.Step 6
Update the preview to use the first landmark from
landmarkData.Step 7
Pass the required data down to your custom types.
Step 8
Finally, call the
navigationBarTitle(_:displayMode:)modifier to give the navigation bar a title when showing the detail view.Step 9
In
SceneDelegate.swift, switch the root view of the app to beLandmarkList.Your app starts with the root view defined in the
SceneDelegatewhen running standalone in the simulator instead of the preview.Step 10
In
LandmarkList.swift, pass the current landmark to the destinationLandmarkDetail.Step 11
Switch to the live preview to see the detail view show the correct landmarks when you navigate from the list.
8.动态生成预览- Previews Dynamically
接下来,您将为LandmarkList_Previewspreview 提供者添加代码,以在不同的设备尺寸下渲染列表视图的预览。默认情况下,预览以活动方案中的设备尺寸呈现。你可以通过调用previewDevice(_:)modifier方法来改变预览设备。
Next, you’ll add code to the
LandmarkList_Previewspreview provider to render previews of the list view at different device sizes. By default, previews render at the size of the device in the active scheme. You can change the preview device by calling thepreviewDevice(_:)modifier method.
- 步骤一
首先将当前的列表预览改为以iPhone SE的大小进行渲染。
您可以提供任何设备的名称,因为它出现在Xcode的方案菜单中。
- 第二步
在列表预览中,将 "LandmarkList "嵌入到 "ForEach "实例中,使用设备名称数组作为数据。
ForEach对集合的操作方式与列表相同,这意味着您可以在任何可以使用子视图的地方使用它,例如在堆栈、列表、组等中。当你的数据元素是简单的值类型时--就像你在这里使用的字符串--你可以使用.self作为标识符的键路径。
- 第三步
使用previewDisplayName(_:)修改器添加设备名称作为预览的标签。
- 第四步
您可以使用不同的设备进行实验,比较您的视图的渲染效果,所有这些都来自画布。
Step 1
Start by changing the current list preview to render at the size of an iPhone SE.
You can provide the name of any device as it appears in Xcode’s scheme menu.
Step 2
Within the list preview, embed the
LandmarkListin aForEachinstance, using an array of device names as the data.
ForEachoperates on collections the same way as the list, which means you can use it anywhere you can use a child view, such as in stacks, lists, groups, and more. When the elements of your data are simple value types — like the strings you’re using here — you can use\.selfas key path to the identifier.Step 3
Use the
previewDisplayName(_:)modifier to add the device names as labels for the previews.Step 4
You can experiment with different devices to compare the renderings of your views, all from the canvas.