使用 UIViewRepresentable 協定 讓你輕鬆建立 SwiftUI TextView


我非常喜歡使用 SwiftUI 框架,但是,與多數的新框架一樣,SwiftUI 也有一個缺點,就是它未能提供所有 UIKit 有的 UI 控件,比如說,你無法在 SwiftUI 找到與文本視圖 (text view) 相對應的控件。幸好,Apple 有一個 UIViewRepresentable 協定,讓你可以輕鬆打包 (wrap) 一個 UIView,並讓 SwiftUI 專案使用。

在本篇文章中,我們會利用 UIViewRepresentable 從 UIKit 打包 UITextView,來創建一個文本視圖。

編者備註:如果你還不熟悉 SwiftUI,可以先閱讀我們的入門教學。另外,iOS 14 將會提供原生 TextEditor 以支援多行文字輸入 。如你正在開發的 App 只在 iOS 14 運行,那就可以直接使用 TextEditor。

使用 UIViewRepresentable

要在 SwiftUI 使用 UIKit 視圖,你可以用 UIViewRepresentable 協定把視圖打包。基本上,你只需要在 SwiftUI 建立一個結構 (struct),使用這個協定來創建和管理 UIView 物件。以下是 UIKit 視圖客製化 Wrapper 的程式碼骨幹:

struct CustomView: UIViewRepresentable {

    func makeUIView(context: Context) -> some UIView {
        // Return the UIView object
    }

    func updateUIView(_ uiView: some UIView, context: Context) {
        // Update the view
    }
}

在實際實作中,你會把一些 UIView 替換為要打包的 UIKit 視圖。比如說,要創建一個 UITextView 的客製化 Wrapper,就可以這樣寫程式碼:

struct TextView: UIViewRepresentable {

    func makeUIView(context: Context) -> UITextView {

        return UITextView()
    }

    func updateUIView(_ uiView: UITextView, context: Context) {

        // Update the view
    }
}

makeUIView 方法中,我們回傳一個 UITextView 的實例 (instance)。如此一來,我們就可以打包 UIKit 視圖,並在 SwiftUI 使用。要使用 TextView,你可以以創建其他 SwiftUI 視圖一樣,如此創建它:

struct ContentView: View {
    var body: some View {
        TextView()
    }
}

在 SwiftUI 創建文本視圖

UIViewRepresentable 有了基本了解後,讓我們來在 SwiftUI 專案中實作客製化文本視圖吧!這個客製化文本視圖可以讓你靈活地更改文本樣式 (text style)。

在 Xcode 中創建了 SwiftUI 專案後,你可以先創建一個名為 TextView 的檔案。要為 UITextView 創建客製化的 Wrapper,你可以如此編寫程式碼:

import SwiftUI

struct TextView: UIViewRepresentable {

    @Binding var text: String
    @Binding var textStyle: UIFont.TextStyle

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()

        textView.font = UIFont.preferredFont(forTextStyle: textStyle)
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true

        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
        uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
    }
}

這段程式碼與前一部分說過的十分相似,但我們更進一步,讓呼叫者 (Caller) 客製文本視圖:

  1. 它接受兩種 Binding:一種用於文本輸入,另一種用於字體樣式 (font style)。
  2. makeUIView 方法中,我們沒有回傳標準的 UITextView,而是使用所選的文本樣式內初始化文本視圖。
  3. 我們添加了一個 Binding 來保存文本輸入。makeUIView 方法負責創建和初始化視圖物件,而 updateUIView 方法則負責更新 UIKit 視圖的狀態。每當 SwiftUI 的狀態有變化時,框架就會自動呼叫 updateUIView 方法,來更新視圖的配置。在這種情況下,當你嘗試在文本視圖中輸入內容時,就會呼叫方法,然後我們會更新 UITextView 的文本。而且,如果呼叫者更改了文本樣式,文本視圖就會重新整理,並更新為新的文本樣式。

現在,讓我們轉到 ContentView.swift。宣告兩個狀態變數來保存文本輸入和文本樣式

@State private var message = ""
@State private var textStyle = UIFont.TextStyle.body

輸入以下程式碼到 body,來顯示文本視圖:

TextView(text: $message, textStyle: $textStyle)
    .padding(.horizontal)

TextView 就像其他 SwiftUI 視圖一樣,你可以應用像 padding 之類的修飾器來調整佈局 (layout)。試試在模擬器中執行 App,你應該能夠在文本視圖中輸入內容。

