SwiftUI自学成长笔记
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.5 创建小吃视图页面

在本节中我们会为这里是北京App创建界面布局最为复杂的视图场景。

2.5.1 设计横幅视图布局

我们先设计一个横幅图片视图,它包括特定小吃的图片及相关文字说明。

在View文件夹中创建一个新的SwiftUI类型文件,将其命名为HeaderView。修改Body部分的代码如下。

考虑到要在图片上面呈现介绍小吃的相关文字,在Body部分,我们使用层叠堆栈(ZStack)作为最外层的容器。需要注意的是,ZStack容器中的所有视图的对齐方式默认都是居中对齐,这里使用frame修饰器将ZStack容器的尺寸设定为480点×320点。

ZStack中的底层是一个Image,其上面则是一个HStack容器。我们利用它呈现文字信息。它的尺寸是285点×105点,因为想将其放置在图片的左下角,所以这里需要通过offset修饰器将其从中央沿x轴方向向左移动66点,向下移动80点。

在HStack容器的内部是一个矩形图形和一个VStack容器,这里通过参数设置其子视图为顶端对齐,间隔距离为0。矩形图形为文字信息框添加竖条颜色块效果,VStack容器则用于组织竖排放置两个Text,并且两个Text左对齐,上下有6点的间隔距离。通过计算可以知道,将矩形图形的宽度设置为4点,VStack容器的宽度设置为281点,两个加起来正好是HStack容器的宽度285点。另外,我们为VStack容器设置了ColorBlackTransparentLight背景色,让文字信息可以更突出一些。

在预览窗口中我们可以看到如图2-21所示的效果。因为我们提供了两种不同模式下的驴打滚-slice-1图片,所以系统会根据情况自适应载入相应图片。

图2-21 两种模式下呈现的横幅图片效果

接下来,我们继续为HStack容器添加动画效果。仿照BeijingView的样子,在Properties部分添加一个被@State封装的属性showHeadline,再添加一个和动画相关的计算属性slideInAnimation,然后将Body部分的代码修改成下面的样子。

showHeadline变量是启动动画的标志,因为是对HStack容器添加动画效果,所以为其添加onAppear和onDisappear修饰器,当容器出现在屏幕上时,showHeadline的值为true,反之则为false。根据showHeadline值的变化,通过HStack容器的offset修饰器,我们让其y值从190移动到80。

那么,HStack容器会执行一个什么样的动画效果呢?这里通过slideInAnimation计算其属性,生成一个Animation类型的对象,我们将这个动画设置为弹簧效果,response参数代表弹簧的刚度系数,它是以秒为单位的近似时间,如果值为0则代表动画为无刚度的弹簧动画。dampingFraction参数代表阻尼系数,它控制着视图反弹的时间。如果为0则意味着无阻尼,它将永远反弹。如果阻尼值大于1则根本不会有弹起的效果。在通常情况下,我们会选择一个介于0到1之间的值,较大的值会降低速度。blendDuration参数用于调整弹簧的响应值。最后,我们为HStack容器添加animation修饰器,将slideInAnimation作为修饰器参数即可。

2.5.2 创建横幅滚动视图

创建好HeaderView以后,我们就可以借助ScrollView在FoodView中创建横幅滚动视图了。在项目导航中打开FoodView.swift文件,修改Body部分代码如下。

因为在FoodView中要呈现的内容比较多,所以在Body部分的代码中,最外层是一个纵向滚动视图,其内部则通过一个VStack容器组织所有的视图。VStack容器的顶端为横向滚动视图,里面会嵌套一个HStack容器,用于呈现所有的HeaderView。注意HStack容器的对齐方式为顶端对齐,内部子视图之间的距离为0,这样才能保证视图会紧密连接在一起。

在Header部分的下面,我们再添加一个Footer视图,同样利用VStack容器组织。其内部的两个Text间隔20点。有意思的是,我们为VStack容器连续设置了两个padding,第一个为容器的四周添加默认的间隔距离,第二个为容器的底部添加85点的间隔距离,这样相当于左、右、上的间隔相同,底部间隔则大一些。

对于最外层的ScrollView,我们还要添加edgesIgnoringSafeArea修饰器,这样就可以充满整个屏幕了,在预览窗口中的效果如图2-22所示,你甚至可以启动Live模式,尝试横向滚动HeaderView,只不过现在只有一个视图而已。

