9. 完成详情视图
在本节中,您将编写一个第二GraphQL 查询,该查询请求有关单个 发射的详细信息,并使用这些数据在一个 DetailView
为了在详情页上显示更多信息,您有几个选择
- 您可以在
LaunchList
查询中请求您想要为每个 launch显示的所有详细信息,然后将检索到的对象传递到DetailViewController
。 - 您可以为 不同的 查询提供 launch 的标识符,然后请求您想要显示的所有详细信息。
如果请求的列表与详情页之间的大小没有实质性差异,第一个选项可能看起来更容易。
但是,记得 GraphQL 的一大优点是您可以为显示在页面上的数据 query 精确。 如此,如果您不打算显示额外的信息,则可以通过在需要数据之前不请求数据来节省带宽、执行时间和电池寿命。
这尤其适用于当您的详情视图的查询比列表视图大得多时。传递标识符并根据该标识符获取数据被视为最佳实践。尽管在这种情况下数据量没有太大差异,但您将建立一个查询来帮助根据 ID 获取详细信息,这样您就会知道如何在未来这样做。
创建详情查询
创建一个新空文件并将其命名为 LaunchDetails.graphql
。在文件中,您将添加要显示在详细视图中的详细信息。首先,您希望返回到 您的沙盒并确保您的 查询正常工作!
在“探索者”标签中,首先在中部 操作区域点击“新建标签页”按钮:
将添加一个新标签页,而其中没有任何内容
在左侧列中,点击“文档”下方的“查询”一词,以便显示可能的查询列表:
通过单击旁边的按钮选择 launch
查询。沙盒探索器将自动为您设置查询:
首先,将 操作名称从“查询”更改为“运行详情”,这将反映在标签页名称中,并有助于区分您正在处理的查询:
让我们看看这里添加了什么
- 同样,我们添加了一个 操作,但这次它有一个传入的参数。这是沙盒探索器自动添加的,因为非空
launchId
参数没有默认值。 - 参数名称前面加有
$
,类型紧随其后。注意,这里的ID
类型有一个感叹号,表示它不能为空。 - 在这个 操作 中,我们调用
launch
查询。id
是查询期望的 参数,而$launchId
是我们上面传入的参数的名称。 - 再次,这里留有空白供您添加要获取的详细信息的 字段,这里是一个
Launch
。 - 最后,在底部,“变量”部分已展开,并添加了一个键为
"launchId"
的字典。在运行时,这将用于填充$launchId
参数中的空白。
注意:GraphQL对null的可空性的假设与Swift不同。在Swift中,如果你没有用问号或感叹号标注属性的类型,那么该属性是非可空的。
在GraphQL中,如果你没有用感叹号标注字段的类型,那么该字段被认为是可空的。这是因为GraphQL的字段默认是可空的。
在编辑Swift和GraphQL文件之间切换时,请记住这个区别。
现在在沙盒资源管理器中,首先使用复选框或输入来添加你在LaunchList
查询中已请求的属性。有一点不同:由于补丁将显示在更大的ImageView
中,所以使用LARGE
来设置任务补丁的大小:
query LaunchDetails($id:ID!) {launch(id: $id) {idsitemission {namemissionPatch(size:LARGE)}}}
接下来,在左侧侧边栏中查看其他哪些字段可用。选中rocket
将为请求有关火箭的详细信息添加一组括号,并钻入rocket
属性,显示Rocket类型上的可用字段:
点击按钮来勾选name
和type
。然后,点击左侧侧边栏中Rocket
类型旁边的后退按钮返回到Launch
:
最后,在Launch
上勾选isBooked
属性。您的最终查询应如下所示:
query LaunchDetails($launchId: ID!) {launch(id: $launchId) {idsitemission {namemissionPatch(size: LARGE)}rocket {nametype}isBooked}}
在操作部分底部,更新变量部分以传入launch的ID。在这种情况下,它需要是一个包含数字的字符串:
{ "id": "25" }
这告诉SandBox Explorer以值$launchId
填充该变量,然后运行查询。按下大播放按钮,你应该会为ID号为25的launch得到一些结果:
现在你已经确认它工作正常,请复制查询(可以通过选择所有文本或像之前一样从昆虫菜单中选择“操作”选项),并将其粘贴到你的LaunchDetails.graphql
文件中。从终端运行代码生成以生成新查询的代码。
执行查询
现在让我们添加代码来运行此查询以检索数据。
打开DetailViewModel.swift
并添加这个import
import RocketReserverAPI
接下来,让我们更新init()
方法并添加一些variables
来存储我们的Launch数据:
let launchID: RocketReserverAPI.ID@Published var launch: LaunchDetailsQuery.Data.Launch?@Published var isShowingLogin = false@Published var appAlert: AppAlert?init(launchID: RocketReserverAPI.ID) {self.launchID = launchID}
接下来需要运行查询,因此在 loadLaunchDetails
方法中将 TODO
替换为此代码:
func loadLaunchDetails() {guard launchID != launch?.id else {return}Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID)) { [weak self] result inguard let self = self else {return}switch result {case .success(let graphQLResult):if let launch = graphQLResult.data?.launch {self.launch = launch}if let errors = graphQLResult.errors {self.appAlert = .errors(errors: errors)}case .failure(let error):self.appAlert = .errors(errors: [error])}}}
现在我们已经启动了查询,需要更新 UI 代码来使用新的数据。
更新 UI 代码
首先,转到 DetailView.swift
,并添加以下 import
声明:
import RocketReserverAPIimport SDWebImageSwiftUI
接下来,需要更新 init()
方法,以使用 launchID
来初始化 DetailViewModel
:
init(launchID: RocketReserverAPI.ID) {_viewModel = StateObject(wrappedValue: DetailViewModel(launchID: launchID))}
几乎完成了!让我们更新 body
视图变量,使用从 DetailViewModel
得到的 launch
数据,并调用 loadLaunchDetails
方法:
var body: some View {VStack {if let launch = viewModel.launch {HStack(spacing: 10) {if let missionPatch = launch.mission?.missionPatch {WebImage(url: URL(string: missionPatch)).resizable().placeholder(placeholderImg).indicator(.activity).scaledToFit().frame(width: 165, height: 165)} else {placeholderImg.resizable().scaledToFit().frame(width: 165, height: 165)}VStack(alignment: .leading, spacing: 4) {if let missionName = launch.mission?.name {Text(missionName).font(.system(size: 24, weight: .bold))}if let rocketName = launch.rocket?.name {Text("🚀 \(rocketName)").font(.system(size: 18))}if let launchSite = launch.site {Text(launchSite).font(.system(size: 14))}}Spacer()}if launch.isBooked {cancelTripButton()} else {bookTripButton()}}Spacer()}.padding(10).navigationTitle(viewModel.launch?.mission?.name ?? "").navigationBarTitleDisplayMode(.inline).task {viewModel.loadLaunchDetails()}.sheet(isPresented: $viewModel.isShowingLogin) {LoginView(isPresented: $viewModel.isShowingLogin)}.appAlert($viewModel.appAlert)}
另外,还需要更新 DetailView
的预览代码,如下所示:
struct DetailView_Previews: PreviewProvider {static var previews: some View {DetailView(launchID: "110")}}
现在,我们需要将 DetailView
连接到我们的 LaunchListView
。让我们转到 LaunchListView.swift
并更新我们的 List
,如下所示:
ForEach(0..<viewModel.launches.count, id: \.self) { index inNavigationLink(destination: DetailView(launchId: viewModel.launches[index].id)) {LaunchRow(launch: viewModel.launches[index])}}
这将允许我们点击列表中的任何 LaunchRow
,并加载对应的 DetailView
。
测试 DetailView
现在,一切都已连接,构建并运行应用程序,当你点击任何 launch 时,你应该看到一个相应的 DetailView
,如下所示:
你可能已经注意到详细信息视图包括一个 Book Now!
按钮,但Currently there is no way to book a seat. To fix this, let's 学习如何通过 mutations 在你的图中对对象进行更改,包括身份验证。