實戰編程·刻在男人DNA裏的浪漫,空氣投籃(二)

語言: CN / TW / HK

theme: smartblue

前提回顧

在上一章節中,我們完成了“準備遊戲”頁面和“遊戲列表”頁面,並完成了遊戲列表的簡單交互,在本章中,我們將繼續完成其他的相關內容。

實戰編程

頁面切換

在整個空氣投籃項目中,“準備遊戲”頁面和“遊戲列表”頁面的交互邏輯是,打開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天后未獲授權禁止轉載,侵權必究!

「其他文章」