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 here!!!???

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的实际需要扩展代码。

感谢观赏,希望本文让你感到物有所值。

大家有什么建议和意见可以向我提出来,多多益善,再会啦 😉

在这里插入图片描述

大熊猫侯佩 CSDN认证博客专家 Swift Objective-C Xcode
非自由程序员,CSDN博客认证专家。
CSDN汇编板块版主, CSDN其他开发语言大版版主。

对App、以及Cocos2D、SpriteKit游戏开饶有兴趣。目前常用的语言是ObjC、Swift、Ruby等。不过看到编程艺术、ASM、逆向和C时依然欲罢不能。虽然不是,但喜欢黑客的思维和哲学,认为社会工程学很酷,但还没有实际用来撩过妹。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付 39.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值