iOS01-FirstiOSapp

第一个 Swift App

Getting Started with SwiftUI

Getting Started with SwiftUI

我们第一个app要做一个小游戏叫做 Bullseye,其实就是一个猜数字的游戏。游戏会随机指定一个数字,然后我们可以拖动进度条来猜一个数字。游戏会判断我们移动的数字是否和随机数相等。如下图所示:

在第一部分,我们列出了一个to-do list

  • Add the “Hit Me” button
  • Show a popup when the user taps it
  • Put game info on screen
  • Put slider on screen : 1 -> 100
  • Read value of the slider

首先,进入 xcode,我们可以在右上角选择预览的机型,这里我选择了iphone12,然后点击run就可以在一个虚拟的iphone中跑app了,如下图所示:

在代码的编写过程中,我们同时可以预览代码的效果,甚至可以直接在预览中对元素进行调整,xcode会自动进行相应的代码修改:用command+鼠标点击就能对这个中间的文字元素进行各方面的修改。我将字体设为绿色,半粗,发现xcode已经修改好了代码。

此外,我们还可以通过右上角的+来给我们的程序添加其他元素组件。直接拖入到我们想要的位置即可。如下:

我在文字下面添加了一个按钮,并修改了按钮和文字的内容。结果如下:

Objects, Data, and Methods

在swift中,构成app的基本要素也是对象。有些对象是iOS提供给我们的(button之类的),有些则需要我们自己去写。在上面展示的手机页面中,一共有三个对象—— Text Object、Button Object 和 ContentView Object(可理解为画布)。对象可以调用不同方法(用.)

现在我们来分析一下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
var body: some View {
VStack {
Text("welcome to my first swift app")
.fontWeight(.semibold)
.foregroundColor(Color.green)
.padding()
Button(action:{}/*@END_MENU_TOKEN@*/) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}
}
}
}

首先,ContentView 申明了一个变量属性body 。body相当于一个容器,将屏幕中的data都包含在内。在body后面写着some View,这其实是告诉 ContentView在body中要呈现的东西是一些view。

在body中首先是一个 VStack, 其作用就是垂直地把对象堆叠起来的一个栈。在这个栈中有两个对象,分别是 Text和 Button。然后,Text调用了三个方法:fontWeightforegroundColor(Color.green)padding() ;同理,在Button中设置了一个 Text属性,并调用了foregroundColor .

Buttons and Actions

细心地我们可能已经发现了,Button虽然已经放在页面上了,但在点击后没有为其编写事件。所以现在我们来做这个事。比如说我希望再点击后打印”Button pressed”, 在模拟手机中点击后,于Xcode中的All Output界面就会显示打印结果

1
2
3
4
5
6
Button(action: {
print("Button pressed")
}) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}

print()是一个很好的debug工具。

SwiftUI State

现在我们来介绍一个很重要的概念: State

我们用汽车仪表盘来举例。一辆汽车中有很多数据,数值类的有:时速、油量、里程数,布尔值有:是否低油量、是否需要保养等。这些数据构成了车的 State.

那么,当司机开始踩油门时,时速会发生变化 ;车内部这时候在燃油,因此油量也会发生变化。那么如果dashbord没有更新这些数据,则会引发很严重的事故。这就是我们需要及时更新state的原因。

那么采用及时更新的策略,可以实现什么功能呢?比如说:点击按钮后会出现弹窗(pop-up),或者是点击后整个页面发生了一些变化之类的。如下:

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@State var alertIsVisible : Bool = false
VStack {
var body: some View {
Text("welcome to my first swift app")
.fontWeight(.semibold)
.foregroundColor(Color.green)

Button(action: {
print("Button pressed")
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text("Hello there"),
message: Text("This is my first pop-up."),
dismissButton: .default(Text("Awesome!")))
}
}
}

现在我来解释一下代码:

