前提回顾
在上一章节中,我们完成了“准备游戏”页面和“游戏列表”页面,并完成了游戏列表的简单交互,在本章中,我们将继续完成其他的相关内容。
实战编程
页面切换
在整个空气投篮项目中,“准备游戏”页面和“游戏列表”页面的交互逻辑是,打开App展示“准备游戏”页面,同时唤起Watch端的授权,授权通过后,进入到“游戏列表”页面。
当前Watch端先行忽略,我们先完成切换切换的逻辑。首先声明一个变量存储切换动作,如下代码所示:
@State var isAffirmInWatch: Bool = false
上述代码中,我们声明了一个Bool类型的变量isAffirmInWatch
,初始状态位false。
当isAffirmInWatch是否授权状态为true时,我们进入到gameListView游戏列表页面,若没有授权,则停留在prepareView准备游戏页面。如下代码所示:
if isAffirmInWatch {
gameListView()
} else {
prepareView()
.onTapGesture {
self.isAffirmInWatch = true
}
}
上述代码中,为了演示方便,我们给prepareView准备游戏视图加了一个onTapGesture点击事件,当点击prepareView准备游戏视图时,切换isAffirmInWatch是否授权状态为true,如此在点击时便可进入到gameListView游戏列表视图。
游戏回合视图
接下来,当用户点击游戏列表的游戏项时,需要进入到游戏详情页。
而对于“投篮项目”来说,一般有3~5个回合,在正式游戏开始之前,会展示一个类似“Round1,Ready Go”的游戏回合页面,然后才是正式游戏详情页。
为此,我们需要创建一个新的SwiftUI页面来承载它,在Xcode左侧视图工具栏中新建一个SwiftUI文件,命名为GameDetailView
,如下图所示:
在“游戏回合”视图中,我们可以看到几个页面元素:游戏回合数(Round1)、游戏规则(5米)、游戏规则说明(距离篮筐)、游戏结果(投中:0)。
由于上述的参数会随着游戏更新内容,因此需要声明其变量进行存储,如下代码所示:
@State var roundNum:Int = 1
@State var distanceNum:Int = 5
@State var gameGoal:Int = 0
上述代码中,我们声明了3个变量:roundNum
游戏回合数、distanceNum
篮筐距离、gameGoal
单回合游戏得分。这里由于在后续要使用到数值计算,因此声明的变量都是Int
类型。
紧接着,我们来分析构建页面,如下代码所示:
// 游戏回合
func preStartView() -> some View {
VStack {
Spacer()
VStack(alignment: .center, spacing: 40) {
Text("Round" + String(roundNum))
.font(.system(size: 48, design: .rounded))
.bold()
.foregroundColor(.white)
VStack(alignment: .center, spacing: 20) {
Text(String(distanceNum) + "米")
.font(.system(size: 24))
.foregroundColor(.white)
.bold()
Text("距离篮筐")
.font(.system(size: 17))
.foregroundColor(.white)
}
}
Spacer()
Spacer()
Text("投中: " + String(gameGoal))
.font(.system(size: 17))
.foregroundColor(.white)
}
}
上述代码中,我们创建了一个新的视图preStartView
。
分析下页面元素,Text回合数文字为主要文字,使用48号圆形字体样式,并设置bold加粗,foregroundColor文字填充颜色为白色,其余文字基本修饰符类型。
这里科普一个知识点。
由于声明的变量是Int类型,而Text文字需要键入String类型的文本,因此需要将Int类型转换为String类型。SwiftUI对于类型转换可以直接使用类型包裹进行转换,示例:String(roundNum)
,如此便可以直接将roundNum游戏回合数转换为String类型的参数。
另外,我们可以使用“+”对字符串进行拼接,组成一个新的字符串,示例:
"I"+"Love"+"You" 结果为 "ILoveYou"
回归正题,文字部分也是使用VStack垂直布局视图进行包裹元素,这里的编程思想是“由中间向两边散开”,即相距离较近的元素可以先组合成一个容器,再和外边的容器进行组合,便于设置视图元素之间的间距。
完成好单个preStartView视图后,我们在Body中展示它,如下代码所示:
ZStack {
Color(.black).edgesIgnoringSafeArea(.all)
preStartView()
}
游戏中视图
在游戏回合视图展示后,用户会进入到“游戏中”视图,正式开始参与游戏。如下图所示:
空气投篮游戏的游戏视图很简答,还原在现实生活中的篮筐,由一个计分板和投篮的篮筐组成,而计分板分为两块,分别为个位数计分板和十位数计分板。
我们首先要导入“篮筐”的图片,同样是在黑色背景下,我们需要一张SVG格式的矢量图,如下图所示:
回到GameDetailView游戏详情页,我们来构建游戏中视图的样式,由于需要统计计分板的分数,因此需要声明好变量部分,如下代码所示:
@State var unitsDigit: Int = 0
@State var tensDigit: Int = 0
上述代码中,unitsDigit
为计分板个位数,tensDigit
为计分板十位数。然后,我们再构建样式部分,如下代码所示:
// 游戏页面
func playGameView() -> some View {
VStack(alignment: .center, spacing: 40) {
HStack(alignment: .center, spacing: 20) {
Text(String(unitsDigit))
.font(.system(size: 120))
.bold()
.foregroundColor(.white)
.padding(40)
.background(Color.gray)
.cornerRadius(8)
Text(String(tensDigit))
.font(.system(size: 120))
.bold()
.foregroundColor(.white)
.padding(40)
.background(Color.gray)
.cornerRadius(8)
}
Image("ball")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: UIScreen.main.bounds.size.width / 2)
}
}
上述代码,在playGameView
视图中,我们使用HStack横向布局容器包裹了计分板的样式内容。
关于Text文字背景部分,SwiftUI提供的方法是使用padding撑开距离,再使用background背景颜色填充撑开的间距,最后再使用cornerRadius设置圆角。
如此,便实现了计分板的样式效果,图片部分这里就不多说了。
同样,前期什么的Int类型的参数,在Text文字组件中使用需要转换成String字符串类型,方可使用。
此时我们就完成了2个页面:preStartView
游戏回合视图、playGameView
游戏中视图。这里做页面的切换,我们也可以声明一个参数来进行状态的切换,如下代码所示:
@State var isReady:Bool = false
然后通过声明好的isReady
参数进行页面间的切换,如下代码所示:
if isReady {
playGameView()
} else {
preStartView()
}
进入&返回
经过两个章节的学习,我们完成了两个主要的视图:ContentView
首页视图、GameDetailView
游戏详情视图,共4个页面,接下来,我们要将这4个页面串起来。
回到ContentView首页视图,我们盘一盘逻辑,在用户点击游戏项时,将会进入到GameDetailView游戏详情视图,游戏详情页首先会展示回合视图,然后再开始游戏。
了解了基本的交互逻辑后,我们先完成页面之间的跳转,这里可以使用基于NavigationView顶部导航栏的跳转方式,如下代码所示:
NavigationView {
ZStack {
Color(.black).edgesIgnoringSafeArea(.all)
if isAffirmInWatch {
gameListView()
} else {
prepareView()
.onTapGesture {
self.isAffirmInWatch = true
}
}
}
}
上述代码中,需要使用NavigationView
将整个视图包裹起来,然后再在gameListView游戏列表视图中添加跳转方法,如下代码所示:
NavigationLink(destination: GameDetailView()) {
gameRowView(gameName: "投篮", gameHelpText: "手举球开始游戏", gameImage: "basketball")
}
如此,当我们点击“投篮”的游戏项时,就会跳转到GameDetailView游戏详情页。
我们来到GameDetailView游戏详情页,由于当前GameDetailView游戏详情页的isReady参数变量为false,因此GameDetailView游戏详情页会展示preStartView游戏回合视图。
我们希望的交互是preStartView游戏回合视图在显示2秒后自动到playGameView游戏中视图。
这里在页面载入时增加多一个方法,如下代码所示:
ZStack {
Color(.black).edgesIgnoringSafeArea(.all)
if isReady {
playGameView()
} else {
preStartView()
}
}
.onAppear(){
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isReady = true
}
}
上述代码中,我们在GameDetailView游戏详情页onAppear
展示时添加了一个在主线程上的操作,即基于当前时间2秒钟后,切换isReady状态。
如此,我们便实现了在用户进入GameDetailView游戏详情页时,先展示preStartView游戏回合视图,再展示preStartView游戏中视图了。
进入操作有了,最后是返回操作。
原有的返回按钮太丑了,我们可以自定义一个返回按钮,如下代码所示:
// 返回上一页
func backButton() -> some View {
Button(action: {
}) {
Image(systemName: "chevron.left.circle")
.font(.system(size: 17))
.foregroundColor(.white)
}
}
并将它加到GameDetailView游戏详情页视图中,如下代码所示:
ZStack {
Color(.black).edgesIgnoringSafeArea(.all)
if isReady {
playGameView()
} else {
preStartView()
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton())
返回的操作交互也很简单,我们可以调用SwiftUI的通用返回方法,如下代码所示:
@Environment(.presentationMode) var mode
最后在点击backButton
返回按钮的时候使用返回方法,如下代码所示:
self.mode.wrappedValue.dismiss()
本章预览
完成后,我们回到ContentView预览下整体项目。
本章小结
恭喜你,完成了本章的所有内容!
在本章中,我们构建了游戏详情页的视图,并完成了详情页的两种状态页面,准备开始游戏和游戏中的状态页面,还实现了从首页跳转到详情页、返回首页的全过程。
空气投篮项目iOS端整体的交互内容基本就到这里了,接下来我们将继续使用MVVM开发模式调整iOS端的内容,后面还会完成Watch端的页面及其交互。
最后如果条件成熟,我们将调用Apple提供的各种传感器来完成真实的交互体验。
请保持期待吧~
版权声明
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!