图2-22 FoodView在预览窗口中的效果

2.5.3 获取HeaderView所需的静态数据

本节我们将从文件中获取Header相关的静态数据,完成HeaderView横向滚动视图。在Model文件夹中新建一个Swift类型文件,将其命名为HeaderModel,数据模型的代码如下。

Header结构体必须符合Identifiable协议,这样我们就可以在ForEach循环语句中遍历所有的静态数据。因为要符合Identifiable协议,所以结构体中必须有id属性,image、headline和subheadline三个属性则是需要在HeaderView中显示的数据内容。

在Data文件夹中新建一个Swift类型文件,将其命名为HeaderData。将“项目资源/Data”文件夹里面HeaderData.swift文件中的全部代码复制到其中即可。该文件会提供一个Header类型的数组headersData

接下来,回到HeaderView中,在Properties部分添加一个属性。

Header类型的属性用于接收从外部传递进来的Header信息,因此在HeaderView_Previews的内部,需要为HeaderView实例添加相应的调用参数。

最后,我们还需要在Body部分将之前的Image和两个Text修改为下面这样。

让我们回到FoodView中,在Properties部分添加一个属性,然后修改Body部分与Header相关的代码。

headers属性用于获取所有的Header数据,然后在Body部分通过ForEach循环遍历headers中所有的Header类型元素,再将每一个元素都传递给HeaderView即可,在预览窗口中启动Live模式,可以看到如图2-23所示的效果。

图2-23 FoodView的两种模式在预览窗口中的效果

2.5.4 创建灵活的表格式布局

本节我们将创建一个非常灵活的表格式布局,外观如图2-24所示。它的布局结构看似复杂,但是利用SwiftUI不用太费力就可以快速完成搭建,只不过在布局的过程中一定要合理使用各种容器,并设置容器的对齐方式。

通过观察你应该可以在头脑中形成一个初步的布局了,最外层应该是一个HStack容器,其内部从左到右是3个VStack容器,第一个VStack容器与第三个大致相同,里面都含有4个HStack容器,只不过左边的是图标+文字,而右边的是文字+图标,文字和图标之间的空间则通过Spacer撑开,HStack容器之间靠Divider分割线分割。

对于中间的VStack容器,其内部分为上中下3部分,上下两部分都是HStack容器,用于呈现竖线,中间则是一个Image。

图2-24 FoodView中的表格式布局

在View文件夹中新建一个SwiftUI类型文件,将其命名为CookingWayView。因为该视图具有固定的宽度和高度,所以先修改Preview部分代码如下,该视图的尺寸被固定在414点×280点。

继续修改Body部分的代码,我们把表格视图的架构搭建好。

在顶层的HStack容器中,我们设置了三列,第一列和第三列为VStack容器,第二列目前只是一个Image。其中第一列和第三列中均只有一个HStack容器,不同的地方是内容Image和Text的呈现顺序。另外,在设置HStack容器的font修饰器的时候,启用了callout风格,它是插图标注样式。

如果继续编写代码,那么读者可能都会想到,我们还需要添加6个Image,并且要为这些Image添加frame修饰器来设定大小,将来的问题可能就出现在这里,因为现在Image的尺寸是42点×42点,如果客户需要将其调整为45点×45点,那么我们一共要修改16处。优秀的代码一定要避免出现这种手动硬性调整的情况,所以接下来我们会创建自定义视图修饰器(custom view modifier),从而实现复用。

在CookingWayView结构体下面,Preview部分的上面,添加一个新的结构体。

自定义视图修饰器IconModifier必须符合ViewModifier协议,该协议要求结构体中必须有body方法,其中,参数content是将要添加修饰器效果的视图对象,返回值是添加好修饰器效果的视图对象。这里为传递进来的视图对象添加frame修饰器,设定宽度和高度为42点,对齐方式为中心对齐。

在需要调用自定义视图修饰器的地方,我们只需要为其添加下面的语句即可。

modifier修饰器的功能是调用自定义视图修饰器,这样不管客户最终对于图标的尺寸要求是多少,我们都只需要修改一个地方。

继续修改CookingWayView的Body部分如下。