首先,我们用@State修饰了一个变量 alertIsVisible 并将其置为false。
然后 ,当我点击button的时,会出发一个效果:将alertIsVisible置为True.
因为这个变量是 @state修饰的,因此会重新刷新view。
此时便落入到.alert 判定中,由于alertIsVisible = True,触发了弹窗。这个弹窗由三部分组成:titlemessagedismisButton。观察gif可以很清楚的找到对应。

总结:

当 State 更新的时候,view将重新校验UI,并且更新自身。也就是说,我们只要改变了 view 里面被关键词 @State 修饰的属性,整个 viewbody 就会被重新渲染。

Challenge

要求:现在我们要设计一个名为:”Knock Knock“ 的按钮。然后点击后会显示 ”Who’s Here?“。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct ContentView: View {
@State var alertIsVisible : Bool = false
@State var secondFlag : Bool = false
var body: some View {


VStack {
Button(action: {
print("Button pressed")
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text("Hello there"),message: Text("This is my first pop-up."),dismissButton: .default(Text("Awesome!")))

}
Button(action: {self.secondFlag = true}) {
Text("Knock Knock!")
}
.alert(isPresented: $secondFlag){()->
Alert in
return Alert(title: Text("Who's There"), message: Text("This is my seconde pop-up"), dismissButton:.default(Text("Nice!")))

}
}
}
}

Conclusion

  1. 我们学会了用canvas来写swift语言
  2. 我们学会了如何写一个button
  3. 我们学会了@State的作用

Creating User Interfaces with SwiftUI

现在我们要写用户界面了。我们列出了我们的To-Do List

  1. Add the “Hit Me” button
  2. Show a popup when the user taps it
  3. Put game info on screen
  4. Put slider on screen :1->100
  5. Read value of the slider

Portrait vs. Landscape

Portrait和landscape分别对应着iphone的竖屏和横屏模式。

在不同的手机代数上,每一个点所拥有的像素数也是不一样的。随着苹果手机的屏幕不断升级,像素也会越来越高。

直接点击左上角Bullseye项目 ,在 Deployment Info中选择是否要竖屏模式。因为我们这个游戏是横屏的,所以我们直接取消勾选Portrait即可

然后再模拟器右上角可以转变手机的方向:

但是在preview界面我们仍然保持着竖屏,这是我因为因为preview使用的是另外一套逻辑,我们可以这样修改:

1
2
3
4
5
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().previewLayout(.fixed(width: 896, height: 414))
}
}

如下所示

SwiftUI Views

现在我们要来写出Bullseye的基本页面元素:

这些框出来的都是 Views

我们现在已经接触到了一些Views: 比如 Text,Button 以及VStack。但是我们发现上面的元素并不是都是垂直分布的,有些是水平分布的,因此除了Vstack还需要HSTack

这个结构就是我们在 Vstack中一共要放下四行,每行又是一个HStack。如下:

1
2
3
4
5
6
7
8
9
10
VStack {
//Target row
...
//Slider row
...
//Button row
...
// Score row
...
}

SwiftUI Layout

Target row

我们可以在preview界面,将一个单独的Text元素嵌入到一个HStack当中,如下:

接下来,我们可以拖动一个新的Text对象放到Text旁边,注意不要放到整个Vstack的旁边

然后对内容进行修改即可:

1
2
3
4
5
6
7
8
VStack {
//Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("100")
}
//....
}

Slider row

现在我们来制作拖动条这一行。首先在拖动条左右各有刻度1,100,我们需要做的就是在中间加一个拖动条。其实也很简单,只要到对象库中把slider拖到Text中间即可:

1
2
3
4
5
HStack{
Text("1")
Slider(value: .constant(10))
Text("100")
}

但是现在这样我们是无法拖动的。 我们过一会来解决这个问题

Score row

1
2
3
4
5
6
7
8
9
10
11
12
HStack{
Button(action: {}) {
Text("Start Over")
}
Text("Score:")
Text("999999")
Text("Round")
Text("999")
Button(action: {}) {
Text("info")
}
}

