使用 UIViewRepresentable 協定 讓你輕鬆建立 SwiftUI TextView
我非常喜歡使用 SwiftUI 框架,但是,與多數的新框架一樣,SwiftUI 也有一個缺點,就是它未能提供所有 UIKit 有的 UI 控件,比如說,你無法在 SwiftUI 找到與文本視圖 (text view) 相對應的控件。幸好,Apple 有一個 UIViewRepresentable 協定,讓你可以輕鬆打包 (wrap) 一個 UIView,並讓 SwiftUI 專案使用。
在本篇文章中,我們會利用 UIViewRepresentable 從 UIKit 打包 UITextView,來創建一個文本視圖。
使用 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) 客製文本視圖:
- 它接受兩種 Binding:一種用於文本輸入,另一種用於字體樣式 (font style)。
- 在
makeUIView
方法中,我們沒有回傳標準的UITextView
,而是使用所選的文本樣式內初始化文本視圖。 - 我們添加了一個 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,你應該能夠在文本視圖中輸入內容。

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》電子書。
原文:Creating a SwiftUI TextView Using UIViewRepresentable