实战编程·刻在男人DNA里的浪漫,空气投篮(二)

7,676 阅读8分钟

前提回顾

在上一章节中,我们完成了“准备游戏”页面和“游戏列表”页面,并完成了游戏列表的简单交互,在本章中,我们将继续完成其他的相关内容。

实战编程

页面切换

在整个空气投篮项目中,“准备游戏”页面和“游戏列表”页面的交互逻辑是,打开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预览下整体项目。

1664534091925-a33785ed-b6bb-42cc-bb18-1822cb65beaa.gif

本章小结

恭喜你,完成了本章的所有内容!

在本章中,我们构建了游戏详情页的视图,并完成了详情页的两种状态页面,准备开始游戏和游戏中的状态页面,还实现了从首页跳转到详情页、返回首页的全过程。

空气投篮项目iOS端整体的交互内容基本就到这里了,接下来我们将继续使用MVVM开发模式调整iOS端的内容,后面还会完成Watch端的页面及其交互。

最后如果条件成熟,我们将调用Apple提供的各种传感器来完成真实的交互体验。

请保持期待吧~

版权声明

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!