这样算是写好了(Button row)之前已经写好了。但这和我们预想的界面还是有差,所以我们接下来要调整间隙。

Spacers and Padding

填入间隙也很简单——因为有个object就叫Spacer,拖进去即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
VStack {
Spacer()
//Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("100")
}

//Slider row
Spacer()
HStack{
Text("1")
Slider(value: .constant(10))
Text("100")
}

//Button row
Spacer()
Button(action: {
print("Button pressed")
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text("Hello there"),message: Text("This is my first pop-up."),dismissButton: .default(Text("Awesome!")))

}
// Score row
Spacer()
HStack{

Button(action: {}) {
Text("Start Over")
}
Spacer()
Text("Score:")
Text("999999")
Spacer()
Text("Round")
Text("999")
Spacer()
Button(action: {}) {
Text("info")
}
}
.padding(.bottom,20)
}

swift()会根据spacer()的大小来自动调整间距。如果觉得离边框太近,还可以使用.padding()这里用来调整score row距离底部的位置,大小为20个点位

除了.bottom之外还有:

1
2
3
4
.top
.horizental
.vertical
.all

字面意思,应该不难理解。

整体效果如下

Bindings

之前我们埋下了一个雷:就是这个滚动条是无法拖动的。那这个问题怎么解决呢?

我们可以使用binding这个小技巧。所谓绑定,就是将一个用@state修饰的变量与用户界面实时绑定,实时更新。在这里我们就可以将slider与一个 state variable绑定在一起。

1
2
3
4
5
6
7
8
9
10
@State var sliderValue: Double  = 50.0
VStack{
//...
HStack{
Text("1")
Slider(value: self.$sliderValue,in:1...100)
Text("100")
}
//...
}

value:self.$sliderValue 代表这个slider与sliderValue成功绑定在了一起,拖动slider时会实时更新sliderValue

需要注意的是,当使用Binding时,State变量前面一定要加一个$

in: 1...100 代表从1开始到100

Strings

相信我们对Strings已不再陌生。那么在strings中引用变量该怎么办呢?

1
"Hello ,\(name)!"

比如在这个字符串中就引用了name变量,只要在前面加个反斜杠,再用括号将变量括起来就可以了。

现在我们要让我们的app在点击Hit Me之后渲染出滑动条的数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Button(action: {
print("Button pressed")
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(self.sliderValue)"),
dismissButton: .default(Text("Awesome!")))

}

但是现在显示的是一个小数,接下来我们要用四舍五入的方式来取到一个整数

Variables

在Swift中,这是一个创建变量的基本模板:(@State是可选项)

1
@State var variableName : Type = InitialValue

一般变量的命名法是骆驼命名法。

那么如果X是浮点数,我新命名一个y为整数。怎么对x进行强制类型转换呢?

1
2
var x : Float = 3.14
var y : Int = Int(x)

我们可以在Button中申请一个临时变量用来存放四舍五入后的sliderValue

那有没有能将浮点数四舍五入的函数呢?我们可以command+点击Double查看快速帮助。里面确实有rounded()的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Button(action: {
print("Button pressed")
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)
}
.alert(isPresented: $alertIsVisible){()->
Alert in
var roundedValue : Int = Int(self.sliderValue.rounded())
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(roundedValue)"),
dismissButton: .default(Text("Awesome!")))

}

细心地我们也许会发现,为什么有些时候我们要用self.来引用一个变量,有时候又可以直接用呢?使用self.有三个层次:

  1. 当我们想在一个闭包中设置self.property的时候,我们必须使用self.
1
2
3
4
5
6
7
dispatch_async(dispatch_get_main_queue(), {
// we cannot assign to properties of self
self.view = nil

// but can access properties
someFunc(view)
})
  1. 当我们想区分class propertylocal variable时,我们应该要用self.