虽然需要添加的代码很多,但是并没有什么技术难度,只要保证结构清晰就可以做出如图2-24所示的效果。

让我们打开FoodView.swift文件,在添加CookingWayView之前,可以为标题创建一个全新的自定义视图修饰器,在FoodView结构体下方添加下面的代码。

这里设置标题的字体为title,颜色为ColorBrownAdaptive,这样就可以自动适应不同的模式了,它的四周有8点的间隔距离。

然后在Body部分添加下面的代码。

对于Text我们为其添加了TitleModifier自定义视图修饰器,对于CookingWayView,我们设置了它的最大宽度为640点。在预览窗口中看到的效果如图2-25所示。

图2-25 FoodView在预览窗口中的效果

2.5.5 创建横幅滚动视图

接下来我们要在烹制方法视图的下面创建一个北京特色小吃的横幅滚动视图,与之前FoodView顶部的横幅滚动视图类似,这里需要先创建一个北京特色小吃卡片视图。

北京特色小吃卡片视图的设计比较有意思,它由Image和Text组成,但是这次我们借助ZStack容器将二者组合到一起,形成如图2-26所示的样子。对于Image部分,这回不仅对其进行了圆形裁剪,而且在其外面嵌套了三个圆环,其中的一个圆环还使用了线性渐变色的特性,相信将来在你的应用程序中会经常用到这样的布局方式。

图2-26 北京特色小吃卡片在不同模式下的效果

在View文件夹中新建一个SwiftUI类型的文件,将其命名为FoodCardView。在Preview部分,通过下面的代码将预览视图的大小设置为400点×220点。

接着,修改Body部分的代码如下。

对于多行文本,我们设定它的尺寸为300点×135点,背景色为从ColorBrownMedium到ColorBrownLight的线性渐变色,方向为从左到右。整个视图采用圆角矩形,文本行数限制在6行,文本的颜色为白色,效果如图2-27所示。

图2-27 FoodCardView在预览窗口中的效果

为了可以呈现出图片与文字的叠加效果,接下来,我们要在Text的外层嵌套一个ZStack容器,并且在Text的下面添加一个Image,代码如下。

在ZStack容器中的Image的大小为66点×66点,将其裁剪为圆形,再利用offset修饰器将其向左移动150点。此时的Image会遮挡Text中的部分文字,所以在Text中,用3个padding修饰器替换了之前的一个padding修饰器,让其左侧空出55点的空间,右侧空出10点的空间,上下各空出3点的空间,此时的效果如图2-28所示。

图2-28 FoodCardView在预览窗口中的效果

最后,我们需要为圆形图片添加由小到大的3层圆环,为Image添加3个修饰器。

实际上,我们对Image连续添加了3个背景视图,每个背景视图都是一个圆,只不过圆的填充色不同,并且尺寸越来越大。另外,第二个圆的填充色使用了线性渐变,所以就产生了如图2-29的效果。

在完成了特色小吃卡片的设计以后,需要我们编写相关的数据模型。在Model文件夹中新建一个Swift类型的文件,将其命名为FoodModel,添加如下代码。

图2-29 FoodCardView在预览窗口中的效果

继续在Data文件夹中新建一个Swift类型的文件,将其命名为FoodData,并将“项目资源/Data”文件夹里面同名文件的内容复制到该文件中。

让我们回到FoodCardView,在Properties部分添加一个变量属性。

然后在Preview部分为FoodCardView添加必要的参数。

最后,修改Body部分Image和Text的参数即可。

如果你愿意,此时可以修改Preview部分food参数的调用值,将foodsData数组的索引值修改为0至11的任何数字,并在预览窗口中查看效果。

在制作完成特色小吃卡片视图以后,就可以回到FoodView生成横向滚动视图了。在项目导航中打开FoodView,然后在Properties部分添加一个新的属性。

在Body的烹制方法代码的下面,添加一段代码。

在这部分代码中,继续使用自定义视图修饰器来设置标题的样式。在其下面则利用ScrollView+HStack容器生成横向滚动视图,效果如图2-30所示。

图2-30 FoodView在预览窗口中的效果

2.5.6 创建特色小吃店卡片视图

