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

語言: 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天后未獲授權禁止轉載,侵權必究!

「其他文章」