1
2
3
4
5
6
7
8
class MyClass {
var someVar: String = "class property"

func setProperty(someVar:String = "method attribute") -> () {
print(self.someVar) // Output: class property
print(someVar) // Output: method attribute
}
}
  1. 当我们想表明常量/变量是从哪里来的时候,我们可加可不加。

在我们写的Bullseye中,self.的情况基本属于第二种。在Button这个对象中,对sliderValuealertIsVisible这类变量,使用self可以说明这些变量是属于前实例(content value)的。如果我们这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
Button(action: {
//...
}) {
//..
}
.alert(isPresented: $alertIsVisible){()->
Alert in
var sliderValue : Int = Int(self.sliderValue.rounded())
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(sliderValue)"),
dismissButton: .default(Text("Awesome!")))

}

swift也是不会报错的,因为我们定义了两个sliderValue,一个是作用于contentView全局的,一个只是在Button中生效的。我在message中引用的时候,如果写self.sliderValue()那么就是引用Double类型的、作用于全局的sliderValue,但是如果我直接写sliderValue()那么引用的是Int类型的,作用于Button的sliderValue。但这样太搞脑子了,我们还是恢复原来的写法:

1
2
3
4
5
6
.alert(isPresented: $alertIsVisible){()->
Alert in
var roundedValue : Int = Int(self.sliderValue.rounded())
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(roundedValue)"),
dismissButton: .default(Text("Awesome!")))

但是这样我们就不能使用self.roundedValue了,因为我们在全局中并没有定义roundedValue,swift到前实例当中去找的时候是不会发现有这个变量的。

Swift Basics

Introduction

在这部分我们会学习一些Swift的基本语法。这是我们的To-Do List:

  • The Swift Standard Library
  • Writing Methods
  • If/Else Statements
  • Variables Vs. Constants
  • Type Inference
  • Variable Scope

The Swift Standard Library

Xcode中自带了swift文档。对于不懂的语法可以直接按住 Option(⌥) 后点击该词,或者直接搜索也可以。还有一个好办法就是利用alfred中的Dash工作流。比如我想生成一个随机整数,怎么搜呢?

1
2
3
4
5
6
7
8
@State var target: Int = Int.random(in: 1...100)
//...
var body: some View {
VStack {
HStack {
Text("\(self.target)")
}
}

这样就可以每次都生成一个整数了

Writing Methods

我们写到现在还没有写过函数,那么现在我们就来写我们第一个swift方法

基本格式:

1
2
3
4
func yourMethodName() -> ReturnType{
// Write our code
return yourReturnValue
}

一般方法写在整个body之后。swift会自动查找全局的方法,所以不用像C语言那样在一开始就声明。

那么怎么调用函数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Button(action: {
//...
}) {
//...
}
.alert(isPresented: $alertIsVisible){()->
Alert in
var sliderValue : Int = Int(self.sliderValue.rounded())
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(sliderValue).\n"
+"You scored \(self.pointsForCurrentRound()) points this round"),
dismissButton: .default(Text("Awesome!")))

}

和调用成员变量一样,可以用self.yourMethodName()来调用我们写好的方法。

Challenge- How To Calculate the Difference

现在我们要修改刚才那个pointForCurrentRound()函数,让其返回目标和滑动条之间的差值。

这是我的解决方案:

1
2
3
4
func pointsForCurrentRound() -> Int {
var absolute = 100-abs(Int(self.sliderValue.rounded())-self.target)
return absolute
}

如下图所示:

If-Else Statements

之前我们讨了个巧,直接用 abs()来计算差的绝对值。其实最朴素的方法就是用if-else语句来写:

1
2
3
4
5
6
7
if sth_is_true {
then_do_this
} else if sth_else_is_true {
then_do_that
} else{
do_something_when_neither_of_the_above_are_true
}

