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

語言: CN / TW / HK

theme: smartblue

前提回顧

不知不覺已經到了第七章了,在過往每一個章節中,我們都增加和完成項目的一些功能,讓Linkwirld這款產品越來越接近完成品。

一款以用户操作為核心功能的產品,需要包含增刪改查4部分功能,在之前的章節中我們已經完成了查看、新增、刪除的功能,還缺少對於身份卡進行編輯的功能。編輯操作為用户對於之前創建的內容的調整或者更新,在ToDo、Note等應用當中很是常見。

那麼在本章中,我們來完成後編輯相關的操作。

功能分析:新增和編輯頁面的區別

很多時候都會有一個疑惑,現在頁面和編輯頁面到底有沒有區別?開發人員究竟只需要維護一個頁面還是要維護兩個頁面?

一般情況下,由於頁面樣式的相似性,新增和編輯操作在開發的角度上是需要做到共用的。但新增和編輯頁面除了數據綁定外沒有太大的樣式區分時,我們建議可以將樣式或者組件抽離出來,然後再單獨應用。

視圖複用:創建單獨的構件

在新增和編輯頁面中,我們會發現有幾部分視圖內容是可以複用的:titleInputView平台輸入框、platformPicker平台選擇器、indexURLView鏈接地址。如下圖所示:

因此對於這三部分,我們可以將其抽離出來搭建單獨的構件,如此便可以在新增頁面和編輯頁面都可以引用。而且如果需要修改樣式,也只需要修改構件的樣式,新增和編輯頁面的樣式就可以統一變化。

我們創建一個新的文件夾,命名為Artifacts,並且創建3個SwiftUI文件,併為其命名為TitleInputView、PlatformPicker、IndexURLView,如下圖所示:

我們先來到TitleInputView文件,將原來NewView中的titleInputView視圖代碼複製過來,如下代碼所示:

``` import SwiftUI

struct TitleInputView: View { @Binding var title: String

var body: some View {
    TextField("請輸入頭銜", text: $title)
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
        .padding(.horizontal)
        .disableAutocorrection(true)
        .autocapitalization(.none)
}

}

struct TitleInputView_Previews: PreviewProvider { static var previews: some View { TitleInputView(title: .constant("")) } } ```

上述代碼中,除了TextField輸入框相關代碼複製過來,我們還需要完善相關的綁定參數。聲明一個用於雙向綁定輸入框的參數title,並且在TitleInputView預覽時給參數賦予默認值。

緊接着來到PlatformPicker視圖,將原來NewView中的platformPicker視圖代碼複製過來,如下代碼所示:

``` import SwiftUI

struct PlatformPicker: View {

@Binding var platformIcon: String
@Binding var platformName: String

let platforms = [
    ("稀土掘金技術社區", "icon_juejin"),
    ("CSDN博客", "icon_csdn"),
    ("阿里雲社區", "icon_aliyun"),
    ("華為雲社區", "icon_huaweiyun"),
]
var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]

var body: some View {
    ScrollView {
        LazyVGrid(columns: gridItemLayout, spacing: 10) {
            ForEach(0 ..< platforms.count, id: .self) { item in
                if platforms[item].0 == platformName {
                    Image(platforms[item].1)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 48, height: 48)
                        .clipShape(Circle())
                        .overlay(
                            Circle()
                                .stroke(Color.green, lineWidth: 4)
                        )
                } else {
                    Image(platforms[item].1)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 48, height: 48)
                        .clipShape(Circle())
                        .onTapGesture {
                            platformIcon = platforms[item].1
                            platformName = platforms[item].0
                        }
                }
            }
        }
    }
    .padding()
    .background(Color(.systemGray6))
    .cornerRadius(8)
    .padding(.horizontal)
    .frame(maxHeight: 140)
}

}

struct PlatformPicker_Previews: PreviewProvider { static var previews: some View { PlatformPicker(platformIcon: .constant("icon_juejin"), platformName: .constant("稀土掘金技術社區")) } } ```

上述代碼中,我們仍舊講需要聲明雙向綁定的變量,如:platformIcon平台圖標、platformName平台名稱,值得注意的是,原有我們聲明的selectedItem完全可以換成判斷platformName是否等於點擊的名稱,減少一個參數。如下代碼所示:

platforms[item].0 == platformName

聲明雙向綁定變量還需要在預覽PlatformPicker時增加參數的默認值,如下代碼所示:

PlatformPicker(platformIcon: .constant("icon_juejin"), platformName: .constant("稀土掘金技術社區"))

下一步到IndexURLView視圖,將原來NewView中的indexURLView視圖代碼複製過來,如下代碼所示:

``` import SwiftUI

struct IndexURLView: View { @Binding var indexURL:String

var body: some View {
    ZStack(alignment: .topLeading) {
        TextEditor(text: $indexURL)
            .font(.system(size: 17))
            .padding(15)
            .disableAutocorrection(true)
            .autocapitalization(.none)

        if indexURL.isEmpty {
            Text("請輸入主頁鏈接")
                .foregroundColor(Color(UIColor.placeholderText))
                .padding(20)
        }
    }
    .background(Color(.systemGray6))
    .cornerRadius(8)
    .padding()
    .frame(maxHeight: 240)
}

}

struct IndexURLView_Previews: PreviewProvider { static var previews: some View { IndexURLView(indexURL: .constant("")) } } ```

完成之後,我們就可以回到NewView視圖中,將原來的參數以及titleInputView平台輸入框視圖、platformPicker平台選擇器視圖、indexURLView鏈接地址視圖的代碼刪掉,如下圖所示:

刪除代碼後,我們使用單獨搭建的構件來重新搭建樣式,如下代碼所示:

``` TitleInputView(title: $title)

PlatformPicker(platformIcon: $platformIcon, platformName: $platformName)

IndexURLView(indexURL: $indexURL) ```

如此,NewView視圖在維持原有功能樣式不變的情況下,代碼量也精簡很多。

界面設計:創建EditView編輯頁面

完成單獨的構件後,我們就可以來完成編輯頁面的設計。創建一個新的SwiftUI文件,命名為EditView,如下圖所示:

我們先搭建基礎的樣式,示例:頂部導航菜單、頁面標題、關閉按鈕等等,如下圖所示:

``` import SwiftUI

struct EditView: View { @Environment(.presentationMode) var presentationMode

var body: some View {
    NavigationStack {
        Text("Hello, World!")
            .navigationBarTitle("編輯身份卡", displayMode: .inline)
            .navigationBarItems(trailing: closeBtn())
    }
}

// 關閉按鈕
func closeBtn() -> some View {
    Button(action: {
        self.presentationMode.wrappedValue.dismiss()
    }) {
        Image(systemName: "xmark.circle.fill")
            .font(.system(size: 17))
            .foregroundColor(.gray)
    }
}

} ```

上述代碼中,我們基本和NewView頁面的設計一樣,使用NavigationStack搭建頂部導航菜單,並使用navigationBarTitle修飾符設置導航標題,使用navigationBarItems修飾符設置關閉頁面按鈕,並通過聲明全局變量presentationMode和搭建單獨的關閉按鈕closeBtn來實現關閉編輯頁面。

對於編輯保存按鈕,樣式和操作和NewView頁面可能不一樣,我們也單獨構建按鈕,如下代碼所示:

// 編輯更新按鈕 func updateBtn() -> some View { Button(action: { self.presentationMode.wrappedValue.dismiss() }) { Text("確定更新") .font(.system(size: 17)) .foregroundColor(.white) .bold() .padding() .frame(maxWidth: .infinity) .background(Color.blue) .cornerRadius(8) .padding(.horizontal) } }

完成後將編輯按鈕updateBtn添加到EditView編輯頁面視圖中,如下代碼所示:

VStack(spacing: 15) { updateBtn() }

在引用構件之前,我們先來理解下數據關係。

EditView編輯頁面和ContentView主頁的數據關係是,用户在ContentView主頁點擊單張身份卡片,然後打開EditView編輯頁面,並將ContentView主頁身份卡片的數據傳遞到EditView編輯頁面。

我們可以聲明一個符合Model數據模型的參數,然後無論是TitleInputView、PlatformPicker、IndexURLView的參數都綁定Model數據模型的參數,然後該數據模型雙向綁定到ContentView主頁中,就可以將ContentView主頁的數據傳遞過來,如下代碼所示:

@State var model: Model

然後調用TitleInputView、PlatformPicker、IndexURLView構件,並且綁定model模型的值,如下代碼所示:

``` VStack(spacing: 15) { TitleInputView(title: $model.title)

PlatformPicker(platformIcon: $model.platformIcon, platformName: $model.platformName)

IndexURLView(indexURL: $model.indexURL)
updateBtn()
Spacer()

} ```

交互動作:打開EditView編輯頁面

完成後,我們回到ContentView主頁中,我們來實現頁面跳轉的邏輯。首先我們先聲明一個用於頁面跳轉的參數,如下代碼所示:

@State var showEditView: Bool = false

然後在CardView視圖中增加頁面跳轉的方法,這裏也可以使用Sheet修飾符,如下代碼所示:

// 打開編輯彈窗 .sheet(isPresented: self.$showEditView, onDismiss: {self.showEditView = false }) { EditView(model: self.item ?? Model(platformIcon: "", title: "", platformName: "", indexURL: "")) }

上述代碼中,我們使用sheet修飾符用户打開模態彈窗,觸發操作綁定聲明好的變量showEditView,打開的模態彈窗的頁面為EditView編輯頁面。

由於EditView編輯頁面需要傳入參數,這裏我們選擇傳入的對應model的參數為聲明的item,當item不存在時,則默認傳入一個符合Model格式的數據,避免報錯。

對於編輯操作,我們可以給CardView增加多一個指示符,告知用户這個卡片是可以被編輯的,如下代碼所示:

Image(systemName: "ellipsis") .padding() .foregroundColor(.black) .gesture( TapGesture() .onEnded({ self.showEditView.toggle() }) )

上述代碼中,我們增加了一個Image圖片到CardView視圖中,當點擊這個Image圖片時,打開EditView編輯頁面。

考慮到在ContentView我們已經使用NavigationLink頂部導航菜單進行跳轉,那麼如果我們給按鈕增加點擊事件,會存在兩個點擊事件衝突的情況。

我們先註釋掉NavigationLink相關的代碼,如下圖所示:

完成後,我們點擊剛剛創建的編輯按鈕,預覽下效果,如下圖所示:

新增功能:編輯並更新內容

完成樣式後,我們來實現下編輯更新的邏輯,來到ViewModel視圖模型中,我們創建一個編輯並更新的方法,如下圖所示:

// 編輯更新數據項 func editItem(item: Model) { if let id = models.firstIndex(where: { $0.id == item.id }) { models[id] = item } }

編輯操作很簡單,首先要找到點擊編輯的數據項的ID在整個models數據集中的位置,更新後將更新內容賦予當前的ID,便實現了編輯更新操作。

回到EditView視圖中,先引入ViewModel視圖模型,如下代碼所示:

var viewModel: ViewModel

然後在EditView視圖預覽時,給聲明的viewModel賦予默認值,如下代碼所示:

EditView(model: Model(platformIcon: "", title: "", platformName: "", indexURL: ""), viewModel: ViewModel())

由於我們在EditView視圖聲明瞭變量viewModel,因此在使用EditView視圖的頁面也要進行參數綁定。

我們來到ContentView文件,在CardView視圖中,需要補充viewModel相關的參數,如下圖所示:

綁定完成後,我們再回到EditView視圖,在點擊updateBtn更新按鈕時,調用ViewModel視圖模型中的editItem方法,如下代碼所示:

self.viewModel.editItem(item: model)

如此,我們便實現了編輯更新的方法。

交互動作:打開HonePageView頁面

完成打卡編輯彈窗,並實現編輯更新操作後,我們來補充完善下打開HonePageView頁面的交互。在之前的章節我們使用NavigationLink導航菜單跳轉方式實現了頁面跳轉,由於會和我們點擊編輯操作相沖突,因此我們註釋了這部分代碼。

這裏我們再介紹一種彈窗,fullScreenCover全屏覆蓋彈窗,用fullScreenCover也可以實現頁面跳轉的效果。

首先先聲明打開彈窗的變量,我們在CardView視圖中聲明變量,如下代碼所示:

@State var showHomePageView: Bool = false

然後實現調用fullScreenCover全屏覆蓋彈窗的方法,如下代碼所示:

//打開身份卡主頁 .fullScreenCover(isPresented: $showHomePageView, content: { HomePageView(platformName: platformName, indexURL: indexURL) .edgesIgnoringSafeArea(.all) })

上述代碼中,我們給CardView的主要內容增加了fullScreenCover修飾符,用於打開全屏覆蓋彈窗。

彈窗打開觸發動作綁定聲明好的參數showHomePageView,目標地址選擇HomePageView,並且綁定相關的參數,我們希望這個彈窗全屏展示,增加了edgesIgnoringSafeArea忽略安全區域修飾符。

打開彈窗的操作,我們希望點擊身份卡片時打開,但又不能和編輯按鈕相沖突。

我們可以將平台圖標、平台稱號、平台名稱部分的樣式再使用一個容器包裹起來,點擊這個容器打開HomePageView頁面,這樣就可以做到不和編輯按鈕相沖突了。如下圖所示:

最後我們增加點擊事件在這個容器中,當點擊時打開fullScreenCover彈窗,如下代碼所示:

.gesture( TapGesture() .onEnded({ self.showHomePageView.toggle() }) )

由於fullScreenCover彈窗是由下往上打開,因此我們可以換一個返回按鈕的樣式,讓操作看起來流暢些,如下圖所示:

項目預覽

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

5f0fb62c-ad1e-4125-8946-1166e4e6d2e4.gif

項目小結

在本章中,我們實現了EditView頁面的界面設計、頁面跳轉、編輯更新方法,單一個編輯操作就很不容易,哭泣。

但最重要的是學習了結構化編程方法,將頁面的元素分塊,然後抽離創建單獨的構件,如此不管在新增頁面還是在編輯頁面,我們都只需要維護一套代碼,這大大減輕了代碼量,也使得編程更加優雅。

那麼對於一個本地項目來説,linkworld已經完成了查看、新增、編輯、刪除操作,似乎可以告一段落了。但是,如果一款產品要成功上線AppStore,那麼僅僅在模擬器中使用這些功能是遠遠不夠的,我們還需要進行本地化存儲等功能的開發等等工作。

那麼在下一章中,我們將介紹如果實現本地存儲及遠端數據存儲相關的內容,請保持期待吧~

版權聲明

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

「其他文章」