SwiftUI中帶格式(Formatter)TextField如何捕獲非法輸入

導言

本文將向大家展示如何利用格式的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做了這麼幾件事:

  1. 用NumberFormatter格式解析器解析輸入內容
  2. 如果輸入合法則用轉換後的數字更新綁定number
  3. 如果輸入非法則…啥也不做(調試時會有提示!)

現在關鍵的問題是:如何在非法輸入時讓用戶得到提示!?

在這裏插入圖片描述

定製格式化器

首先創建一個數據模型:

class Model: ObservableObject {
    @Published var number = 0
    let ft = HyFormatter()

編譯掛掉是肯定的啦,因爲我還沒寫HyFormatter類呢!!!

HyFormatter是NumberFormatter的子類,關於如何創建Formatter的子類,超出了本文的內容,有興趣的童鞋可以到蘋果技術官網觀看詳細內容:

Creating a Custom 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的實際需要擴展代碼。

感謝觀賞,希望本文讓你感到物有所值。

大家有什麼建議和意見可以向我提出來,多多益善,再會啦 😉

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章