那么根据这个模板我们可以这样写逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
func pointsForCurrentRound() -> Int {
var difference : Int
var roundedValue : Int = Int(self.sliderValue.rounded())
if roundedValue > self.target {
difference = roundedValue-self.target
}else if self.target > roundedValue{
difference = self.target-roundedValue
}else{
difference = 0
}
return difference
}

也可以这么写:

1
2
3
4
5
6
7
8
9
func pointsForCurrentRound() -> Int {
var roundedValue : Int = Int(self.sliderValue.rounded())
var difference : Int = self.target-roundedValue
if difference < 0 {
difference = difference * -1
}
var awardedPoints : Int = 100-difference
return awardedPoints
}

Variables and Constants

相信我们对常量和变量已经足够了解了。事实上我们之前都是用 var来声明常量和变量的。但Xcode会给我们warning:

事实上,常量一般是不允许改变的,而变量是可以一直改变的。

因此我们要养成好习惯: 用 var来声明变量而用let来声明常量

Type Inference

当我们能确定一个变量(常量)它的数值类型的时候,我们可以直接用等于号赋值:

1
2
3
let sliderValueRounded : Int = Int(self.sliderValue.rounded())
//可写为:
let sliderValueRounded = Int(self.sliderValue.rounded())

Variable Scope

Swift有三种 变量作用域:Global scope, Instance scope, Local scope

  • Global scope 就是可以在整个项目中都可以引用
  • Instance scope 就是可以在一整个实例中可以使用。比如target,sliderValue 就作用于整个ContentView
  • Local scope 则是在一个作用于一个method的变量。比如我们上面写的sliderValueRounded只作用于pointsForCurrentRound()这个方法

Clean Code

无论用什么语言写程序,我们都要遵守一个原则:Dry(Don’t Repeat Your Code)。因此我们可以把重复的代码提取出来成为一个Method

1
2
3
4
5
6
7
func pointsForCurrentRound() -> Int {
100 - abs(target - sliderValueRounded())
}

func sliderValueRounded() -> Int {
Int(sliderValue.rounded())
}

More Swift Basics

Challenge- Showing the Total Score

现在我们想在底部的 Score row 显示我们游戏的总分。怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView: View {
//...
@State var totalScore : Int = 0
var body: some View {
VStack {
// Score row
Spacer()
HStack{
//...
Text("Score:")
Text("\(totalScore)")
//...
}
}
}
}

Your First Bug

现在我们的游戏出了点小问题,就是说我每次点击后目标数值并不会重新刷新。

最朴素的解决方式就是在点击Hit Me按钮的时候重置target

但是我们要注意,不能在Button的action作用域中写刷新语句。因为计算、呈现都发生在action之后,如果我们在action时就刷新了target,那么计算时就会以新的target为标准。

就如同下图,本来的target是20,但是在我点按钮的时候刷新到了66,那么游戏计算得到的分数就不是100而是$100-|66-20|=54$

那么怎么解决这个问题呢?我们可以把逻辑设置成当点击 dismiss button时更新分数以及target,因为dismiss button也是按钮,因此swift允许在点击按钮之后再运行一些代码。因此我们可以在.default()之后打一个大括号,括号中写需要执行的代码

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Button(action: {
self.alertIsVisible = true
self.totalScore = self.totalScore + pointsForCurrentRound()
}) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text("Hello there"),
message: Text("The slider's value is \(sliderValueRounded()).\n"+
"You scored \(self.pointsForCurrentRound()) points this round"),
dismissButton: .default(Text("Awesome!")){
target = Int.random(in: 1...100)
self.totalScore = self.totalScore + pointsForCurrentRound()
})

}

Challenge- Tracking Rounds

现在我们还要把轮次写上去。同样的我们要把逻辑加载 Dismiss Button 之后:

Better Alert Title

现在我们要设计一个更好的弹出提示。

