導言
本文將向大家展示如何利用格式的TextField來過濾用戶非法輸入,同時解決一個TextField的"怪異"行爲。
這是本貓第一篇付費博文,相信不會讓你失望,如果能夠解決到大家的難點、痛點那就更妙了…
So廢話少說,Let’s Go!!!
TextField的格式器有什麼用?
帶格式器的TextField可以過濾用戶的非法輸入,相當於將以下幾個步驟的工作量放到了一起,做了一個封裝:
- 取得用戶輸入
- 解析輸入內容
- 若輸入格式有效則更新綁定
- 若輸入格式非法則產生錯誤,且原綁定內容原封不動
舉個最簡單的例子,假如我們只希望用戶在TextField中輸入數字,那麼除了自己從頭到尾統統一把抓以外,使用Formatter就會是一個更好的選擇了 ;)
大家都用過常規版本的TextField,其實它帶格式的初始化器簽名也很簡單:
TextField(title: StringProtocol, value: Binding<T>, formatter: Formatter)
最後一個參數類型是一個抽象類Formatter,也許大家對Formatter不太熟悉,但是NumberFormatter或DateFormatter相信大家都已耳熟能詳了,後面兩個類都是Formatter的子類。
要使用TextField過濾除數字以外的非法輸入很簡單,你肯定不敢相信:
@State var number = 0
var body: some View {
TextField("just input number ...", value: $number, formatter: NumberFormatter())
}
大工告成!
上面TextField做了這麼幾件事:
- 用NumberFormatter格式解析器解析輸入內容
- 如果輸入合法則用轉換後的數字更新綁定number
- 如果輸入非法則…啥也不做(調試時會有提示!)
現在關鍵的問題是:如何在非法輸入時讓用戶得到提示!?
定製格式化器
首先創建一個數據模型:
class Model: ObservableObject {
@Published var number = 0
let ft = HyFormatter()
編譯掛掉是肯定的啦,因爲我還沒寫HyFormatter類呢!!!
HyFormatter是NumberFormatter的子類,關於如何創建Formatter的子類,超出了本文的內容,有興趣的童鞋可以到蘋果技術官網觀看詳細內容:
簡單的說,爲了能夠讓我們定製的Formatter子類工作,至少要重載(override)兩個方法:
- stringForObjectValue:
- getObjectValue:forString:errorDescription:
前者用來將目的對象轉換成字符串類型,而後者用來將字符串轉換成目的對象:
In the first method you convert the cell’s object to a string
representation; in the second method you convert the string to the
object associated with the cell.
現在新建HyFormatter類:
class HyFormatter: NumberFormatter {
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
// 讓父類爲我們幹活
let success = super.getObjectValue(obj, for: string, errorDescription: error)
if let errorString = error?.pointee as String?{
// 檢測到非法輸入,等待處理...
// (PS:實際產品中需要先判斷是否有錯誤發生,再去訪問obj指針中的內容,
// 否則可能會發生訪問違例!)
}
return success
}
override func string(for obj: Any?) -> String? {
// 同樣讓父類爲我們操心
return super.string(for: obj)
}
}
好了,除了第一個方法簽名比較恐怖以外,其實HyFormatter還是非常簡單的嘛。
這樣我麼的Model就可以編譯成功了!
注意上面在檢測到非法輸入時還不知道怎麼辦纔好…
那麼問題還在啊:該怎麼通知用戶非法輸入呢?
拯救者Combine!
Combine是蘋果在Swift 5.1中加入的一個消息管理框架。使用它我們可以很好的解決很多和消息相關的問題,比如上面那一個。
我們的思路是創建一個發佈者,在用戶輸入非法值時發出消息,消息內容就是錯誤信息。聽起來不太容易?相信我其實也沒什麼難度。
在HyFormatter類中添加以下內容:
private let p = PassthroughSubject<String,Never>()
func errorPublisher() -> AnyPublisher<String,Never> {
p.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
(PS: 其實常規的操作應該是創建一個會產生錯誤的發佈者,即上面的錯誤類型不應該是Never。但SwiftUI的onReceive方法只支持Never的發佈者類型,So…)
你可能有疑惑: 爲什麼要兩個發佈者呢?
這是爲了封裝!只讓外界看到它需要看到的內容,而將潛在信息隱藏起來。無論對於以後的重構、內部實現的更改和架構簡潔性來說都大有好處,我只暗示到這了,否則就跑偏了…
簡單來說,我們將會在HyFormatter內部使用發佈者p發消息,而把errorPublisher方法返回的發佈者提供給調用者訂閱,完美!!!
現在我們知道用戶非法輸入時要幹什麼了-----發消息! 將上面的註釋
// 檢測到非法輸入,等待處理
替換爲如下一行:
p.send(errorString)
errorString就是出錯信息的字符串。
現在發佈者有了,我們還差一個訂閱者。SwiftUI非常貼心的爲我們提供了一個onReceive方法,恰到好處:
@ObservedObject var model = Model()
@State var errorString = ""
@State var showError = false
var body: some View {
VStack {
TextField("Title", value: $model.number, formatter: model.ft)
}
.onReceive(model.ft.errorPublisher()){
self.errorString = $0
self.showError = true
}
.alert(isPresented: $showError){
// 提示用戶非法輸入
Alert(title: Text(self.errorString))
}
}
好了,現在當我們信心滿滿的運行上面代碼吧!!!
很快,你會發現…很尷尬的是,你不會收到任何用戶非法輸入的消息… -_-b
So what’s wrong!!!???
TextField的"怪癖"?
當你用盡Combine所有調試方法(比如print())之後,你會發現: 每次用戶非法輸入後,SwiftUI會重新刷新TextField視圖,這倒沒什麼。關鍵是它幫你把Formatter也順便重建了…
這帶來的直接後果就是,你的發佈者早已不是之前那個發佈者,而你的訂閱者卻還在傻傻的等待原來那個發佈者…苦命的娃啊!
大家可以爲HyFormatter和Model分別添加初始化器init(),在其中加入調試打印代碼。你將會看到:
- Model只會創建一次
- 但HyFormatter會創建許多次
真是頭疼…所以好容易走到這裏,還是要問一句: 現在又咋辦呢?
一個小小的變通: 全局變量
其實解決有很多種辦法,但無外乎將計就計,直接無視HyFormatter無限重建的問題:使用靜態變量!
將HyFormatter中的兩個和發佈器改爲靜態類型:
private static let p = PassthroughSubject<String,Never>()
static func errorPublisher() -> AnyPublisher<String,Never> {
p.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
同時發送消息和訂閱消息的代碼也要做相應改變:
// 發送消息
HyFormatter.p.send(errorString)
// 訂閱消息
.onReceive(HyFormatter.errorPublisher()){...}
現在運行代碼!
正真的大功告成了!!! 😉
結尾
當然這只是最簡單的理想情況,大家可以根據自己App的實際需要擴展代碼。
感謝觀賞,希望本文讓你感到物有所值。
大家有什麼建議和意見可以向我提出來,多多益善,再會啦 😉