我报名参加金石计划1期应战——分割10万奖池,这是我的第2篇文章,点击检查活动概况。

swiftui 中最常用的构建布局的视图当属:VStack、HStack、ZStack 莫属。

swiftui 开发之旅:三大 Stack 构建布局

凭借这 3 个 Stack,咱们能构建出,上下,左右,等宽,叠放,或者是类似于前端的 Flex 布局;下面让咱们来看看这 3 个利器的具体运用。

VStack

VStack 用于垂直摆放视图。

struct Test: View {
  var body: some View {
    VStack{
      Text("高档会员").padding(5).font(.headline).foregroundColor(Color(hex: "#8E6A30"))
      Divider()
      Text("无限制账户").padding(.vertical, 5)
      Text("币种切换,主动核算汇率").padding(.vertical, 5)
      Text("价格主动更新").padding(.vertical, 5)
      Text("云端数据同步").padding(.vertical, 5)
      Text("专属布景皮肤").padding(.vertical, 5)
    }
    .foregroundColor(Color(hex: "#8E6A30"))
  }
}

在上面的代码中,咱们将文本在垂直线上摆放,同时给 VStack 添加了 foregroundColor 修饰符,这会使 VStack 视图内的文本运用同一种色彩。

swiftui 开发之旅:三大 Stack 构建布局

对齐方向

VStack 还支撑对内部视图进行对齐操控。

VStack 支撑三种对齐方法,默许运用居中对齐:

  • .leading:左对齐
  • .trailing:右对齐
  • .center:居中对齐

需求留意的是,VStack 设置的对齐方向是水平方向

struct Test: View {
  var body: some View {
    VStack(alignment: .leading){
      Text("高档会员").padding(5).font(.headline).foregroundColor(Color(hex: "#8E6A30"))
      Divider()
      Text("无限制账户").padding(.vertical, 5)
      Text("币种切换,主动核算汇率").padding(.vertical, 5)
      Text("价格主动更新").padding(.vertical, 5)
      Text("云端数据同步").padding(.vertical, 5)
      Text("专属布景皮肤").padding(.vertical, 5)
    }
    .foregroundColor(Color(hex: "#8E6A30"))
  }
}

swiftui 开发之旅:三大 Stack 构建布局

设置距离

在上面例子中,咱们对每个文本设置了垂直方向的 padding,也就是上下边距分别为 5 来使文本不至于紧挨在一起。但这种设置方法太多余繁琐,咱们能够凭借 VStack 的第二个属性 spacing 来设置内部子视图的上下距离。

struct Test: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 15){
      Text("高档会员").padding(5).font(.headline).foregroundColor(Color(hex: "#8E6A30"))
      Divider()
      Text("无限制账户")
      Text("币种切换,主动核算汇率")
      Text("价格主动更新")
      Text("云端数据同步")
      Text("专属布景皮肤")
    }
    .foregroundColor(Color(hex: "#8E6A30"))
  }
}

swiftui 开发之旅:三大 Stack 构建布局

留意:这儿 spacing 的值为 15,作用近似于 padding(.vertical, 5)。

HStack

HStack 用于将内部子视图摆放在水平方向上。

import SwiftUI
struct Test: View {
  var body: some View {
    HStack{
      Text("高档会员").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
      Divider()
      Text("无限制账户")
      Text("币种切换,主动核算汇率")
      Text("价格主动更新")
      Text("云端数据同步")
      Text("专属布景皮肤")
    }
    .foregroundColor(Color(hex: "#8E6A30"))
  }
}
struct Test_Previews: PreviewProvider {
  static var previews: some View {
    Test()
  }
}

swiftui 开发之旅:三大 Stack 构建布局

HStack 同样能够设置内部子视图的对齐方法和距离,但其设置的对齐方向和距离和 VStack 相反,是针对垂直方向的。

HStack 的 spacing 运用方法和 VStack 的相同,这儿不再赘述。

HStack 支撑五种对齐方法,默许运用居中对齐:

  • .top:左顶部对齐
  • .bottom:底部对齐
  • .center:居中对齐
  • .firstTextBaseline:根据第一行文本的基线对齐
  • .lastTextBaseline:根据最终一行文本的基线对齐

swiftui 开发之旅:三大 Stack 构建布局

来看一个具体示例:

import SwiftUI
struct Test: View {
    var body: some View {
        VStack {
            HStack(alignment: .top){
                Text("顶部对齐").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
                Divider()
                Text("无限制账户")
                Text("币种切换,主动核算汇率")
                Text("价格主动更新")
                    .padding(.vertical, 5)
                Text("云端数据同步")
                    .padding(.vertical, 5)
                Text("专属布景皮肤")
            }
            .foregroundColor(Color(hex: "#8E6A30"))
            HStack(alignment: .center){
                Text("居中对齐").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
                Divider()
                Text("无限制账户")
                Text("币种切换,主动核算汇率")
                Text("价格主动更新")
                    .padding(.vertical, 5)
                Text("云端数据同步")
                    .padding(.vertical, 5)
                Text("专属布景皮肤")
            }
            .foregroundColor(Color(hex: "#8E6A30"))
            HStack(alignment: .bottom){
                Text("底部对齐").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
                Divider()
                Text("无限制账户")
                Text("币种切换,主动核算汇率")
                Text("价格主动更新")
                    .padding(.vertical, 5)
                Text("云端数据同步")
                    .padding(.vertical, 5)
                Text("专属布景皮肤")
            }
            .foregroundColor(Color(hex: "#8E6A30"))
            HStack(alignment: .firstTextBaseline){
                Text("第一行文本基线对齐").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
                Divider()
                Text("无限制账户")
                Text("币种切换,主动核算汇率")
                Text("价格主动更新")
                    .padding(.vertical, 5)
                Text("云端数据同步")
                    .padding(.vertical, 5)
                Text("专属布景皮肤")
            }
            .foregroundColor(Color(hex: "#8E6A30"))
            HStack(alignment: .lastTextBaseline){
                Text("最终一行文本基线对齐").font(.headline).foregroundColor(Color(hex: "#8E6A30"))
                Divider()
                Text("无限制账户")
                Text("币种切换,主动核算汇率")
                Text("价格主动更新")
                    .padding(.vertical, 5)
                Text("云端数据同步")
                    .padding(.vertical, 5)
                Text("专属布景皮肤")
            }
            .foregroundColor(Color(hex: "#8E6A30"))
        }
    }
}
struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

swiftui 开发之旅:三大 Stack 构建布局

ZStack

ZStack 主要用于将内部子视图在 Z 轴上摆放,其特点是对于内部的接连子视图,都会分配一个比前一个子视图优先级更高的 Z 轴值,也就是,越在后边出现的子视图,越会显现在“前”。

import SwiftUI
struct Test: View {
    let colors: [Color] =
        [.red, .orange, .yellow, .green, .blue, .purple]
    var body: some View {
        ZStack {
            ForEach(0..<colors.count) {
                Rectangle()
                    .fill(colors[$0])
                    .frame(width: 100, height: 100)
                    .offset(x: CGFloat($0) * 10.0,
                            y: CGFloat($0) * 10.0)
            }
        }
    }
}
struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

swiftui 开发之旅:三大 Stack 构建布局

再看一个示例:

import SwiftUI
struct Test: View {
    let colors: [Color] =
        [.red, .orange, .yellow, .green, .blue, .purple]
    var body: some View {
          ZStack {
              Image(systemName: "gamecontroller.fill")
                  .resizable()
                  .aspectRatio(contentMode: .fit)
              HStack {
                  VStack(alignment: .leading) {
                      Text("Game Play")
                          .font(.headline)
                      Text("Go! Go! Go!")
                          .font(.subheadline)
                  }
                  Spacer()
              }
              .padding()
              .foregroundColor(.primary)
              .background(Color.primary
              .colorInvert()
              .opacity(0.75))
          }.background(Color.gray)
      }
}
struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

swiftui 开发之旅:三大 Stack 构建布局

微信读书会员页面仿写

介绍完 3 个布局神器,下面让咱们来做一个复杂点的示例。

下图是微信读书会员卡页面,让咱们用 3个 Stack 来模仿这个页面。

swiftui 开发之旅:三大 Stack 构建布局

  1. 先设置基本布局

会员卡页面的3个主要的布局内容包含:顶部导航、中心的翻滚内容区域、底部固定区域,咱们先把这3个区域的布局设置好。

import SwiftUI
struct Test: View {
  var body: some View {
    NavigationView {
            ZStack {
                Color(red: 39/255, green: 46/255, blue: 71/255).edgesIgnoringSafeArea(.all)
                ScrollView(showsIndicators: false) {
                }
            }
            .safeAreaInset(edge: .bottom) {
            }
            .navigationBarTitleDisplayMode(.inline)
        }
  }
}

NavigationView 用于设置导航栏;ZStack 用于设置底层的布景色,其他视图将会在布景色之上显现;safeAreaInset 用于设置底部固定区域。

  1. 主要修饰符

