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

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

「其他文章」