首页 iOS.& Swift Books uikit学徒

4
网点 由Matthijs Hollemans撰写& Fahim Farook

您已构建了用户界面 牛眼 而且你知道如何找到滑块的当前位置。已经敲了一些待办事项列表的一些项目。本章从待办事项列表中处理其他一些项目,并涵盖以下项目:

  • 改进滑块: 将初始滑块值(在代码中)设置为故事板中设置的任何值,而不是假设初始值。
  • 生成随机数: 通过游戏生成随机数用作目标。
  • 在游戏中添加圆形: 添加启动新一轮游戏的能力。
  • 显示目标值: 在屏幕上显示生成的目标号码。

改善滑块

您完成将滑块的值存储到变量中并通过警报显示它。这很棒,但你仍然可以改善它一点。

What if you decide to set the initial value of the slider in the storyboard to something other than 50 — say, 1 or 100? Then currentValue would be wrong again because the app always assumes it will be 50 at the start. You’d have to remember to also fix the code to give currentValue a new initial value.

把它带到我 - 那种东西很难记住,特别是当项目变得更大时,你有几十个视图控制器担心,或者当你没有看几周的代码时。

获取初始滑块值

To fix this issue once and for all, you’re going to do some work inside the viewDidLoad() method in ViewController.swift.。该方法目前如下所示:

override func viewDidLoad() {
  super.viewDidLoad()
  // Do any additional setup after loading the view.
}

When you created this project based on Xcode’s template, Xcode inserted the viewDidLoad() method into the source code. You will now add some code to it.

The viewDidLoad() message is sent by UIKit immediately after the view controller loads its user interface from the storyboard file. At this point, the view controller isn’t visible yet, so this is a good place to set instance variables to their proper initial values.

➤ Change viewDidLoad() to the following:

override func viewDidLoad() {
  super.viewDidLoad()
  currentValue = lroundf(slider.value)
}

The idea is that you take whatever value is set on the slider in the storyboard (whether it is 50, 1, 100 or anything else) and use that as the initial value of currentValue.

Recall that you need to round off the number, because currentValue is an Int and integers cannot take decimal (or fractional) numbers.

不幸的是,Xcode甚至在尝试运行应用程序之前立即抱怨这些更改。

关于缺少标识符的Xcode错误消息
关于缺少标识符的Xcode错误消息

笔记:Xcode试图有用,并在您输入时分析了错误的错误。有时,当您完成正在制作的更改时,您可能会看到临时警告和错误消息将消失。

这些消息不要太吓倒;它们只是短暂的,而代码处于助焊状态。

This error is because viewDidLoad() does not know of anything named 滑块.

Then why did this work earlier, in 滑块Moved()? Let’s take a look at that method again:

@IBAction func sliderMoved(_ slider: UISlider) {
  currentValue = lroundf(slider.value)
}

Here, you do the exact same thing: You round off 滑块.value and put it into currentValue. So why does it work here but not in viewDidLoad()?

The difference is that, in the code above, 滑块 is a 范围 of the 滑块Moved() method. Parameters are the things inside the parentheses following a method’s name. In this case, there’s a single parameter named 滑块, which refers to the UISlider object that sent this action message.

Action methods can have a parameter that refers to the UI control that triggered the method. This is convenient when you wish to refer to that object in the method, just as you did here (the object in question being the UISlider).

When the user moves the slider, the UISlider object basically says, “Hey view controller! I’m a slider object and I just got moved. By the way, here’s my phone number so you can get in touch with me.”

The 滑块 范围 contains this “phone number,” but it is only valid for the duration of this particular method.

In other words, 滑块 is 当地的;你不能在别的地方使用它。

当地人

当我第一次提到变量时,我说每个变量都有一定的生命周期,称为它 范围。变量的范围取决于您定义该变量的程序中的位置。

SWIFT有三个可能的范围水平:

  1. 全球范围:这些对象存在于应用程序的持续时间内,可从任何位置访问。
  2. 实例范围: This is for variables such as currentValue. These objects are alive for as long as the object that owns them stays alive.
  3. 本地范围: Objects with a local scope, such as the 滑块 范围 of 滑块Moved(), only exist for the duration of that method. As soon as the execution of the program leaves this method, the local objects are no longer accessible.

