English version also available: TinyCreditCard: Some tricky problems for implement
如果你不熟悉TinyCreditCard的作用和效果,可以去这篇文章查看相关介绍。
基本结构
在开始踩坑之前,先简单说明一下TinyCreditCard里面的基本代码结构。
TinyCreditCardView
是最初始主要视图的类,配合同名的xib文件。TinyCreditCardInputView
是输入框的承载视图类。TinyCreditCardBackView
则是信用卡背面视图的类,同样配合同名xib文件工作。- Font里面就是用到的自定义字体文件,都是free to use的字体。
- Main.storyboard 只是作为初始界面没什么特殊。
- Assets.xcassets里面只放了几张信用卡logo图片和一个按钮图片。
xib的使用
整个TinyCreditCard的结构其实很简单,简单修改后还直接可以放在项目内使用。但作为第三方库,很少会出现xib文件在里面,一方面可能很多第三方开发者喜欢使用纯代码编写视图(到底选择纯代码还是Interface builder,这个问题以后的文章我们再来讨论),另一方面是第三方库一般是View
为基础,不常使用到ViewController
,而Interface builder本来就是以ViewController
为基础而设计的,我们没法直接创建一个Xib去初始化自定义View
。
不过我不是用xib配合View来使用了么?对的。虽然不能直接使用,但可以用稍微复杂的方法实现。首先我们需要考虑我们的Class需要链接Interface Builder的组件和操作,一般我们设置View的Custom Class,但我们要使用xib视图的时候不能这么做,此时我们需要设置xib file’s owner的Custom Class,这样我们就可以把IB中的组件和操作连接到Class中。
然后我们需要在代码中手动加载xib的视图,这个其实很简单,通过UINib
类初始化视图,然后取出第一个(也是唯一一个)View
,添加到Class的view下面,设定好具体的布局。
这样的操作看似比较麻烦,为什么我们还要这么折腾一下呢?我觉得xib里的内容可以很好的说明我的目的:
如你所见整个信用卡视图直接在xib里面就可以看到了,我们不需要通过代码里面复杂的约束猜想效果,也不需要每次运行一下来查看效果,直接预览到效果就是IB最大的好处,而且省下了很多布局代码,让Class代码更为清晰。
卡号效果的实现
卡号作为第一个需要输入的内容,也是最长的一段无意义内容,TinyCreditCard做了两个设计去优化这部分内容。
卡号分隔
第一个就是常见的卡号分隔,很多卡号输入都会做这个优化,因为一长串数字输入确实很容易出错,也难以校对,而且卡号在信用卡上原本就是以每4位分隔的方式印刷。一般来说这个效果其实就是自动为用户输入的内容添加空格,有些人可能会使用UITextFieldDelegate
的func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
来直接控制用户的输入。但在TinyCreditCard里面使用的是检测UITextField
的editingChanged
事件。
其实代理方法可以实现更好的控制,但我们需要同时控制输入有效性和插入删除空格的操作,其实有点复杂化,而editingChanged
事件处理方式实质是在用户每次输入后重新格式化内容,这个操作就直接简单很多了。具体实现代码如下:
let rawText = textField.text?.components(separatedBy: " ").joined() ?? ""
var newText = String(rawText.prefix(16))
let spaceIndex = [12, 8, 4]
for index in spaceIndex {
guard newText.count >= index + 1 else { continue }
newText.insert(" ", at: String.Index(encodedOffset: index))
}
setText(newText)
代码相当直接,首先我们去除去掉空格的rawText
,然后去掉超出16个字符的部分(因为卡号都是16位的)赋值给newText
,然后我们添加空格,就可以把newText
重新赋值到textField
了。
添加空格有一个小窍门,我们有16个数字而我们空格出现在每4位数字之间,所以空格的位置应该是在[4, 8, 12]
。如果我们按照一般的从前往后插入的话,前面插入的空格就会影响后面空格的index位置,因此实际位置会是[4, 9, 14]
,但这组位置对于我们来说并不直观。而在代码里面你可以看到实际我们用了最直观的位置信息来进行插入,但顺序是反过来的,这就是重点,因为从后往前插入前一个操作不会对后一个操作的位置产生副作用,这样就可以增加代码的易读性。
位数提示
这里说的位数提示主要是信用卡视图上面的占位XXXX
形成的提示,这就相当于一个占位符的效果,让未输入的时候效果更佳具体,输入时当前位置也更为明显。这个效果其实实现的方法跟分隔效果可以用同样的方式,每次用XXXX
补全位数然后再显示即可。但这样有个麻烦的地方,我们需要对输入信息再做一层处理,而且XXXX
的颜色跟已输入卡号是不一样的,我们还需要使用NSAttributedString
进行显示。而其实我们有一个更简易的方式。
在TinyCreditCard里我们使用了两个重叠的UILabel
来实现这个效果,下层的UILabel
就是占位XXXX
的部分,直接在xib进行设置,也没有任何处理逻辑。上层的UILabel
就是同步用户输入的卡号内容,也没有复杂的逻辑,唯一要注意的是需要设置跟信用卡底色一直的backgroundColor
用来覆盖下层的占位字符。
虽然比常用的方法多了一个UILabel
,但代码逻辑上简化了很多,后期维护也更为简单。
AutoLayout与Frame设置的选择
相信在现在已经很少人会手动设置对象Frame,因为AutoLayout实在是很方便。但AutoLayout对于特定动画效果的支持是相当的差,如果简单的尺寸变化,那AutoLayout还可以处理,稍微复杂的动画效果就没法子了。但如果所有视图都使用Frame设置,适配各种尺寸的工作量就大很多了,也容易出问题。
因此在TinyCreditCard里面我基本都是使用AutoLayout进行布局,但唯独有一个View使用了Frame设置,就是代码里面的focusArea
,也就是信用卡上当前编辑区域的橙色提示框。因为这个编辑区域需要动态切换到其他位置,这种位置变化使用Frame设置比AutoLayout要合适得多,而且这不会影响到其他视图使用AutoLayout布局。
不过还有一个需要处理的问题,因为原本的输入区域是通过AutoLayout布局的,我们布局focusArea
的时候就需要通过self.focusArea.frame = self.cardNumberButton.frame
这种方式获取实际的布局位置,但如果我们直接使用设个代码我们会得到下面这个结果:
可以看到橙色提示框位置偏了,这是因为AutoLayout的Auto,我们不知道它什么时候才设置到最终的布局,UIKit也没有提供相应的布局刷新事件给我们,而我目前使用的解决方法是通过异步来把操作延后到布局刷新之后,这样我们就会得到正确的布局了。
DispatchQueue.main.async {
self.focusArea.frame = self.cardNumberButton.frame
}
如果你有更好的解决方法欢迎留言分享。
翻转效果的实现
TinyCreditCard使用翻转效果提示用户安全码在卡片背面,但事实上上面这段效果演示里面包含的是两个不同的翻转效果:
操作翻转
因为用户可以直接滑动输入框范围去切换输入框,而就像橙色提示框一样,开篇翻转的效果也应该随用户的操作而显示动态效果。这个并不困难,我们需要用到的就是CATransform3DRotate
去实现。具体代码如下:
var transform = CATransform3DIdentity
transform.m34 = 1.0 / -800
cardFrontView.layer.transform = CATransform3DRotate(transform, -CGFloat.pi * percent, 0, 1, 0)
cardBackView.layer.transform = CATransform3DRotate(transform, -CGFloat.pi * percent - CGFloat.pi, 0, 1, 0)
我们需要同时旋转卡片正面的视图和反面的视图,这里有两个需要注意的地方:
- 翻转卡片的时候,正面是从
0
开始翻转到-pi
,而反面是从-pi
翻转到-2pi
,这样才吻合他们的状态。 - 除了需要控制卡片的翻转角度以外,我们还需要控制卡片的显示,因为我们的视图是二维的,视图层级也是二维的,所以无论怎么翻转都只会显示二维状态下上层的视图,所以我们需要加入以下代码控制显示的切换。
if percent < 0.5 {
// show cardBgView
cardFrontView.isHidden = false
cardBackView.isHidden = true
} else {
// show cardBackView
cardFrontView.isHidden = true
cardBackView.isHidden = false
}
动画翻转
在scrollViewDidScroll(_ scrollView: UIScrollView)
下的操作翻转效果按道理已经可以应对所有情况了,但事实上并不是。当用户通过按钮点击切换到安全码输入界面的时候,我们会调用func scrollRectToVisible(_ rect: CGRect, animated: Bool)
移动到合适的位置,但这个方法并不会持续触发scrollViewDidScroll
,而只会调用一次,因此利用scrollView的contentOffset
控制翻转效果并不适用于这种情况,因此我们还需要一个直接播放的翻转动画。而这个效果其实更容易实现:
cardFrontView.layer.transform = CATransform3DIdentity
cardBackView.layer.transform = CATransform3DIdentity
let transitionOptions: UIView.AnimationOptions = [.transitionFlipFromRight, .showHideTransitionViews]
UIView.transition(with: cardContainerView, duration: 0.5, options: transitionOptions, animations: {
self.cardFrontView.isHidden = true
self.cardBackView.isHidden = false
})
首先我们先把卡片视图的transform
都设成CATransform3DIdentity
,也就是重置视图的transform
属性,确保状态稳定。然后就是一个简单的transition动画,附带两个动画选项,.transitionFlipFromRight
用于控制翻转效果,.showHideTransitionViews
用于控制视图状态。由于我们没有按钮用于从安全码界面返回到前面的输入界面,只能直接滑动输入框,所以这个动画并不需要制作反向的效果。
虽然分开了两种实现,但因为行为是基本一致的,所以并没有什么突兀的效果差异。当然如果能合并成一个动画效果对于后期维护会更加容易,但鉴于两个分开实现都相对简单,强行用一种方法实现可能产生的复杂程度比维护两个简单效果还高,所以对于TinyCreditCard来说分别实现也是一种可取的方式。