比如说当我们刚好击中了target,弹出的文本可以是”Perfect”
,当离target很近的时候,可以是”You almost had it”
一般近的时候,可以是”Not Bad”
最差一档是:”Are you even trying ?”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
struct ContentView: View {
@State var alertIsVisible : Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)
@State var totalScore : Int = 0
@State var round : Int = 1
var body: some View {


VStack {
Spacer()
//Target row
HStack {
Text("Put the bullseye as close as you can to:")
Text("\(self.target)")
}

//Slider row
Spacer()
HStack{
Text("1")
Slider(value: self.$sliderValue,in:1...100)
Text("100")
}

//Button row
Spacer()
Button(action: {
self.alertIsVisible = true
}) {
Text("Hit me")
.foregroundColor(Color.blue)/*@END_MENU_TOKEN@*/
}
.alert(isPresented: $alertIsVisible){()->
Alert in
return Alert(title: Text(self.alertTitle()),
message: Text("The slider's value is \(sliderValueRounded()).\n"+"You scored \(self.pointsForCurrentRound()) points this round"),
dismissButton: .default(Text("Awesome!")){
target = Int.random(in: 1...100)
self.totalScore = self.totalScore + pointsForCurrentRound()
self.round = self.round+1
})

}
// Score row
Spacer()
HStack{

Button(action: {
totalScore = 0
}) {
Text("Start Over")
}
Spacer()
Text("Score:")
Text("\(totalScore)")

Spacer()
Text("Round")
Text("\(round)")
Spacer()
Button(action: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Action@*/{}/*@END_MENU_TOKEN@*/) {
Text("info")
}
}
.padding(.bottom,20)
}
}

func amountOff() -> Int {
abs(target - sliderValueRounded())
}

func pointsForCurrentRound() -> Int {
100 - amountOff()
}

func sliderValueRounded() -> Int {
Int(sliderValue.rounded())
}

func alertTitle() -> String {
let difference = amountOff()
let title : String
if difference == 0{
title = "Perfect!"
}else if difference<5{
title = "You almost had it!"
}else if difference <= 10 {
title = "Not Bad."
}else{
title = "Are You even trying?"
}
return title
}
}

Challenge- Bonus Points

现在我们再加入一个机制。就是当我们完美击中目标时,会获得额外的奖励100分。而离目标仅有1时,会获得额外50分

1
2
3
4
5
6
7
8
9
10
11
12
13
func pointsForCurrentRound() -> Int {
let maximumScore = 100
let difference = amountOff()
let bonus : Int
if difference == 0 {
bonus = 100
}else if difference == 1{
bonus = 50
}else{
bonus=0
}
return maximumScore - difference + bonuså
}

Challenge- Starting Over

这个很简单,直接重置Score、Round、sliderValue为初始值即可,同时要刷新一个随机值。我们可以写一个没有返回值的函数来是现这些功能

1
2
3
4
5
6
7
8
9
10
11
12
Button(action: {
self.startNewGame()
}) {
Text("Start Over")
}

func startNewGame() {
totalScore = 0
round = 1
sliderValue = 50.0
target = Int.random(in: 1...100)
}

Styling the App

Introduction

到现在为止app的基本框架我们已经搭好了,但是还需要美化一下。因此点击这里 下载需要的图片

Images

首先我们要把要用的照片都拖动到 Assets 文件夹当中:

我们会发现图片分为三个档次-1x,2x,3x 其实这和图片的分辨率有关系。

1x 适合最早没有视网膜屏的手机

2x 适合视网膜屏的手机

3x 适合高清视网膜屏的手机(10代以后)

对于不同的手机像素我们可以上这个网站查看

那么怎么给我们的app设置背景呢?可以搜索apple 文档

然后到body的最后面设置整个body的背景

1
.background(Image("Background"),alignment: .center)

View Modifiers

