我非常喜歡使用 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