Hacking with iOS: SwiftUI Edition - Moonshot 项目(二)

Hacking with iOS: SwiftUI Edition - Moonshot 项目(二)_第1张图片
moon

使用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.jsonastronauts.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])
    }
}

现在,我们可以使用另一个NavigationLinkMissionView中进行演示。这需要放在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 项目挑战

赏我一个赞吧~~~

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - Moonshot 项目(二))