Let’s look at the top part of showAlert():

@IBAction func showAlert() {
  let message = "The value of the slider is: \(currentValue)"

  let alert = UIAlertController(
    title: "Hello, World",
    message: message, 
    preferredStyle: .alert)

  let action = UIAlertAction(
    title: "OK", 
    style: .default,
    handler: nil)
  . . .

Because the message, alert, and action objects are created inside the method, they have local scope. They only come into existence when the showAlert() action is performed and they cease to exist when the action is done.

As soon as the showAlert() method completes, i.e., when there are no more statements for it to execute, the computer destroys the message, alert, and action objects and their storage space is cleared out.

The currentValue variable, however, lives on forever… or at least for as long as the ViewController does, which is until the user terminates the app. This type of variable is named an 实例变量,因为它的范围与它所属的对象实例的范围相同。

换句话说,如果要在一个操作事件到下一个操作事件,则使用实例变量使用实例变量。

设置出口

所以,通过这种新获得的变量和他们的范围知识,你如何解决遇到的错误?

The solution is to store a reference to the slider as a new instance variable, just like you did for currentValue. Except that this time, the data type of the variable is not Int, but UISlider. And you’re not using a regular instance variable but a special one called an 出口.

➤添加以下行 ViewController.swift.:

@IBOutlet var slider: UISlider!

It doesn’t really matter where this line goes, just as long as it is somewhere inside the curly brackets for class ViewController. I usually put outlets with the other instance variables — at the top of the class implementation.

This line tells Interface Builder that you now have a variable named 滑块 that can be connected to a UISlider object. Just as Interface Builder likes to call methods 行动,它调用这些变量 网点. Interface Builder doesn’t see any of your other variables, only the ones marked with @IBOutlet.

Don’t worry about the exclamation point for now. Why that is necessary will be explained later on. For now, just remember that a variable for an outlet needs to be declared as @IBOutlet var and has an exclamation point at the end. (Sometimes you’ll see a question mark instead; all this hocus pocus will be explained in due time.)

Once you add the 滑块 variable, you’ll notice that the Xcode error goes away. Does that mean that you can run your app now? Try it and see what happens.

应用程序崩溃,以类似于以下内容的错误崩溃:

插座未连接时应用程序崩溃
插座未连接时应用程序崩溃

所以发生了什么事?

请记住,出口必须是 连接的 to something in the storyboard. You defined the variable, but you didn’t actually set up the connection yet. So, when the app ran and viewDidLoad() was called, it tried to find the matching connection in the storyboard and could not — and crashed.

让我们现在在故事板上设置连接。

➤打开故事板。抓住 控制 然后点击 滑块。但是,不要拖任何地方 - 应该弹出一个菜单,它显示此滑块的所有连接。 (而不是控制单击,您也可以右键单击一次。)

此弹出菜单与Connections Inspector完全相同。我只是想告诉你这种替代方法。

➤单击旁边的打开圆圈 新的参考插座 并拖累 查看控制器:

将滑块连接到出口
将滑块连接到出口

➤在出现的弹出窗口中,选择 滑块.

This is the outlet that you just added. You have successfully connected the slider object from the storyboard to the view controller’s 滑块 outlet.

Now that you have done all this set up work, you can refer to the slider object from anywhere inside the view controller using the 滑块 variable.

With these changes in place, it no longer matters what you choose for the initial value of the slider in Interface Builder. When the app starts, currentValue will always correspond to that setting.

➤运行应用程序并立即按下击中我!按钮。它正确地说:“滑块的价值是:50。”停止应用程序,进入接口构建器并将滑块的初始值更改为其他内容 - 例如,25.再次运行应用程序,然后按按钮。警报现在应该读取25。

笔记:更改滑块值 - 或者在任何接口构建器字段中的值 - 更改时记得在字段中退出。如果您进行了更改,但您的光标仍仍然存在,则更改可能不会生效。这是可以经常绊倒的东西。

当您完成播放时,将滑块的起始位置恢复到50。

锻炼: Give currentValue an initial value of 0 again. Its initial value is no longer important — it will be overwritten in viewDidLoad() anyway — but Swift demands that all variables always have some value and 0 is as good as any.

评论

You’ve probably noticed the grey text that begins with // a few times now. As I explained earlier briefly, these are comments. You can write any text you want after the // symbol as the compiler will ignore such lines from the // to the end of the line completely.

// I am a comment! You can type anything here.

Anything between the /* and */ markers is considered a comment as well. The difference between // and /* */ is that the former only works on a single line, while the latter can span multiple lines.

/*
   I am a comment as well!
   I can span multiple lines.
 */

The /* */ comments are often used to temporarily disable whole sections of source code, usually when you’re trying to hunt down a pesky bug, a practice known as “commenting out”. You can use the cmd- / 键盘快捷键评论/取消注释当前所选的行,或者如果您没有选择,则当前行。

评论行的最佳用途是解释您的代码如何工作。写得良好的源代码是不言自明的,但有时额外的澄清是有用的。向谁解释?对自己,主要是。

除非你有一个大象的记忆,否则你可能会忘记你的代码在六个月后看待你的代码如何工作。使用注释来慢慢慢跑。

生成随机数

在游戏播放之前,您仍然有相当的方法。因此,让我们继续列出下一个项目:生成随机数并在屏幕上显示它。

当您制作游戏时,随机数字提出了很多因素,因为游戏经常需要有一些不可预测性的元素。您无法真正获得计算机生成真正随机和不可预测的数字,但您可以使用 伪随机发电机 吐出至少似乎随机的数字。

With previous versions of Swift, you had to use external methods such as arc4random_uniform(), but as of Swift 4.2, Swift numeric types such as Int have the built-in ability to generate random numbers. How handy, right?

在生成随机值之前,您需要一个存储它的地方。

➤在顶部添加新变量 ViewController.swift.,其他变量:

var targetValue = 0

You might wonder why we didn’t specify the type of the targetValue variable, similar to what we’d done earlier for currentValue. This is because Swift is able to 推断 the type of variables if it has enough information to work with. Here, for example, you initialize targetValue with 0 and, since 0 is an integer value, the compiler knows that targetValue will be of type Int.

We’ll discuss Swift type inference again later on but, for the time being, the important point is that targetValue is initialized to 0. That 0 is never used in the game; it will always be overwritten by the random value that you’ll generate at the start of the game.

I hope the reason is clear why you made targetValue an instance variable: You want to calculate the random number in one place – like in viewDidLoad() — and then remember it until the user taps the button in showAlert() when you have to check this value against the user selection.

接下来,您需要生成随机数。这样做的好地方就是游戏开始时。

➤添加以下行 viewDidLoad() in ViewController.swift.:

targetValue = Int.random(in: 1...100)

The complete viewDidLoad() should now look like this:

override func viewDidLoad() {
  super.viewDidLoad()
  currentValue = lroundf(slider.value)
  targetValue = Int.random(in: 1...100)
}

What are you doing here? You call the random() function built into Int to get an arbitrary integer (whole number) between 1 and 100. The 1...100 part is known as a 关闭范围 wherein you specify a starting value and an ending value to specify a range of numbers. The ... part indicates that you want the range to include the closing value (100), but if you wanted a range without the final value, then you would specify the range as 1..<100 and would get only values from 1 to 99.

全清?向前!

显示随机数

➤ Change showAlert() to the following:

@IBAction func showAlert() {
  let message = "The value of the slider is: \(currentValue)" +
                "\nThe target value is: \(targetValue)"

  let alert = . . .
}

小费: Whenever you see . . . in a source code listing, I mean that as shorthand for: “This part didn’t change” — don’t go replacing the existing code with actual ellipsis! :]