在开发的过程中咱们会用到大量的 swiftui 修饰符,修饰符的运用次序是会影响布局的结果的哦,下面是用到的一些主要修饰符:

  • .padding: 边距。
  • foregroundColor:设置视图色彩,比如文本、图标的色彩。
  • font:设置文字大小。
  • .frame:设置视图宽高。
  • .background:设置视图布景色。
  • .cornerRadius:设置圆角。
  • .safeAreaInset:设置一个安全区域,也就是固定区域。
  • .bold:设置文字字重,需求 ios16。
  • .navigationBarTitleDisplayMode:设置导航栏形式
  • .toolbar:自定义导航栏内容。
  • .navigationBarItems:设置导航栏左右两头按钮。
  • .overlay:这儿用于设置圆角边框。
  1. 完好代码示例

直接上完好代码,详细内容请看代码内注释。

import SwiftUI
struct Test: View {
    var body: some View {
        NavigationView {
            ZStack {
                // 设置页面布景色
                Color(red: 39/255, green: 46/255, blue: 71/255).edgesIgnoringSafeArea(.all)
                ScrollView(showsIndicators: false)  {
                    VStack(alignment: .leading) {
                        VStack(alignment: .leading) {
                            Text("付费会员卡")
                                .bold()
                                .padding(.bottom, 4)
                                .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                        }
                        // 填充中心空白区域,使文字上下靠边
                        Spacer()
                        HStack(alignment: .bottom) {
                            Text("3").bold().padding(.bottom, -4).font(.system(size: 24))
                            Text("天9月27日到期").font(.system(size: 10))
                            // 空白区域填充,使文字居左
                            Spacer(minLength: 0)
                        }.foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                    }
                    .padding()
                    .frame(height: 180)
                    // 会员卡布景色突变
                    .background(RadialGradient(
                        gradient: Gradient(
                            colors: [
                                Color(red: 56/255, green: 81/255, blue: 116/255),
                                Color(red: 39/255, green: 46/255, blue: 71/255),
                                Color(red: 231/255, green: 200/255, blue: 153/255),
                                Color(red: 39/255, green: 46/255, blue: 71/255),
                            ]
                        ),
                        center: .center,
                        startRadius: 2,
                        endRadius: 650)
                    )
                    // 圆角边框
                    .overlay(
                        RoundedRectangle(cornerRadius: 12, style: .continuous)
                            .stroke(Color(red: 231/255, green: 200/255, blue: 153/255), lineWidth: 1)
                    ).padding(.bottom, 10)
                    VStack(alignment: .leading) {
                        HStack(alignment: .top, spacing: 0) {
                            VStack(alignment: .leading, spacing: 20) {
                                HStack() {
                                    Image(systemName: "infinity")
                                    Text("付费会员卡").bold()
                                }.padding(.bottom, -5)
                                Divider().overlay(Color.gray)
                                Text("全场出版书畅读")
                                Text("全场有声书畅听")
                                Text("书架无上限")
                                Text("离线下载无上限")
                                Text("时长可兑换体会卡和书币")
                                Text("专属阅读布景和字体")
                            }
                            .padding()
                            .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                            Spacer()
                            VStack(alignment: .leading, spacing: 20) {
                                HStack() {
                                    Image(systemName: "infinity")
                                    Text("体会卡")
                                }.padding(.bottom, -5)
                                Divider().overlay(Color.gray)
                                Text("部分出版书畅读")
                                Text("仅可收盘 AI 朗读")
                                Text("书架 500 本上限")
                                Text("每月可下载 3 本")
                                Text("仅可兑换体会卡")
                                Text("-")
                            }
                            .padding()
                            .foregroundColor(Color.gray)
                        }.font(.system(size: 12))
                    }
                    .background(Color(red: 47/255, green: 54/255, blue: 77/255))
                    .cornerRadius(12)
                }.padding([.top, .leading, .trailing])
            }
            // 设置一个底部固定区域,然后自定义其内部子视图
            .safeAreaInset(edge: .bottom) {
                VStack() {
                    VStack() {
                        HStack {
                            VStack(alignment: .leading) {
                                Text("接连包月 19.00").bold().padding(.bottom, 6)
                                Text("19元/月-主动续费可随时取消").font(.system(size: 10))
                            }
                            Spacer()
                            Text("立即开通")
                                .font(.system(size: 14))
                                .bold()
                                .padding(EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 20))
                                .foregroundColor(Color(hex: "#6F5021"))
                                .background(Color(hex: "#EACEA6"))
                                .cornerRadius(16)
                        }.foregroundColor(Color(hex: "#6F5021"))
                    }
                    .padding()
                    // 布景线性突变,从左到右
                    .background(LinearGradient(gradient: Gradient(colors: [Color(hex: "#E7C899"), Color(hex: "#F9E9CF")]), startPoint: .leading, endPoint: .trailing))
                    .cornerRadius(12)
                    .padding(.bottom, 10)
                    VStack(alignment: .leading) {
                        HStack {
                            VStack(alignment: .leading) {
                                Text("购买年卡").padding(.bottom, 1)
                                HStack {
                                    Text("228.00").font(.headline)
                                    Text("(19元/月)").font(.subheadline)
                                }
                            }.frame(maxWidth: .infinity).font(.system(size: 14)) .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255)).padding().background(Color(red: 52/255, green: 58/255, blue: 78/255)).cornerRadius(12)
                            HStack {
                                Image(systemName: "gift").font(.system(size: 20))
                                VStack(alignment: .leading) {
                                    Text("赠送年卡给好友").padding(.bottom, 1)
                                    VStack(alignment: .leading) {
                                        Text("228.00").font(.headline)
                                    }
                                }
                            }.frame(maxWidth: .infinity).font(.system(size: 14)) .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255)).padding().background(Color(red: 52/255, green: 58/255, blue: 78/255)).cornerRadius(12)
                        }
                        HStack {
                            VStack(alignment: .leading) {
                                Text("购买季卡").padding(.bottom, 1)
                                HStack {
                                    Text("60.00").font(.headline)
                                    Text("(20元/月)").font(.subheadline)
                                }
                            }.frame(maxWidth: .infinity).font(.system(size: 14)) .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255)).padding().background(Color(red: 52/255, green: 58/255, blue: 78/255)).cornerRadius(12)
                            VStack(alignment: .leading) {
                                Text("购买月卡").padding(.bottom, 1)
                                VStack(alignment: .leading) {
                                    Text("30.00").font(.headline)
                                }
                            }.frame(maxWidth: .infinity).font(.system(size: 14)) .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255)).padding().background(Color(red: 52/255, green: 58/255, blue: 78/255)).cornerRadius(12)
                        }
                        Text("确认购买后,将向您的 iTunes 账户收款。购买接连包月项目,将主动续订,iTunes 账户会在到期前 24 小时内扣费。在此之前,您能够在系统[设置] -> [iTunes Store 与 App Store] -> [Apple ID] 里面进行退订。").font(.system(size: 10)).foregroundColor(Color.gray).padding(.top, 10)
                    }
                }
                .padding()
                .background(
                    Color(red: 41/255, green: 50/255, blue: 75/255)
                        )
            }
            // 设置导航栏为行内形式
            .navigationBarTitleDisplayMode(.inline)
            // 自定义导航栏标题内容
            .toolbar {
                ToolbarItem(placement: .principal) {
                    VStack {
                        Text("会员卡").font(.headline).padding(.bottom, 2)
                        Text("已运用 517 天累计节省 839.76 元").font(.system(size: 12))
                    }.foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                }
            }
            // 自定义导航栏左右两头的按钮
            .navigationBarItems(
                leading: Button(action: {
                    // 点击按钮时的操作
                }, label: {
                    Image(systemName: "chevron.left")
                        .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                }), trailing: Button(action: {
                    // 点击按钮时的操作
                }, label: {
                    Text("明细")
                        .foregroundColor(Color(red: 231/255, green: 200/255, blue: 153/255))
                }))
        }
    }
}
struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

swiftui 开发之旅:三大 Stack 构建布局

总结

咱们学习了 VStack、HStack、ZStack 3个构建布局的重要东西,运用它们及其其他的视图和修饰符,完成了一个微信读书会员卡页面,这个页面包含了许多常见的布局方法和 swiftui 开发中常用的修饰符和控件,相信经过本文的学习,你现已掌握了不少技巧。大家还用 swiftui 开发出了哪些美观的页面呢,欢迎在评论区晒出你的作品。

这是 swiftui 开发之旅专栏的文章,是 swiftui 开发学习的经验总结及实用技巧共享,欢迎重视该专栏,会坚持输出。同时欢迎重视我的个人大众号 @JSHub:提供最新的开发信息速报,优质的技术干货引荐。

点赞:如果有收成和帮助,请点个赞支撑一下!

保藏:欢迎保藏文章,随时检查!

评论:欢迎评论交流学习,共同进步!