實戰教程·元宇宙來了,準備好你的電子名片了嗎?(六)

語言: CN / TW / HK

theme: smartblue

前提回顧

在上一章節中,我們學習了Model-View-ViewModel架構模式,並實現了新增、刪除方法。其中刪除方法使用的是最簡單的,利用SwiftUI自帶的contextMenu上下文選單按鈕控制元件,實現長按喚起刪除操作,點選刪除操作呼叫ViewModel檢視模型中的刪除方法刪除指定ID的資料項。

而在卡片式列表應用中,常用的刪除方法是橫向滑動喚起刪除的方法,網上也有很多使用SwiftUI自帶的EditButton喚起橫向刪除的方法,但總有這樣那樣的原因導致自帶的滑動刪除並不好用。

在本章中,我們將學習一種使用gesture手勢修飾符實現滑動刪除的互動。

互動動作:向左滑動喚起刪除操作

在SwiftUI提供的手勢操作中,有onTapGesture點選手勢、LongPressGesture長按手勢、DragGesture拖拽手勢三種主要操作。而要使用這三種手勢,需要使用到gesture手勢修飾符,如下程式碼所示:

``` // 拖拽手勢 .gesture( DragGesture() .onChanged { value in // 拖動時的操作 }

    .onEnded { value in
        // 拖動結束時操作
    }

) ```

使用DragGesture拖拽手勢前,需要使用@GestureState屬性包裝器定義一個拖拽位置引數viewState,用來記錄我們的拖拽前的初始位置CGSize.zero,也用來監聽和更新UI,如下程式碼所示:

@State var viewState = CGSize.zero

因為我們要拖動的是單張身份卡片,因此需要給身份卡片構件CardView新增拖動位置的偏移量修飾符,如下程式碼所示:

//設定只能從右往左拖動 .offset(x: self.viewState.width < 0 ? self.viewState.width : 0)

上述程式碼中,我們給CardView的檢視內容添加了offset偏移量修飾符,設定只能沿X軸拖動,並新增條件如果拖拽前的初始位置viewState小於0,則可被沿X軸橫向拖拽,如果大於等於0,即向右邊拖拽時,則維持位置為0,保證CardView的檢視內容只能從右往左拖動。

緊接著,我們給DragGesture手勢增加更新方法,如下程式碼所示:

``` // 拖拽手勢 .gesture( DragGesture() .onChanged { value in // 拖動時的操作 self.viewState = value.translation }

    .onEnded { value in
        // 拖動結束時操作
        self.viewState = .zero
    }

) ```

上述程式碼中,我們在onChanged拖動時,讓身份卡的位置viewState等於拖動的位置translation,如此便實現了卡片拖動動作。而在onEnded拖動結束時,我們讓身份卡片回到初始的位置zero。

完成之後,我們可以嘗試拖動下卡片,如下圖所示:

為了突出當前正在進行拖拽刪除的操作,我們可以給身份卡片CardView在拖拽時增加樣式,比如在向左拖拽到左邊一定位置的時候,讓卡片填充一個背景顏色。

那麼首先我們先宣告拖動到某個位置是操作刪除的位置,並還需再宣告一個變數告知系統當前是否在執行刪除操作,如下程式碼所示:

@State var valueToBeDeleted: CGFloat = -75 @State var readyToBeDeleted: Bool = false

下一步我們可以在拖動時新增判斷當前拖動位置是否達到準備刪除的位置,如下程式碼所示:

self.readyToBeDeleted = self.viewState.width < self.valueToBeDeleted ? true : false

並且在刪除時,給身份卡片修改背景色,如下程式碼所示:

.background(self.readyToBeDeleted ? Color(.systemRed) : .white)

當然,拖動結束後,還需要更新readyToBeDeleted是否操作更新的狀態為false,如下程式碼所示:

self.readyToBeDeleted = false

樣式完成後,我們來實現刪除邏輯,我們在ViewModel檢視模型中建立了刪除方法deleteItem,deleteItem方法需要基於卡片ID進行指定刪除,而在CardView卡片構件中,並沒有ID,而資料集和其ID是在ContentView檢視中存在。

要想獲得UUID,我們還需要在ViewModel檢視模型中建立一個獲得資料UUID的方法,如下程式碼所示:

// 獲得資料項的UUID func getItemById(itemId: UUID) -> Model? { return models.first(where: { $0.id == itemId }) ?? nil }

上述程式碼中,我們建立了一個獲得資料項的ID的方法,通過傳入點選項的UUID,然後返回對應資料項在資料集中的UUID,告知系統當前操作的資料項是models陣列中的哪一個資料。

然後我們再回到ContentView檢視中,在CardView身份卡片檢視中宣告相關的變數,如下程式碼所示:

var viewModel: ViewModel var itemId: UUID var item: Model? { return viewModel.getItemById(itemId: itemId) }

上述程式碼中,我們使用全域性變數引用ViewModel模型檢視,並且聲明瞭兩個引數itemId和item。itemId為UUID格式,作為資料項的唯一識別符號,item為符合Model模型的資料項,用於通過ID找到Model模型的資料。

由於CardView聲明瞭變數,因此在引用CardView的地方需要繫結相關的引數,如下程式碼所示:

// 卡片檢視 CardView(platformIcon: item.platformIcon, title: item.title, platformName: item.platformName, indexURL: item.indexURL, viewModel: viewModel, itemId: item.id)

下一步,我們就可以拖動結束時,判斷身份卡是否拖動到刪除的位置,如果是,則呼叫ViewModel檢視模型的deleteItem刪除資料項方法,刪除指定ID的資料項,如下程式碼所示:

if self.viewState.width < self.valueToBeDeleted { self.viewModel.deleteItem(itemId: itemId) }

我們嘗試拖拽卡片操作下刪除操作的互動,如下圖所示:

373fdd30-20b0-4106-828a-7ba3683850a3.gif

二次提醒:使用警告彈窗提示使用者

上面我們實現了滑動刪除的操作,但操作太直接了,可能存在使用者誤操作的情況。為了避免使用者誤操作,我們可以增加一層判斷機制。當用戶喚起刪除操作時提醒使用者當前正在執行刪除操作,請求使用者的二次確認,使用者確認後方可執行刪除。

這種強提醒的使用者場景下,我們可以使用警告彈窗告知使用者。警告彈窗和模態彈窗的使用方式型別,需要提前宣告一個是否開啟警告彈窗的變數,如下程式碼所示:

@State var showDeleteAlert: Bool = false

對於刪除使用的警告彈窗,我們可以單獨構建警告彈窗檢視,然後再呼叫,如下程式碼所示:

``` // 刪除彈窗 private var deleteAlert: Alert { let alert = Alert(title: Text(""), message: Text("確定要刪除嗎?"), primaryButton: .destructive(Text("確認")) {

    }, secondaryButton: .cancel(Text("取消")))
    return alert
}

```

上述程式碼中,我們建立了一個警告彈窗deleteAlert,檢視型別為Alert彈窗。在deleteAlert彈窗中,我們設定了Alert的標題、副標題、主要按鈕、取消按鈕,最終返回這個Alert樣式給到deleteAlert。

要使用Alert彈窗的方式也比較簡單,可以使用alert彈窗修飾符,如下程式碼所示:

//開啟刪除確認彈窗 .alert(isPresented: $showDeleteAlert, content: { deleteAlert })

上述程式碼中,我們給整個卡片檢視添加了alert警告彈窗修飾符,並繫結開啟彈窗的引數showDeleteAlert,警告彈窗的內容為我們單獨構建的deleteAlert刪除彈窗檢視。

然後我們可以再拖動判斷刪除的時候觸發開啟刪除彈窗,在刪除彈窗中點選確定時,呼叫刪除方法,如下程式碼所示:

self.showDeleteAlert.toggle()

完成之後,我們再在模擬器中預覽下操作後的效果,如下圖所示:

2c5347a5-10cc-4809-a8e4-e2a0df6e6103.gif

體驗升級:文字說明和震動反饋

實現滑動刪除動作後,總感覺好像少了點東西。操作幾次後發現,當用戶拖動卡片向左滑動時,確實可以通過顏色表示當前使用者正在執行操作,而且確實有警告彈窗進行二次提示,但對於小白使用者來說,也確實在警告彈窗出來之前是不知道正在操作刪除的。

這存在學習成本,我們可以加一點點小細節,當用戶向左滑動卡片時,在卡片背後出現提示文字,如下程式碼所示:

// 提示文字 HStack { Spacer() Text("左滑刪除") .padding() .foregroundColor(Color(.systemGray)) }

上述程式碼中,我們在List列表中遍歷資料項時,在ZStack堆疊檢視包裹中的NavigationLink導航連結、CardView身份卡片中增加了一個“提示文字”檢視,使用Spacer空間墊片將Text文字撐到右邊。

由於ZStack堆疊檢視的層級關係和程式碼的前後順序有關,Text文字在CardView身份卡片前,那麼常規情況下文字會被遮擋。而當CardView身份卡片向左滑動時,就出現了Text文字了。

除了文字外,我們還可以再觸發刪除操作的時候新增震動反饋,進一步提升使用者體驗。 SwiftUI提供了反饋生成器 UIFeedbackGenerator供開發者呼叫iOS系統的線性馬達形成震動效果。我們可以建立一個新的Swift檔案專門管理震動反饋內容。

建立一個新的資料夾,命名為SupportFile,並建立一個新的Swift檔案,命名為Haptics,如下圖所示:

然後我們引入SwiftUI,並建立一個類來管理震動反饋的內容,如下程式碼所示:

``` import Foundation import SwiftUI

struct Haptics { static func hapticSuccess() { let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) }

static func hapticWarning() {
    let generator = UINotificationFeedbackGenerator()
    generator.notificationOccurred(.warning)
}

} ```

上述程式碼中,我們建立了一個結構體Haptics,聲明瞭2個方法hapticSuccess成功時的震動動效、hapticWarning警告時的震動動效,並呼叫UINotificationFeedbackGenerator震動反饋生成器元件,賦予不同的震動反饋效果:success或者warning。

緊接著我們回到ContentView檢視中,在CardView檢視中拖動身份卡片操作時,我們呼叫Haptics中的震動反饋方法,如下程式碼所示:

Haptics.hapticWarning()

如此,在使用者向左滑動身份卡執行刪除操作時,系統就會基於使用者一個震動反饋,告知使用者這是一個“值得謹慎的操作”,進一步地提供使用者的體驗。

很多時候,加一點點小細節,整個應用會上一個臺階。

專案小結

在本章中,我們實現了自定義滑動刪除的互動操作,這比起直接使用List自帶的滑動刪除,或者使用簡單的控制元件實現更加“高階一些”,當然這也增加了一些學習成本。

對於很多時候SwiftUI自帶控制元件無法滿足開發需要時,我們要具備各種自定義實現控制元件或者操作能力,這是區分只會用框架的“搬磚員”和真正的程式設計師的重要特徵。

另外我們還在該專案中增加了“一點點細節”,讓應用的互動性和使用者體驗更好一些,這也是作為一個創作者的追求。其實也想表達一個觀念,如果一個程式設計師只會使用框架而加入自己的思考和理解,那麼25歲和30歲也只是打字快慢的區別罷了。

接下來的章節,我們想要做的還有很多,我也會把每一步的實現細節和操作流程都分享出來,請保持期待吧~

版權宣告

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

「其他文章」