[SwiftUI 100天] Day-5 猜国旗 · part2

644 阅读5分钟
更多内容欢迎关注公众号:Swift花园

堆叠按钮

我们即将为我们的app构建基本的 UI 结构,它们包括两个指示用户操作的标签以及三个显示国旗的图片按钮。

首先,打开 Xcode 的 Assets.xcassets,然后把国旗图片拖进去,确保你找到的国旗图片包含 @2x 或者 @3x 的版本。它们是用于处理不同类型的 iPhone 屏幕的两倍分辨率和三倍分辨率的图像。

接下来,我们添加两个属性来存储游戏的数据:一个是存放所有国家图片名字的数组,另一个是代表正确国家的数组索引。

var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"]
var correctAnswer = Int.random(in: 0...2)

Int.random(in:) 方法随机选择一个整数,在这里刚好适用 – 我们用这个数来决定应当点击哪个国家的国旗。

在 body 方法里,我们需要用一个 VStack 来布局游戏提示:

var body: some View {
    VStack {
        Text("Tap the flag of")
        Text(countries[correctAnswer])
    }
}

在提示下方,我们会放置可点击的按钮。你可以直接放进同一个VStack,当然也可以创建一个新的 VStack,以便单独控制间距。

前一个存放两个文本视图的VStack 没有间距,但它同国旗的 VStack 之间设置一个30的距离会让视觉效果更好。

因此,在前面的 VStack 默认添加一个ForEach

ForEach(0 ..< 3) { number in
    Button(action: {
       // flag was tapped
    }) {
        Image(self.countries[number])
            .renderingMode(.original)
    }
}

renderingMode(.original) 修改器告诉 SwiftUI 渲染原始图像的像素而不是作为一个按钮重新着色。

现在我们会遇到一个问题:我们的 body 属性正试图传回两个 view,一个 VStack 和 一个ForEach,这是不允许的。 所以我们需要第二个VStack:我们会用一个新的VStackForEach 和原来的 VStack包裹起来,并且设置30的间距。

把代码修改成这样:

var body: some View {
    VStack(spacing: 30) {
        VStack {
            Text("Tap the flag of")
            // etc
        }

        ForEach(0 ..< 3) { number in
            // etc
        }
    }
}

两个 VStack 允许我们更精确地掌控位置:外层的 stack 会用 30 的间距来分隔它的子视图,而内层的 stack 则没有间距。

UI 目前看起来还行,但你会发现一些问题。由于某些国旗的图案里有白色,它们会混入白色的背景,并且所有的国旗都被居中到屏幕中间。

为了解决国旗颜色的问题,我们引入一个 ZStack

var body: some View {
    ZStack {
        VStack(spacing: 30) {
            VStack {
                Text("Tap the flag of")
                // etc
            }

            ForEach(0 ..< 3) { number in
                // etc
            }
        }
    }
}

我们还需要在 VStack 的下面再设置一个背景:

Color.blue.edgesIgnoringSafeArea(.all)

edgesIgnoringSafeArea() 修改器确保颜色能够覆盖整个屏幕。

现在我们有了一个暗色的背景,我们需要调整文本的颜色以便它们能从背景中凸显出来:

Text("Tap the flag of")
    .foregroundColor(.white)

Text(countries[correctAnswer])
    .foregroundColor(.white)

最后,我们还要美化一下,以便整个 VStack 居于屏幕上部,只需要在 ForEach 之后加一个 Spacer 视图:

Spacer()

用 Alert 显示玩家分数

为了让游戏有趣,我们需要随机安排国旗展示的顺序,在玩家点击国旗时触发 alert 告诉玩家选择是正确还是错误,然后重新洗国旗的顺序。

我们已经设置 correctAnswer 为一个随机数,但是所有的国旗每次都是一样的顺序。为了解决这个问题,我们需要在游戏开始时重新洗 countries 数组,把属性改成这样:

var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()

如你可见,shuffled() 方法自动帮我们解决了随机性的问题。

现在来到更有趣的部分:当国旗出现时,我们应该怎么做? 用实际的代码来替换 // flag was tapped 这段注释。

最好的方式是运行一个接收整数的方法,检查这个整数是否匹配correctAnswer 属性。

不管回答是否正确,我们想要展示给玩家一个 alert,告诉它们发生了什么,以便玩家可以跟踪游戏进程。因此,我们可以加一个属性来存储 alert 是否显示:

@State private var showingScore = false

并且再添加一个用作 alert 标题的属性:

@State private var scoreTitle = ""

然后是我们的点击响应方法:

func flagTapped(_ number: Int) {
    if number == correctAnswer {
        scoreTitle = "Correct"
    } else {
        scoreTitle = "Wrong"
    }

    showingScore = true
}

// flag was tapped 注释换成下面这句:

self.flagTapped(number)

我们已经有 number 参数,它是来自ForEach,所以只需要传进flagTapped()就行。

因为我们展示了 alert,我们需要考虑 alert 消失之后做些什么。

显然游戏不应该结束,我们应当写一个askQuestion()方法继续发问,通过重新洗牌国旗然后选择一个新的答案:

func askQuestion() {
    countries.shuffle()
    correctAnswer = Int.random(in: 0...2)
}

上面的代码无法编译通过,希望你已经发现问题所在:我们正试图修改视图里没有标记@State的属性,这是不允许的。因此,回到 countriescorrectAnswer 的声明, 加上 @State private,就像这样:

@State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
@State private var correctAnswer = Int.random(in: 0...2)

现在我们已经可以展示 alert 了,需要做三件事:

  1. alert() 修改器,在 showingScore 为 true 时展示 alert。
  2. scoreTitle 设置 alert 的标题。
  3. Dismiss 按钮点击时调用askQuestion()

把最后这段代码放在 ZStack上:

.alert(isPresented: $showingScore) {
    Alert(title: Text(scoreTitle), message: Text("Your score is ???"), dismissButton: .default(Text("Continue")) {
        self.askQuestion()
    })
}

是的,分数的计算还没有搞定,我们将在下一部分解决。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~