本节,我们将设计并制作北京特色小吃店的卡片视图,该视图用于显示小吃店的特色菜品和基本信息。因为考虑到其他方面的因素,卡片中所呈现的数据并不真实,请读者谅解,并将注意力集中到界面的设计上。

在Model文件夹中创建一个新的Swift文件,将其命名为SnackBarModel。将文件修改为下面这样。

当SnackBar数据模型创建好以后,在Data文件夹中新建一个Swift文件,将其命名为SnackBarData。然后将“项目资源/Data”文件夹里面同名文件的内容复制到该文件之中。

准备好了基础数据以后,在View文件夹中新建一个SwiftUI文件,将其命名为SnackBarCardView。在Properties部分添加一个属性,并在Preview中做出相应修改。

接下来,修改Body部分的代码如下。

目前卡片视图中只有一个Image,利用overlay修饰器,我们在Image的上方创建了一个浮动视图。该视图的核心还是一个Image书签图标,白色,具有阴影效果。

因为overlay修饰器中的视图是居中对齐的,而我们需要将其定位到图片的右上角,所以这里使用了一个“讨巧”的方式。在Image的外层先嵌套一个VStack容器,再在容器中通过Spacer将Image“挤”到顶部。然后在VStack容器的外层嵌套一个HStack容器,并通过另一个Spacer将Image“挤”到尾部。最后,我们通过两个padding修饰器将图标定位到距尾部20点,距顶部22点,效果如图2-31所示。

图2-31 overlay修饰器中利用HStack容器和VStack容器将Image定位到右上角

在Image的下方,继续添加一个VStack容器,代码如下。

在Image的下方,我们添加一个VStack容器,容器中目前实现了Title和Headline两部分视图。另外,我们为卡片视图添加了background、cornerRadius和shadow修饰器,将卡片的背景设置为白色、12点的圆角并指定颜色的阴影。

接下来是小吃店的评星视图,在View文件夹中新建一个SwiftUI文件,将其命名为SnackBarRatingView。修改其代码如下。

在评星视图中,需要接收一个SnackBar类型的对象,然后在Body部分通过ForEach循环生成指定数量的黄色星星。

在预览窗口中的效果如图2-32所示。

图2-32 小吃店评星视图的预览效果

接下来,在View文件夹中新建一个SwiftUI文件,并将其命名为SnackBarInfoView。修改其代码如下。

与SnackBarRatingView类似,这里使用3个HStack容器显示小吃店可用餐人数、小吃准备时长和小吃热度的信息。

在预览窗口中的效果如图2-33所示。

图2-33 小吃店服务信息视图的预览效果

回到SnackBarCardView,在//Rating//Info两行注释语句的下面分别添加两行代码。

此时,预览窗口中的效果如图2-34所示。

图2-34 特色小吃店卡片视图的预览效果

最后回到FoodView,在Properties部分添加一个属性。

接下来,在特色小吃的下面添加如下代码。

这里通过ForEach循环遍历了snackBars数组,并生成了所有的小吃店卡片视图,效果如图2-35所示。

图2-35 FoodView中特色小吃店的预览效果

2.5.7 创建小吃店详细页面视图

本节我们将创建小吃店的详细页面视图,依照之前的惯例,我们还是在View文件夹中新建一个SwiftUI类型文件,将其命名为SnackBarDetailView。因为在详细页面中要体现出标题、评星、服务信息、美食介绍和制作方法5部分内容,所以我们先为其搭建好一个基础框架,修改代码如下。

对于SnackBarDetailView结构体,我们首先在Properties部分添加一个SnackBar类型的属性,该属性用于呈现指定的小吃店信息。

然后在Body部分使用纵向滚动视图,里面是一个VStack容器,在VStack容器中添加了一个Image,用于显示小吃店的特色小吃图片。接下来是一个Group容器,设置它的左右间隔距离为24点,上下间隔距离为12点,这里之所以使用Group是为了避免在一个容器内部使用过多的视图导致编译器编译失败。在一般情况下,建议在一个容器中最多使用不超过10个视图,如果多于10个就在容器内部再添加一个容器,比如Group。

当前,预览窗口中的效果如图2-36所示。

图2-36 SnackBarDetailView的预览效果

接下来,我们修改//标题、//评星//服务信息部分的代码如下。