You’ve simply added the random number, which is now stored in targetValue, to the message string. This should look familiar to you by now: The \(targetValue) placeholder is replaced by the actual random number.

The \n character sequence is new. It means that you want to insert a special “new line” character at that point, which will break up the text into two lines so the message is a little easier to read. The + is also new but is simply used here to combine two strings. We could just as easily have written it as a single long string, but it might not have looked as good to the reader. :]

➤运行应用程序并尝试一下!

警报显示了新行上的目标值
警报显示了新行上的目标值

笔记: Earlier, you used the + operator to add two numbers together (just like how it works in math) but, here, you’re also using + to glue different bits of text into one big string.

Swift allows the use of the same operator for different tasks, depending on the data types involved. If you have two integers, + adds them up. But with two strings, + concatenates, or combines, them into a longer string.

编程语言通常根据上下文使用不同目的的相同符号。毕竟,只有这么多的符号来绕过!

在游戏中添加圆形

如果你按下击中我!按钮几次,您会注意到随机数永远不会改变。我担心这场比赛不会那样有趣。

This happens because you generate the random number in viewDidLoad() and never again afterwards. The viewDidLoad() method is only called once when the view controller is created during app startup. The item on the to-do list actually said: “Generate a random number 在每一轮的开始时“。让我们谈谈这场比赛的圆形手段。