但是这个背景并不适合黑体字,而且字体也不太好看。因此我们还需要修改一下。对于文字,我们可以之间用command+click修改,但是如果要修改字体、添加阴影,那么直接写代码反而会变得方便。关于苹果可以用哪些字体,我们可以上这个网站 查看. 我们使用的是 Arial Rounded MT Bold

1
2
3
4
Text("Put the bullseye as close as you can to:") 
.foregroundColor(Color.white)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
.font(Font.custom("Arial Rounded MT Bold", size: 18))

对于Text的修改如上. 聪明的你可能会问:这么多Text,难道对于每一条我们都要修改这么多吗? 其实我们有更好的方法。那就是创建一个ViewModifier类型的结构,其作用就是对view进行格式修改。如下:

1
2
3
4
5
6
7
8
9
10
11
    struct LabelStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.white)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
.font(Font.custom("Arial Rounded MT Bold", size: 18))
}
}

//然后对于每一个Text,只要在后面加上 .modifier(LabelStyle())即可。如:
Text("Put the bullseye as close as you can to:").modifier(LabelStyle())

Challenge

现在我们要修改值得样式,因为值需要看起来更大一些,更好辨认一些。我们的要求是:

  • Create a ViewModifier named ValueStyle
    • Foreground color: Yellow
    • Font: Arial Rounded MT Bold , size 24
    • Shadow: The same as in LabelStyle
  • Bonus : Can you make the code DRY
  • Hint: ViewModifiers can use other ViewModifiers!

第一个任务很简单,就是创建一个基本一模一样的struct

1
2
3
4
5
6
7
8
struct ValueStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.yellow)
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
.font(Font.custom("Arial Rounded MT Bold", size: 24))
}
}

但是问题是,我们发现两个struct的shadow是重复的,因此我们是否可以利用dry原则进行进一步优化呢?我们可以将shadow样式的语句单独提出来再做成一个ViewModifier,因为ViewModifier本身也可以调用其他ViewModifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct LabelStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.white)
.modifier(ShadowStyle())
.font(Font.custom("Arial Rounded MT Bold", size: 18))
}
}

struct ValueStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.yellow)
.modifier(ShadowStyle())
.font(Font.custom("Arial Rounded MT Bold", size: 24))
}
}

struct ShadowStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.shadow(color: Color.black, radius: 5, x: 2, y: 2)
}
}

Styling the Buttons and Slider

现在我们来修改按钮以及滑动条的样式。

首先是给按钮加一个背景。这个很简单就不说了

其次是我们想给按钮的字体换一个颜色换一个大小。于是我们仍需要创建一个 ViewModifier :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ButtonLargeTextStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.black)
.font(Font.custom("Arial Rounded MT Bold", size: 18))
}
}

struct ButtonSmallTextStyle: ViewModifier{
func body(content: Content) -> some View {
return content
.foregroundColor(Color.black)
.font(Font.custom("Arial Rounded MT Bold", size: 12))
}
}

现在还需要把返回图表和信息图表加到按钮上去。为了实现这个操作,我们可以在按钮中添加一个HStack。

1
2
3
4
5
6
7
8
Button(action: {
self.startNewGame()
}) {
HStack {
Image("StartOverIcon")
Text("Start Over").modifier(ButtonSmallTextStyle())
}
}.background(Image("Button")).modifier(ShadowStyle())

最后,我们将默认颜色设置成自定义的深空蓝

1
2
let midnightblue = Color(red: 0.0/255.0, green: 51.0/255.0 ,blue: 102.0/255.0)
.accentColor(midnightblue)

App Icon and Display Name

现在我们要给我们的Bullseye添加一个图标

很简单的操作

Running the App on Your Device

最后我们要在手机上运行这个app。首先用手机连上电脑,在Window菜单下拉找到devices and simulators界面。然后打开手机信任电脑

之后,在preference中需要写上与手机上一致的Apple ID

最后,在最上面的设备栏将 simulator改成自己的手机。点击运行就可以了。

-------------本文结束,感谢您的阅读-------------