针对标题,我们使用了ColorBrownAdaptive颜色集,这样标题就可以在不同模式中呈现不同的颜色。对于评星和服务信息,我们调用了之前的两个视图,这也就解释了之前我们要单独为评星和服务信息创建视图的原因。目前在预览窗口中的效果如图2-37所示。

图2-37 SnackBarDetailView的预览效果

接下来,让我们继续添加//美食介绍和//制作方法的相关代码。

在美食介绍部分,Text使用了之前在FoodView中的自定义修饰器TitleModifier。然后通过ForEach循环遍历了snackBar中的method数组,它是一个字符串数组,因为不具备id属性,所以在ForEach中需要设置id参数为\.self。每一次循环都会生成一个VStack容器,而该容器中会包含Text+Divider。

在制作方法部分,我们对Text进行了同样的修饰器设置。在ForEach循环中的VStack容器里面,则是Image+Text。

现在,在预览窗口中的呈现效果如图2-38所示。

图2-38 SnackBarDetailView的预览效果

对于详细页面视图,我们还需要为其添加一个返回按钮,该按钮位于顶部图片的右上角,如图2-39所示。

图2-39 为SnackBarDetailView添加返回按钮

在Body部分为ScrollView添加overlay修饰器,代码如下。

在overlay修饰器中,我们沿用之前的方法,先通过HStack容器将按钮挤到右侧,再通过VStack容器将按钮挤到顶部。对于按钮来说,它的外观则是一个Image图标,白色并带有阴影效果。

接下来,我们为这个按钮添加两个动画效果,当小吃店的详细页面视图出现在屏幕上时,调整按钮的透明度和尺寸。

首先,在Properties部分添加一个新的属性。

然后为ScrollView添加onAppear修饰器,将pulsate的值修改为true。

最后,修改Button的Label参数,为Image添加3个修饰器。

当pulsate的值变为true的时候,Image的透明度会从0.6变成1,尺寸会从原来的0.8倍以中心扩展的方式变大到1.2倍。而这一切的效果则是以1.5s为周期,循环往复一直发生下去。

2.5.8 使用Sheet修饰器呈现新的视图

在本节,我们将使用Sheet修饰器在现有的视图中呈现一个新的视图窗口,从而实现用户在FoodView中单击某一个特色小吃店卡片后,呈现该小吃店的详细页面视图的效果。

为了增强用户的交互体验,我们先在SnackBarCardView中添加触控反馈效果。在SnackBarCardView的Properties部分添加一个属性,该属性用于设置触控反馈的振动力度。

除了触控反馈,我们还添加了被@State封装的showModal变量,该变量用于决定是否开启一个新的视图。

对于顶层的VStack容器,我们为其添加了onTapGesture修饰器,当用户单击卡片的时候,首先会发生振动,然后将showModal变量的值修改为true。

而sheet修饰器会侦测showModal变量的值,一旦该值变为true,就会从屏幕底部滑出SnackBarDetailView。注意,这里一定要在变量showModal前使用$符号,代表引用传递参数值形式。这样,只有在Sheet修饰器内部showModal的值变为false的时候,SnackBarCardView才能关闭滑出的SnackBarDetailView。

现在,让我们在项目导航中打开FoodView,在预览窗口中启动Live模式,单击某个特色小吃店,此时SnackBarDetailView会从屏幕底部滑出。目前还无法通过详细页面右上角的按钮关闭滑出的视图,只能通过点住SnackBarDetailView的顶部区域,向下拖曳该视图,效果如图2-40所示。

图2-40 在FoodView中单击特色小吃店卡片调出详细页面视图

接下来,让我们回到SnackBarDetailView,为其添加关闭视图页面的功能。在Properties部分添加一个新的属性。

然后在Button的//Action部分添加下面的代码。

这里的@Environment相当于调用系统的全局变量,一旦某个视图通过sheet修饰器被调出,相关信息就会被“打入”环境变量presentationMode里。通过@Environment封装器我们可以在视图中获取到该变量的值,然后在用户单击按钮的时候,也就是需要关闭滑出视图的时候,调用其dismiss()方法,就可以将其关闭了。注意,这里必须使用wrappedValue,否则无法实现关闭功能。

至此,我们已经完成了所有FoodView的全部代码,你可以在模拟器中运行该项目,测试所有的功能。