当游戏开始时,播放器的分数为0,圆形数量为1.您将滑块中途设置为1,并计算随机数。然后你等待玩家按下击中我!按钮。一旦这样做,圆角。

您可以计算此回合的点并将其添加到总分。然后你递增圆数字并开始下一轮。您再次将滑块重置为中途位置并计算新的随机数。冲洗,重复。

开始一个新的回合

每当你发现自己沿着那条线的想法,“在应用程序中的这一点我们必须这样做,那么为它创建一个新方法是有意义的。这种方法将在自己包含的单位单位中捕获该功能。

➤请记住,添加以下新方法 ViewController.swift.:

func startNewRound() {
  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

It doesn’t really matter where you put the code, as long as it is inside the ViewController implementation (within the class curly brackets), so that the compiler knows it belongs to the ViewController object.

It’s not very different from what you did before, except that you moved the logic for setting up a new round into its own method, startNewRound(). The advantage of doing this is that you can execute this logic from more than one place in your code.

使用新方法

First, you’ll call this new method from viewDidLoad() to set up everything for the very first round. Recall that viewDidLoad() happens just once when the app starts up, so this is a great place to begin the first round.

➤ Change viewDidLoad() to:

override func viewDidLoad() {
  super.viewDidLoad()
  startNewRound()  // Replace previous code with this
}

笔记 that you’ve removed some of the existing statements from viewDidLoad() and replaced them with just the call to startNewRound().

You will also call startNewRound() after the player pressed the Hit Me! button, from within showAlert().

➤ Make the following change to showAlert():

@IBAction func showAlert() {
  . . .

  startNewRound()
}

The call to startNewRound() goes at the very end, right after present(alert, …).

Until now, the methods from the view controller have been invoked for you by UIKit when something happened: viewDidLoad() is performed when the app loads, showAlert() is performed when the player taps the button, 滑块Moved() when the player drags the slider, and so on. This is the event-driven model we talked about earlier.

也可以直接呼叫方法,这是您在此处的作用。您正在将对象中的一个方法发送消息到同一对象中的另一个方法。

In this case, the view controller sends the startNewRound() message to itself in order to set up the new round. Program execution will then switch to that method and execute its statements one-by-one. When there are no more statements in the method, it returns to the calling method and continues with that — either viewDidLoad(), if this is the first time, or showAlert() for every round after.

以不同方式调用方法

有时,您可能会看到这样的方法调用,如下所示:

self.startNewRound()

That does the exact same thing as startNewRound() without self. in front. Recall how I just said that the view controller sends the message to itself. Well, that’s exactly what self means.

要在对象上调用方法,您通常会写入:

receiver.methodName(parameters)

The receiver is the object you’re sending the message to. If you’re sending the message to yourself, then the receiver is self. But because sending messages to self is very common, you can also leave this special keyword out for many cases.

To be fair, this isn’t exactly the first time you’ve called methods. addAction() is a method on UIAlertController and present() is a method that all view controllers have, including yours.

当您编写SWIFT程序时,您所做的很多都是在对象上调用方法,因为您应用程序中的对象是如何通信的。

使用方法的优点

I hope you can see the advantage of putting the “new round” logic into its own method. If you didn’t, the code for viewDidLoad() and showAlert() would look like this:

override func viewDidLoad() {
  super.viewDidLoad()

  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

@IBAction func showAlert() {
  . . .

  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

你能看到这里发生了什么吗?在两个地方复制相同的功能。当然,它只是三行代码,但通常,您重复的代码可能会更大。

如果您决定改变此逻辑(如您的意愿),该怎么办?那么你将不得不在两个地方进行同样的变化。

如果您最近写过这段代码,您可能会记得这样做,并且在内存中仍然是新鲜的,但是,如果您必须在路上几个星期的变化,那么您只需在一个上更新它放置并忘记另一个。

代码复制是错误的重要来源。因此,如果您需要在两个不同的地方做同样的事情,请考虑为其制作新方法而不是重复代码。

命名方法

该方法的名称也有助于使其清楚地表明它应该做什么。你能瞥一眼以下是什么吗?

targetValue = Int.random(in: 1...100)
currentValue = 50
slider.value = Float(currentValue)

您可能需要推理您的方式:“它正在计算新的随机数,然后重置滑块的位置,所以我猜它必须是新一轮的开始。”

有些程序员将使用注释来记录发生的事情(并且您也可以这样做),但是,在我看来,以下比上述代码块更清晰,具有解释性评论:

startNewRound()

This line practically spells out for you what it will do. And if you want to know the specifics of what goes on in a new round, you can always look up the startNewRound() method implementation.

写得很好的源代码为自己说话。我希望我确信你制作新方法的价值!

➤运行应用程序并验证它在按钮上的每次抽头后计算1到100之间的新随机数。

You should also have noticed that, after each round, the slider resets to the halfway position. That happens because startNewRound() sets currentValue to 50 and then tells the slider to go to that position. That is the opposite of what you did before (you used to read the slider’s position and put it into currentValue), but I thought it would work better in the game if you start from the same position in each round.

锻炼:只要有趣,修改代码,使滑块不会在新一轮的开始时重置到中途位置。

类型转换

By the way, you may have been wondering what Float(…) does in this line:

滑块.value = Float(currentValue)

Swift是A. 强烈打字 language, meaning that it is really picky about the shapes that you can put into the boxes. For example, if a variable is an Int, you cannot put a Float, or a non-whole number, into it, and vice versa.

The value of a UISlider happens to be a Float — you’ve seen this when you printed out the value of the slider — but currentValue is an Int. So the following won’t work:

滑块.value = currentValue

The compiler considers this an error. Some programming languages are happy to convert the Int into a Float for you, but Swift wants you to be explicit about such conversions.

When you say Float(currentValue), the compiler takes the integer number that’s stored in currentValue and converts it into a new Float value that it can pass on to the UISlider.

因为Swift是关于这种类型的事情比大多数其他编程语言更严格,所以它通常是对语言的新人混淆的源泉。不幸的是,Swift的错误消息并不总是很清楚代码的一部分是错误的,还是为什么。

只要记住,如果您收到错误消息,“无法为”某些类型“的值分配”键入“其他东西”“,那么您可能正在尝试混合不兼容的数据类型。解决方案是明确地将一种类型转换为另一个 - 如果允许转换,当然是 - 正如您在此处完成的那样。

显示目标值

Great, you figured out how to calculate the random number and how to store it in an instance variable, targetValue, so that you can access it later.

现在,您将在屏幕上显示该目标号码。没有它,玩家不会知道瞄准什么,这将使游戏无法获胜。

设置故事板

When you set up the storyboard, you added a label for the target value (top-right corner). The trick is to put the value from the targetValue variable into this label. To do that, you need to accomplish two things:

  1. 为标签创建一个插座,以便您可以发送消息。
  2. 为标签新文本显示。

This will be very similar to what you did with the slider. Recall that you added an @IBOutlet variable so you could reference the slider anywhere from within the view controller. Using this outlet variable you could ask the slider for its value, through 滑块.value. You’ll do the same thing for the label.

➤in. ViewController.swift.,在其他出口声明下方添加以下行:

@IBOutlet var targetLabel: UILabel!

➤in. main.storyboard.,单击以选择正确的标签 - 在最顶部的标签上表示“100”。

➤去 连接检查员 并拖动 新的参考插座 在中央场景中查看控制器顶部的黄色圆圈。你也可以拖到 查看控制器 在文档大纲中 - 在接口构建器中有很多方法可以做同样的事情。

将目标值标签连接到其插座
将目标值标签连接到其插座

➤选择 targetlabel. 从弹出窗口,并进行连接。

通过代码显示目标值

➤ On to the good stuff! Add the following method below startNewRound() in ViewController.swift.:

func updateLabels() {
  targetLabel.text = String(targetValue)
}

您正在将此逻辑放在一个单独的方法中,因为它是您可能从不同的地方使用的东西。

该方法的名称使其清除它所做的:它更新标签的内容。目前它只是设置单个标签的文本,但后来您将添加代码来更新其他标签(总分,圆数字)。

The code inside UPD.ateLabels() should have no surprises for you, although you may wonder why you cannot simply do:

targetlabel..text = targetValue

再次答案是,您无法将一个数据类型的值放入另一个类型的变量 - 方形挂钩不会进入圆孔。

The targetlabel. 出口 references a UILabel object. The UILabel object has a text property, which is a String object. So, you can only put String values into text, but targetValue is an Int. A direct assignment won’t fly because an Int and a String are two very different kinds of things.

So, you have to convert the Int into a String, and that is what String(targetValue) does. It’s similar to what you’ve done before with Float(…).

Just in case you were wondering, you could also convert targetValue to a String by using string interpolation, like you’ve done before:

targetlabel..text = "\(targetValue)"

你使用的方法是一种味道。任何一种方法都会正常工作。

Notice that UPD.ateLabels() is a regular method — it is not attached to any UI controls as an action — so it won’t do anything until you actually call it. You can tell because it doesn’t say @IBAction before func.

动作方法与正常方法

那么动作方法和常规方法之间有什么区别?

回答: Nothing.

An action method is really just the same as any other method. The only special thing is the @IBAction attribute, which allows Interface Builder to see the method so you can connect it to your buttons, sliders and so on.

Other methods, such as viewDidLoad(), don’t have the @IBAction specifier. This is good because all kinds of mayhem would occur if you hooked these up to your buttons.

这是动作方法的简单形式:

@IBAction func showAlert()

您还可以通过参数要求对触发此操作的对象引用:

@IBAction func sliderMoved(_ slider: UISlider)
@IBAction func buttonTapped(_ button: UIButton)

但以下方法不能用作来自Interface Builder的操作:

func updateLabels()

That’s because it is not marked as @IBAction and as a result, Interface Builder can’t see it. To use UPD.ateLabels(), you will have to call it yourself.

调用方法

The logical place to call UPD.ateLabels() would be after each call to startNewRound(), because that is where you calculate the new target value. So, you could always add a call to UPD.ateLabels() in viewDidLoad() and showAlert(), but there’s another way, too!

What is this other way, you ask? Well, if UPD.ateLabels() is always (or at least in your current code) called after startNewRound(), why not call UPD.ateLabels() directly from startNewRound() itself? That way, instead of having two calls in two separate places, you can have a single call.

➤ Change startNewRound() to:

func startNewRound() {
    targetValue = Int.random(in: 1...100)
    currentValue = 50
    slider.value = Float(currentValue)
    updateLabels()  // Add this line
}

您应该能够键入方法名称的前几个字母,如 UPD.,Xcode将向您展示与您键入的内容匹配的建议列表。按 进入 接受建议(如果您在右侧项目 - 或滚动列表以查找合适的项目,然后按Enter):

Xcode自动完成提供建议
Xcode自动完成提供建议

你也可以使用 标签 要进行选择,但标签将从菜单中完成项目一次(或单词)的时间 - 尝试看看我的意思。因此,您可能必须在选择完成之前多次按下标签,而不是输入,然后立即选择当前项目。

值得注意的是,您不必开始键入您从开始的方法(或属性)名称 - Xcode使用模糊搜索并键入“Datel”或“Label”应该帮助您找到“UpdatElabels”就像容易地。

➤运行应用程序,您实际上会看到屏幕上的随机值。这应该让它更容易瞄准。

右上角的标签现在显示了随机值
右上角的标签现在显示了随机值

您可以在此点下找到应用程序的项目文件 04 - 插座 在源代码文件夹中。

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC