1. 程式人生 > Android開發 >在 SwiftUI 中構建服務端驅動的 UI 元件

在 SwiftUI 中構建服務端驅動的 UI 元件

在 SwiftUI 中構建服務端驅動的 UI 元件

在不提交給 Apple 稽核的情況下即時修改應用

本文將討論使用可重用 UIComponents 元件來實現服務端驅動的 UI,以及如何建立通用垂直列表檢視。最後將簡要討論如何使用 UI 元件實現不同的需求。

什麼是服務端驅動的 UI ?

  • 它是一種架構,其中約定應用程式中 UI 檢視在螢幕上的渲染是由伺服器決定的。
  • 應用程式和伺服器之間存在協議。該協議的基礎是讓伺服器可以控制應用程式的 UI。

協議是什麼?—— 伺服器定義的元件列表。對於伺服器上定義的每個元件,我們在應用程式(UIComponent)中都有一個相應的 UI 實現。比如像 Hotstar 這樣的娛樂應用,其協議定義如下。左邊是伺服器中的元件,右邊是相應的 UI 元件。

執行 —— 螢幕上沒有像 storyboard 一樣預定義的佈局。取而代之的是一個普通的列表檢視,它會根據伺服器的響應,在垂直方向上渲染多個不同的檢視。為了實現這一點,我們必須建立獨立並且在整個應用中可重用的檢視。我們將這些可重用的檢視稱為 UIComponent

協議 —— 對於每個服務端的元件,我們有與之對應的 UIComponent。

SwiftUI

SwiftUI 是一個用宣告式程式設計來設計屏幕布局的 UI 框架。

struct NotificationView: View {
    
    let notificationMessage: String
    
    var body: some View {
        Text(notificationMessage)
    }
}
複製程式碼

在 SwiftUI 中實現服務端驅動的 UI

它分為三個步驟。

  1. 定義獨立的 UIComponents。
  2. 根據 API 響應結果構建 UIComponents。
  3. 在螢幕上渲染 UIComponents。

1. 定義獨立的 UIComponents

輸入:首先,要使 UIComponent 能夠渲染,應為其提供資料。

輸出:UIComponent 中定義的 UI。當螢幕渲染時,它根據提供的資料(輸入)進行渲染。

UIComponent 實現

protocol UIComponent {
    var uniqueId: String  { get }
    func render() -> AnyView
}
複製程式碼
  • 所有 UI 檢視都必須遵守 UIComponent 協議。
  • 由於元件是在通用垂直列表中渲染的,所以每個 UIComponent 必須有一個獨立的標識。uniqueId 屬性用於實現標識的功能。
  • 我們在 render() 方法中定義元件的 UI。呼叫這個方法時會在螢幕上渲染元件。現在我們來看一下 NotificationComponent 的實現。
struct NotificationComponent: UIComponent {
    var uniqueId: String
    
    // The data required for rendering is passed as a dependency
    let uiModel: NotificationUIModel
    
    // Defines the View for the Component
    func render() -> AnyView {
        NotificationView(uiModel: uiModel).toAny()
    }
}

// Contains the properties required for rendering the Notification View
struct NotificationUIModel {
    let header: String
    let message: String
    let actionText: String
}

// Notification view takes the NotificationUIModel as a dependency
struct NotificationView: View {
    let uiModel: NotificationUIModel
    var body: some View {
        VStack {
            Text(uiModel.header)
            Text(uiModel.message)
            Button(action: {}) {
                Text(uiModel.actionText)
            }
        }
    }
}
複製程式碼
  • NotificationUIModel 是元件渲染所需的資料。這是 UIComponent 的輸入。
  • NotificationView 是一個 SwiftUI 檢視,用於定義元件的 UI。它以 NotificationUIModel 作為依賴。當螢幕渲染時,此檢視是 UIComponent 的輸出。

2. 根據 API 響應結果構建 UIComponents

 class HomePageController: ObservableObject {
 
    let repository: Repository
    @Published var uiComponents: [UIComponent] = []
  
    ..
    .. 
    
    func loadPage() {
        val response = repository.getHomePageResult()
        response.forEach { serverComponent in
          let uiComponent = parseToUIComponent(serverComponent)
          uiComponents.append(uiComponent)
        }
    }
}

func parseToUIComponent(serverComponent: ServerComponent) -> UIComponent {
  var uiComponent: UIComponent
  
  if serverComponent.type == "NotificationComponent" {
    uiComponent = NotificationComponent(serverComponent.data,serverComponent.id)
  }
  else if serverComponent.type == "GenreListComponent" {
    uiComponent = GenreListComponent(serverComponent.data,serverComponent.id)
  }
  ...
  ...
  return uiComponent
}
複製程式碼
  • HomePageController 從儲存庫載入伺服器元件並將其轉換為 UIComponents。
  • uiComponent 屬性負責儲存 UIComponents 的列表。我們用 @Published 屬性包裝使其轉化為可觀察的物件。其值的任何更改都將釋出到 Observer(View)。這樣可以使 檢視 與應用程式狀態保持同步。

3. 在螢幕上渲染 UIComponents

這是最後一部分。螢幕的唯一職責是渲染 UIComponents。它訂閱了可觀察的 uiComponents。每當 uiComponents 的值更改時,就會通知 HomePage,然後更新其 UI。通用的 ListView 用於展示 UIComponent。

struct HomePageView: View {
    
    @ObservedObject var controller: HomePageViewModel
    
    var body: some View {
    
        ScrollView(.vertical) {
            VStack {
                ForEach(controller.uiComponents,id: \.uniqueId) { uiComponent in
                    uiComponent.render()
                }
            }
        }
        .onAppear(perform: {
            self.controller.loadPage()
        })
        
    }
}
複製程式碼

通用的 VStackVStack 內部所有的 UIComponent 都在垂直方向上展示。因為 UIComponent 是唯一可識別的,所以我們可以使用 ForEach 進行渲染。

所有遵守 UIComponent 協議的元件都必須返回通用型別,因此 render() 方法返回的型別是 AnyView。以下是 View 的擴充套件,用於將其轉換為 AnyView

extension View {
    func toAny() -> AnyView {
        return AnyView(self)
    }
}

複製程式碼

結論

我們學習瞭如何使用 UIComponent 來使伺服器控制應用程式的 UI。其實 UIComponents 還可以實現更多功能。

現在我們考慮沒有伺服器端驅動時介面的情況。這種情況下,UI 片段在整個應用程式中使用很多次。這會導致檢視和檢視邏輯的重複。因此,最好將介面定義為有意義的,可重用的元件。

這種方式可以讓控制層/業務層定義和構造 UI 元件。另外,業務層也可以承擔控制 UI 的責任。

你可以在 GitHub 上找到這個專案

您還可以閱讀在 Android 中使用 Jetpack Compose 建立基於元件的架構這篇文章,它詳細解釋了 UI 元件原理。文中使用的是 Jetpack compose —— Android 中宣告式的 UI 框架,因此這篇文章的內容也不難理解。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