swiftui 開發之旅:三大 Stack 構建佈局

語言: CN / TW / HK

theme: cyanosis

我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第2篇文章,點選檢視活動詳情

swiftui 中最常用的構建佈局的檢視當屬:VStack、HStack、ZStack 莫屬。

image.png

藉助這 3 個 Stack,我們能構建出,上下,左右,等寬,疊放,或者是類似於前端的 Flex 佈局;下面讓我們來看看這 3 個利器的具體使用。

VStack

VStack 用於垂直排列檢視。

swift 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 檢視內的文字使用同一種顏色。

image.png

對齊方向

VStack 還支援對內部檢視進行對齊控制。

VStack 支援三種對齊方式,預設使用居中對齊: - .leading:左對齊 - .trailing:右對齊 - .center:居中對齊

需要注意的是,VStack 設定的對齊方向是水平方向

swift 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"))     } }

image.png

設定間距

在上面例子中,我們對每個文字設定了垂直方向的 padding,也就是上下邊距分別為 5 來使文字不至於緊挨在一起。但這種設定方式太多餘繁瑣,我們可以藉助 VStack 的第二個屬性 spacing 來設定內部子檢視的上下間距。

swift 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"))     } }

image.png

注意:這裡 spacing 的值為 15,效果近似於 padding(.vertical, 5)。

HStack

HStack 用於將內部子檢視排列在水平方向上。

```swift 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()     } } ```

image.png

HStack 同樣能夠設定內部子檢視的對齊方式和間距,但其設定的對齊方向和間距和 VStack 相反,是針對垂直方向的。

HStack 的 spacing 使用方式和 VStack 的一樣,這裡不再贅述。

HStack 支援五種對齊方式,預設使用居中對齊: - .top:左頂部對齊 - .bottom:底部對齊 - .center:居中對齊 - .firstTextBaseline:基於第一行文字的基線對齊 - .lastTextBaseline:基於最後一行文字的基線對齊

image.png

來看一個具體示例:

```swift 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() } } ```

image.png

ZStack

ZStack 主要用於將內部子檢視在 Z 軸上排列,其特點是對於內部的連續子檢視,都會分配一個比前一個子檢視優先順序更高的 Z 軸值,也就是,越在後面出現的子檢視,越會顯示在“前”。

```swift 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() } } ```

image.png

再看一個示例:

```swift 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() } } ```

image.png

微信讀書會員頁面仿寫

介紹完 3 個佈局神器,下面讓我們來做一個複雜點的示例。

下圖是微信讀書會員卡頁面,讓我們用 3個 Stack 來仿照這個頁面。

2571663297324_.pic.jpg

  1. 先設定基本佈局

會員卡頁面的3個主要的佈局內容包括:頂部導航、中間的滾動內容區域、底部固定區域,我們先把這3個區域的佈局設定好。

```swift 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:這裡用於設定圓角邊框。

  • 完整程式碼示例

直接上完整程式碼,詳細內容請看程式碼內註釋。

```swift 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() } } ```

image.png

總結

我們學習了 VStack、HStack、ZStack 3個構建佈局的重要工具,使用它們及其其他的檢視和修飾符,完成了一個微信讀書會員卡頁面,這個頁面包含了很多常見的佈局方式和 swiftui 開發中常用的修飾符和控制元件,相信通過本文的學習,你已經掌握了不少技巧。大家還用 swiftui 開發出了哪些好看的頁面呢,歡迎在評論區晒出你的作品。

這是 swiftui 開發之旅專欄的文章,是 swiftui 開發學習的經驗總結及實用技巧分享,歡迎關注該專欄,會堅持輸出。同時歡迎關注我的個人公眾號 @JSHub:提供最新的開發資訊速報,優質的技術乾貨推薦。

👍點贊:如果有收穫和幫助,請點個贊支援一下!

🌟收藏:歡迎收藏文章,隨時檢視!

💬評論:歡迎評論交流學習,共同進步!