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

8,608 阅读7分钟

项目背景

和往常一样的下午,午后的阳光洒在身上,感觉有些舒服。突然脑海里闪过一个念头,小步快跑,起身,双手举起,目视前方,挥手投篮,唰.........最近一款APP几乎引起了所有男孩子的兴趣,这就是空气投篮。

想起年少时和一群朋友走在路上,男孩子们都或多或少做过这种“傻傻的”动作,那是青春的味道。

而竟然有家公司将它做了出来,一时间马上下载、安装.......额.......这.......需要AppleWatch才能用。好吧,竟然没有办法体验空气投篮,不如就用用自己的专长,画画iOS端的页面吧。

项目搭建

首先打开Xcode,创建一个新的SwiftUI项目,命名为AirBall,如下图所示:

空气投篮分为iOS端和Watch,本章我们先来完成iOS的相关页面。iOS端页面操作及其流程如下图所示:

实战编程

准备游戏视图

首先是准备游戏视图,简单分析可知,它由文字Text和图片Image组成,背景颜色填充为黑色。Image图片部分,需要导入一张SVG适量图片,使得其很好地与背景融合。如下图所示:

导入完成后,我们来构建页面部分。在ContentView文件中,我们键入以下代码:

// 准备游戏
func prepareView() -> some View {
	VStack(alignment: .center, spacing: 80) {
		Spacer()
		Text("请确定你已启用Apple Watch上的空气投篮App")
		.font(.system(size: 17))
		.foregroundColor(.white)
		.lineLimit(2)
		.lineSpacing(15)
		.multilineTextAlignment(.center)

		Image("watch_application")
		.resizable()
		.aspectRatio(contentMode: .fit)
		Spacer()
		Spacer()
	}.frame(maxWidth: Constants.screenWidth / 2)
}

上述代码中,我们创建了一个新的View视图prepareView准备开始游戏视图。

在prepareView视图中,文字Text和Image图片使用VStack垂直布局容器包裹,并设置其对齐方式为居中对齐,容器内容元素间距为80。

Text文字部分的处理为设置font字体为17号字,设置foregroundColor填充色为白色,由于文字过长可能导致页面无法展示的原因,这里设置lineLimit文字显示行数为2行,并设置换行时multilineTextAlignment文字对齐方式为居中对齐。

Image图片部分,设置resizable图片缩放,并设置aspectRatio保持原本的宽高比避免变形。

最后使用frame设置VStack垂直布局容器的宽度,为屏幕宽度的一半。为了增强用户体验,在VStack垂直布局容器中使用Spacer占位符,下方设置2个,上方设置一个,这个视觉元素就会展示在屏幕上部分2/3的位置,又是一个小技巧。

完成后,我们在Body中展示,如下代码所示:

ZStack {
	Color(.black).edgesIgnoringSafeArea(.all)
	prepareView()
}

上述代码中,我们使用ZStack叠加视图,将prepareView准备游戏视图和Color颜色叠加,颜色部分使用edgesIgnoringSafeArea忽略全部安全区域,便可让黑色背景铺满整个屏幕。

游戏列表视图

打开App,进入准备游戏视图,此时需要与Watch端联动,在Watch端确认后,iOS端将进入至游戏列表页面。

游戏列表页面的交互逻辑是,点击游戏卡片则进入到游戏中,在游戏列表页左右滑动可切换游戏。

我们先来完成单张游戏卡片的设计。分析得知,单张游戏卡片的内容包括3块内容:游戏项目、游戏说明、游戏封面。

我们创建一个新的视图,代码如下所示:

// MARK: 游戏项

struct gameRowView: View {

    var gameName: String
    var gameHelpText: String
    var gameImage: String

    var body: some View {
        VStack(alignment: .center, spacing: 60) {
            Text(gameName)
                .font(.system(size: 48))
                .bold()
                .foregroundColor(.white)

            VStack(alignment: .center, spacing: 10) {
                Text(gameHelpText)
                    .font(.system(size: 17))
                    .foregroundColor(.white)
                Image(gameImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxHeight:UIScreen.main.bounds.size.width - 20)
            }
        }
    }
}

上述代码中,我们创建了一个新的结构体gameRowView游戏项视图。

之所以创建一个新的结构体,而没有和上面prepareView准备游戏视图一样直接定义View视图,是因为我们需要将gameRowView游戏项视图作为“母版”,然后按照单个gameRowView游戏项视图构建多个一样样式的游戏卡片。

在gameRowView游戏项视图中,我们声明了3个String类型的变量:gameName游戏名称、gameHelpText游戏说明、gameImage游戏封面。

然后在其Body中创建样式,由于游戏名称的Text和其余两块在页面上还是有些距离,这里使用了2个VStack垂直布局容器,将游戏说明和游戏封面放在一个容器中,它们直接的间距为10,而再用一个VStack垂直布局容器再把游戏标题包裹在一起,间距为60。

这里额外再补充一个知识点。

就是游戏封面使用frame设置其大小的问题,由于我们导入的游戏封面图片可能存在大小不一致的问题,因此如果需要让这个游戏卡片看起来元素位置保持一致,由于使用了VStack垂直布局容器,因此,可以设置图片maxHeight高度为屏幕width宽度,再减去20留点边距。

这样做,无论图片尺寸是多少,每个游戏卡片展示的游戏名称、游戏说明、游戏封面的位置就保持一致了。

我们导入两张SVG格式的游戏图片作为素材使用,如下图所示:

游戏卡片是左右滑动切换的交互,这时我们就可以在ContentView视图中再创建一个View视图构建它,如下代码所示:

// 游戏列表
func gameListView() -> some View {
	TabView {
			gameRowView(gameName: "投篮", gameHelpText: "手举球开始游戏", gameImage: "basketball")
			gameRowView(gameName: "打棒球", gameHelpText: "双手挥动开始游戏", gameImage: "baseball")
		}
	.tabViewStyle(PageTabViewStyle())
}

上述代码中,我们创建了一个游戏列表视图gameListView。

然后使用TabView滚动视图容器包裹了2个gameRowView游戏项视图,游戏项视图中我们给声明的变量赋值以显示内容。最后设置TabView滚动视图的样式,为PageTabViewStyle分页滚动类型,如此便实现了横向切换游戏卡片的交互。

本章代码

为方便学习,本章完整代码如下所示:

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Color(.black).edgesIgnoringSafeArea(.all)
            
            gameListView()
        }
    }

    // 准备游戏
    func prepareView() -> some View {
        VStack(alignment: .center, spacing: 80) {
            Spacer()
            Text("请确定你已启用Apple Watch上的空气投篮App")
                .font(.system(size: 17))
                .foregroundColor(.white)
                .lineLimit(2)
                .lineSpacing(15)
                .multilineTextAlignment(.center)

            Image("watch_application")
                .resizable()
                .aspectRatio(contentMode: .fit)
            Spacer()
            Spacer()
        }.frame(maxWidth: UIScreen.main.bounds.size.width / 2)
    }

    // 游戏列表
    func gameListView() -> some View {
        TabView {
            gameRowView(gameName: "投篮", gameHelpText: "手举球开始游戏", gameImage: "basketball")
            gameRowView(gameName: "打棒球", gameHelpText: "双手挥动开始游戏", gameImage: "baseball")
        }
        .tabViewStyle(PageTabViewStyle())
    }
}

// MARK: 游戏项

struct gameRowView: View {
    var gameName: String
    var gameHelpText: String
    var gameImage: String

    var body: some View {
        VStack(alignment: .center, spacing: 60) {
            Text(gameName)
                .font(.system(size: 48))
                .bold()
                .foregroundColor(.white)

            VStack(alignment: .center, spacing: 10) {
                Text(gameHelpText)
                    .font(.system(size: 17))
                    .foregroundColor(.white)
                Image(gameImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(maxHeight: UIScreen.main.bounds.size.width - 20)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

本章小结

首先恭喜你,完成了本章的介绍的所有内容!

空气投篮iOS端的页面目前我们只完成了前2张,接下来,我们将继续完成其余的页面及其交互,以及后面也会切换到Watch端,完成空气投篮Watch端的相关页面设计。

总的来说,空气投篮App的页面及其交互并不复杂。

这个项目很小,却实实在在戳中了很多用户的内心。仿佛某些个人“很傻”的小习惯被大众所认知、所接受,这种满足感刚好刺激到了某个痛点,很小,但很痛。

这可能就是一款好的产品的象征,也应该是每个提供产品服务的企业所追求的。

望共勉之~

版权声明

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