使用ScrollView和GeometryReader显示任务详细信息
当用户从我们的主列表中选择一个阿波罗任务时,我们希望显示有关该任务的信息:其图像,任务徽章以及机组人员中的所有宇航员及其角色。前两个并不太难,但是第二个需要更多的工作,因为我们需要在两个JSON文件中将乘员ID与乘员详细信息进行匹配。
让我们从简单开始并逐步进行:创建一个名为MissionView.swift
的新SwiftUI视图。最初,它只具有mission
属性,以便我们可以显示任务徽章和说明,但不久之后我们将对其添加更多内容。
就布局而言,此东西需要具有一个滚动的VStack
,其中带有可调整大小的任务徽章图像,然后是文本视图,然后是分隔符,以便所有内容都可以推送到屏幕顶部。我们将使用GeometryReader
设置任务图像的最大宽度,经过反复试验,尽管经过反复试验,我发现任务徽章在不全宽时效果最好——宽度在50%到75%之间的位置看起来更好,避免它在屏幕上显得很大。
现在将此代码放入MissionView.swift
中:
struct MissionView: View {
let mission: Mission
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
Image(self.mission.image)
.resizable()
.scaledToFit()
.frame(maxWidth: geometry.size.width * 0.7)
.padding(.top)
Text(self.mission.description)
.padding()
Spacer(minLength: 25)
}
}
}
.navigationBarTitle(Text(mission.displayName), displayMode: .inline)
}
}
您是否注意到间隔是使用minLength:25
创建的?这不是我们以前使用过的东西,但是它可以确保间隔的最小高度至少为25点。这在滚动视图内部很有用,因为总的可用高度是灵活的:间隔符通常会占用所有可用的剩余空间,但是在滚动视图内部没有意义。
我们可以使用Spacer().frame(minHeight:25)
来达到相同的结果,但是使用Spacer(minLength:25)
的好处是,如果您改变了堆栈方向——如果从VStack
转到HStack
,那么它实际上变成了Spacer().frame(minWidth:25)
。
无论如何,有了我们的新视图后,代码将不再生成,这完全是因为它下面的预览结构——该东西需要传入一个Mission
对象,以便它可以渲染。幸运的是,我们的Bundle
扩展程序在这里也可用:
struct MissionView_Previews: PreviewProvider {
static let missions: [Mission] = Bundle.main.decode("missions.json")
static var previews: some View {
MissionView(mission: missions.randomElement()!)
}
}
如果您在预览中查看,将会发现这是一个不错的开始,但是下一部分比较棘手:我们要在说明下方显示参与任务的宇航员列表。接下来解决这个问题...
使用 first(where:) 合并Codable结构体
在我们的任务描述下方,我们想显示每个机组人员的图片,姓名和角色,这说起来容易做起来难。
这里的复杂性在于,我们的JSON分两部分提供:missions.json
和astronauts.json
。这消除了我们的数据重复,因为一些宇航员参加了多次任务,但这也意味着我们需要编写一些代码以将我们的数据连接在一起——例如,将“armstrong”解析为“Neil A. Armstrong””。您会看到,一方面,我们执行的任务知道机组人员“armstrong”扮演的角色是“指挥官(Commander)”,但不知道谁是“ armstrong”,而另一方面,我们的任务是“Neil A. Armstrong”及其描述他,但不知道他是阿波罗11号的指挥官。
因此,我们需要做的是使我们的MissionView
接受获得的任务以及完整的宇航员阵容,然后确定哪些宇航员实际上参与了发射。由于此合并数据只是暂时的,因此我们可以使用元组而不是结构体,但是老实说并没有太大的区别,因此我们将在这里使用新的结构体。
立即在MissionView
中添加此嵌套结构体:
struct CrewMember {
let role: String
let astronaut: Astronaut
}
现在到了棘手的部分:我们需要向MissionView
添加一个属性,该属性存储一组CrewMember
对象——这些是完全解析的角色/宇航员配对。首先,这就像添加另一个属性一样简单:
let astronauts: [CrewMember]
但是,我们如何设置该属性?好吧,想一想:如果我们将这一视图传递给它的任务以及所有宇航员,我们就可以对任务组进行遍历,然后让每个乘员对我们所有的宇航员进行遍历,以找到具有匹配ID的宇航员。当我们找到一个对象时,可以将其及其角色转换为CrewMember
对象,但如果不这样做,则意味着我们以某种方式使用了无效或未知名称的组员角色。
Swift为我们提供了一个名为first(where :)
的数组方法,该方法确实可以帮助完成此过程。我们可以给它一个谓词(条件的花哨词),它将发回与该谓词匹配的第一个数组元素,如果没有则返回nil
。在我们的案例中,我们可以这样说:“给我第一名ID为 armstrong 的宇航员。”
这是其对应的代码:
init(mission: Mission, astronauts: [Astronaut]) {
self.mission = mission
var matches = [CrewMember]()
for member in mission.crew {
if let match = astronauts.first(where: { $0.id == member.name }) {
matches.append(CrewMember(role: member.role, astronaut: match))
} else {
fatalError("Missing \(member)")
}
}
self.astronauts = matches
}
输入该代码后,我们的预览结构将再次停止工作,因为它需要更多信息。因此,在该处添加第二个调用,以decode()
加载所有宇航员,然后再将它们传递进来:
struct MissionView_Previews: PreviewProvider {
static let missions: [Mission] = Bundle.main.decode("missions.json")
static let astronauts: [Astronaut] = Bundle.main.decode("astronauts.json")
static var previews: some View {
MissionView(mission: missions[0], astronauts: astronauts)
}
}
现在我们有了所有宇航员数据,我们可以使用ForEach
在任务说明的正下方显示此数据。这将使用与ContentView
中使用的相同的HStack
/ VStack
组合,除了现在我们需要在HStack
的末尾使用一个空格将视图向左推——以前我们是免费获得的,因为我们在List
中,但是现在不是这样。我们还将使用胶囊的形状和覆盖层,为宇航员的图片添加一些额外的样式,以使其看起来更好。
在MissionView
中的Spacer(minLength:25)
之前添加以下代码:
ForEach(self.astronauts, id: \.role) { crewMember in
HStack {
Image(crewMember.astronaut.id)
.resizable()
.frame(width: 83, height: 60)
.clipShape(Capsule())
.overlay(Capsule().stroke(Color.primary, lineWidth: 1))
VStack(alignment: .leading) {
Text(crewMember.astronaut.name)
.font(.headline)
Text(crewMember.role)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal)
}
您应该在预览中看到看起来不错,但是要在模拟器中看到它,我们需要在ContentView
中修改NavigationLink
:现在将其Text(“ Detail View”)
,替换为:
NavigationLink(destination: MissionView(mission: mission, astronauts: self.astronauts)) {
现在继续在模拟器中运行该应用——它开始变得有用!
在继续之前,请尝试花费几分钟来定制宇航员的显示方式——我使用了胶囊夹的形状和覆盖层,但是您可以尝试使用圆形或圆角矩形,可以使用不同的字体或更大的图像,甚至可以添加标记任务指挥官是谁的某种方式。
解决 buttonStyle() 和 layoutPriority() 的问题
要完成此程序,我们将制作第三个也是最后一个视图以显示宇航员的详细信息,这可以通过在任务视图中点击一位宇航员来实现。这大多数情况下只是您的实践,但我确实想强调一个有趣的地方,以及如何使用名为 layoutPriority()
的新修饰符解决它。
首先创建一个名为AstronautView
的新SwiftUI视图。这将具有一个宇航员属性,因此它知道要显示的内容,然后使用与MissionView
中类似的GeometryReader
/ ScrollView
/ VStack
组合进行布局。输入以下代码:
struct AstronautView: View {
let astronaut: Astronaut
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
Image(self.astronaut.id)
.resizable()
.scaledToFit()
.frame(width: geometry.size.width)
Text(self.astronaut.description)
.padding()
}
}
}
.navigationBarTitle(Text(astronaut.name), displayMode: .inline)
}
}
我们再次需要更新预览,以便它使用一些数据创建其视图:
struct AstronautView_Previews: PreviewProvider {
static let astronauts: [Astronaut] = Bundle.main.decode("astronauts.json")
static var previews: some View {
AstronautView(astronaut: astronauts[0])
}
}
现在,我们可以使用另一个NavigationLink
从MissionView
中进行演示。这需要放在ForEach
内部,以便包装现有的HStack
:
NavigationLink(destination: AstronautView(astronaut: crewMember.astronaut)) {
HStack {
// current code
}
.padding(.horizontal)
}
立即运行该应用程序,并进行全面尝试——您应该至少看到一个错误,或者取决于SwiftUI,可能会看到两个错误。
第一个错误非常明显:在任务视图中,我们所有的宇航员图片均显示为纯蓝色胶囊,而不是其图片。您可能还会注意到,每个人的名字都用相同的蓝色阴影书写,这可能为您提供了线索–现在,这是一个导航链接,SwiftUI通过将视图涂成蓝色来使整个外观看起来很活跃。
要解决此问题,我们需要让SwiftUI将导航链接的内容呈现为一个普通按钮,这意味着它不会对图像或文本应用颜色。因此,将其作为修改器添加到MissionView
中的宇航员NavigationLink
中:
.buttonStyle(PlainButtonStyle())
至于第二个错误,您甚至可能根本没有看到它-在我看来这是SwiftUI本身的一个错误,因此它可能会在将来的版本中修复,或者有可能仅影响特定设备配置。因此,如果在使用与我相同的iPhone模拟器时不存在此错误,则可能已解决!
(当前版本已经修复)Bug是这样的:如果您选择某些宇航员,例如阿波罗1号的爱德华·怀特二世,您可能会看到其说明文字被裁剪到底部。因此,您只看到了一些文本,而不是看到所有文本,后面是省略号。而且,如果您仔细观察图像的顶部,您会发现它不再直接靠在顶部的导航栏上。
我们看到的是SwiftUI的布局算法,很难得出关于我们内容的正确结论。在我看来,这是一个SwiftUI错误,有可能到您自己尝试时它甚至不存在。但是它就在这里,因此,我将向您展示如何使用layoutPriority()
修饰符对其进行修复。
为此,只需在AstronautView
中的描述文本视图中添加layoutPriority(1)
,如下所示:
Text(self.astronaut.description)
.padding()
.layoutPriority(1)
解决了这两个错误之后,我们的程序就完成了——上一次运行它,然后尝试一下!
译自
Showing mission details with ScrollView and GeometryReader
Merging Codable structs using first(where:)
Fixing problems with buttonStyle() and layoutPriority()
Moonshot 项目(一) | Hacking with iOS: SwiftUI Edition | Moonshot 项目挑战 |
---|
赏我一个赞吧~~~