swiftui-textview-uiviewrepresentable

Capturing the Text Input

在 SwiftUI App 中呈現 UIKit 視圖非常容易。但是,文本視圖尚未完成。現在,你雖然可以在文本視圖中輸入內容,並顯示所輸入的內容,但如果我們試著印出message 變數的值,就會發現變數是空值。這是因為我們尚未將儲存在 UITextView 中的文本,同步到 message 變數中。

UITextView 有一個名為 UITextViewDelegate 的協定,它定義了一組可選方法 (optional methods),用於接收相應 UITextView 物件的更改。舉例來說,只要使用者在文本視圖中輸入內容,就會呼叫以下方法:

optional func textViewDidChange(_ textView: UITextView)

為了追逐文本更改,UITextView 物件應採用 UITextViewDelegate 協定,並實作該方法。

到目前為止,我們只討論了 UIViewRepresentable 協定中的幾種方法。如果你需要在 UIKit 中使用委託 (delegate) 並與 SwiftUI 溝通,就必須實現 makeCoordinator 方法,並提供一個 Coordinator 實例。Coordinator 是 UIView 的委託 和 SwiftUI 之間的橋樑。讓我們看一下程式碼,讓你理解得更清楚吧!

TextView 結構中,創建一個 Coordinator 類別,並如此實作 makeCoordinator 方法:

func makeCoordinator() -> Coordinator {
    Coordinator($text)
}

class Coordinator: NSObject, UITextViewDelegate {
    var text: Binding<String>

    init(_ text: Binding<String>) {
        self.text = text
    }

    func textViewDidChange(_ textView: UITextView) {
        self.text.wrappedValue = textView.text
    }
}

makeCoordinator 方法會回傳一個 Coordinator 的實例。而 Coordinator 就採用 UITextViewDelegate 協定,並實作 textViewDidChange 方法。我們剛剛說過,每次使用者更改搜索文本時,都會呼叫此方法。因此,我們將捕獲更新後的文本,並更新 text Binding 來將其傳遞回 SwiftUI。 

現在我們有了一個採用 UITextViewDelegate 協定的 Coordinator,我們只需要進行多一個更改。在 makeUIView 方法中插入以下程式碼,以將 Coordinator 分配給文本視圖。

textView.delegate = context.coordinator

完成了!這樣我們就成功向 SwiftUI 傳達 UITextView 物件的改變了!

處理文本樣式的更改

在一開始時,我就說過客製化文本視圖可以管理文本樣式的改變。現在,文本樣式是預設的 body。讓我們來添加一個按鈕,讓使用者在兩種文本樣式之間切換吧!

ContentView.swift 中,如此更新 body 屬性:

var body: some View {
    ZStack(alignment: .topTrailing) {
        TextView(text: $message, textStyle: $textStyle)
            .padding(.horizontal)

        Button(action: {
            self.textStyle = (self.textStyle == .body) ? .title1 : .body
        }) {
            Image(systemName: "textformat")
                .imageScale(.large)
                .frame(width: 40, height: 40)
                .foregroundColor(.white)
                .background(Color.purple)
                .clipShape(Circle())

        }
        .padding()    
    }
}

我們在螢幕的右上角添加了一個按鈕,點擊按鈕時,就可以在把文字樣式在 .body.title1 之間作切換。

現在我們可以再測試 App 了。點擊 Size 按鈕,試試切換文本視圖的文字樣式吧!

總結

在這篇教學中,你學會了使用 UIViewRepresentable 協定,來把 UIKit 視圖整合到 SwiftUI 中。SwiftUI 框架還很新,還沒有提供所有基本的 UI 元件,但這種反向相容性 (backward compatibility) 讓你可以利用舊的框架,以及所需要的任何視圖。

你可以在 GitHub 下載完整專案作參考。

如果你想深入了解SwiftUI 這個框架,可以參考我們的 《精通 SwiftUI》電子書。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。
原文:Creating a SwiftUI TextView Using UIViewRepresentable


軟件工程師,AppCoda 創辦人。著有《iOS 13 App 程式設計實力超進化實戰攻略》、《iOS 13 App 程式設計實力超進化實戰攻略》以及《精通 SwiftUI》。曾任職於HSBC, FedEx等公司,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda,致力於iOS程式教學,產品設計及開發。

blog comments powered by Disqus
Shares
Share This