TinyCreditCard: Some tricky problems for implement

Galvin Li
9 min readMar 24, 2019

--

此文章同时提供中文版本:TinyCreditCard:实现代码踩过的坑

If you are not familiar with the role and effect of TinyCreditCard, you can go to this article to see the relevant introduction.

TinyCreditCard: A clear and animated credit card input workflow implement

Basic structure

Before we start to go through the tricky problems, let’s briefly explain the basic code structure in TinyCreditCard.

  • TinyCreditCardView is the class of the initial primary view, works with the xib file of the same name.
  • TinyCreditCardInputView is the view class of the input box.
  • TinyCreditCardBackView is the class of the back view of the credit card, works with the xib file of the same name.
  • Font folder contain all free to use fonts.
  • Main.storyboard is nothing special as an initial interface.
  • There are only a few credit card logo images and a button image in Assets.xcassets.

Xib file work with View

The structure of the entire TinyCreditCard is actually very simple, and can be directly used in your project after simple modification. However, as a third-party library, there are very few library would contain xib files in it. On the one hand, many third-party developers may like to write views in pure code (Should we choose pure code or Interface builder, I will talk about that in future article). On the other hand third-party library is generally based on View, but not ViewController. The Interface builder is originally designed based on ViewController. We can't directly create a Xib to initialize the custom View.

But am I not using xib with View to use it? Yes. Although not directly usable, it can be implemented in a slightly more complicated way. First of all, we need to consider the components and operations that our Class needs to link to Interface Builder. Generally we set the Custom Class of the View, but we can’t do this when we use the xib view. In this case, we need to set the Custom Class of the xib file’s owner, so that we can connect components and operations in IB to the Class.

Then we need to manually load the view of the xib in the code. This is actually very simple, initialize the view through the UINib class, and then take the first (and only) View, add it to the view of the Class with specific Layout.

This kind of operation seems to be more troublesome. Why do I make it in this way? I think the content in xib can explain my purpose very well:

As you can see, the entire credit card view can be seen directly in the xib. We don’t need to guess the effect through the complicated constraints in the code, and we don’t need to run the view every time to see the effect. Direct preview to the effect is the biggest benefit of IB, and save a lot of layout code to make the class code clearer.

Implementation of card number design

The card number is the first content that user needs to be input, and it is also the longest piece of meaningless content. TinyCreditCard has made two designs to optimize this part.

Card number separation

The first one is the common card number separation. Many card number inputs will do this optimization, because a long string of digital inputs is really easy to make mistakes, and it is difficult to proofread. And the card number is originally printed on the credit card in a manner separated by 4 bits. In general, this effect is to automatically add spaces to the user input, some people may use UITextFieldDelegate func textField (_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool to directly control the user input. But in TinyCreditCard I used editingChanged event of UITextField to detect.

In fact, the proxy method can achieve better control, but we need to control the input validity and insert/delete space at the same time, which is actually a bit complicated. And editingChanged event processing method is essentially to reformat the content every time after user input. This operation is a lot simpler. The specific implementation code is as follows:

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)

The code is quite straightforward. First we remove the rawText that removes the space, then remove the part that exceeds 16 characters (because the card number is 16 bits) and assign it to newText. Then we add a space then we can set newText to textField.

There is a small trick to adding spaces. We have 16 numbers and our spaces appear between every 4 digits, so the position of the space should be at [4, 8, 12]. If we insert it as usual from the beginning, the previously inserted space will affect the index position of the trailing space, so the actual position will be [4, 9, 14]. But this set of positions is not intuitive for us. In the code you can see that we actually use the most intuitive position information to insert, but the order is reversed. This is the key point because inserting the previous operation from the back to the front will not produce the side effect of position change. And can increase the legibility of the code.

Remind input position

The input position mentioned here is mainly the prompt formed by the placeholder XXXX on the credit card view, which is equivalent to the effect of a placeholder, so that the current position is more obvious when inputting. This effect can be achieved in the same way as the separation effect, each time using XXXX to complete the number of digits and then display. But there is a trouble here, we need to do another logic layer to handle the input information, and the color of XXXX is not the same as the input card number, we also need to use NSAttributedString to display. In fact, we have an easier way.

In TinyCreditCard we use two overlapping UILabel to achieve this effect, the lower layer of UILabel is the part of the placeholder XXXX, set directly in the xib, there is no processing logic. The upper layer of UILabel is the synchronization content of the card number input by the user, and there is no complicated logic. The only thing to note is that you need to set the background color of the credit card background to cover the placeholder character in lower layer.

Although there is one more UILabel than the usual method, the code is logically simplified a lot, and the post-maintenance is much simpler.

AutoLayout or Frame?

I believe that few people will manually set the object frame now, because AutoLayout is very convenient. However, AutoLayout’s support for specific animation effects is quite poor. If just size changes animation, AutoLayout can still handle it. But a slightly complicated animation effect can’t be done. But if all the views manually set by frame, the workload for adapting to various sizes is much larger and more prone to problems.

Therefore, in TinyCreditCard I basically use AutoLayout for layout, but only one View uses the Frame setting, which is the focusArea in the code, which is the orange prompt box of the current editing area on the credit card. Because this editing area needs to be dynamically switched to different locations with different size, this positional change using frame settings is much more appropriate than AutoLayout, and this does not affect other views using AutoLayout layouts.

However, there is still a problem that needs to be solved, because the original input area is laid out through AutoLayout. When we layout focusArea, we need to get the actual layout by self.focusArea.frame = self.cardNumberButton.frame. But if we use a code directly we will get the following result:

You can see that the orange prompt box is in incorrect position. This is because AutoLayout is automatic, we don’t know when it is set to the final layout and UIKit does not provide the corresponding layout refresh event to us, and the solution I am currently using is asynchronously, the operation is postponed until the layout is refreshed, then we get the correct layout.

DispatchQueue.main.async {
self.focusArea.frame = self.cardNumberButton.frame
}

If you have a better solution, please let me know.

Implementation of flipping effect

TinyCreditCard uses the flip effect to prompt the user security code on the back of the card, but in fact the above effect demo contains two different flip effects:

Flip by user’s drag

Because the user can directly scroll on the input box to switch, just like the orange prompt box, the effect of the flip should also show the dynamic effect with the user’s operation. This is not difficult, we need to use CATransform3DRotate to achieve. The specific code is as follows:

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)

We need to rotate both the front and back views of the card at the same time. Here are two things to note:

  1. When flipping the card, the front side is flipped from 0 to -pi, and the reverse side is flipped from -pi to -2pi, to match their state.
  2. In addition to the need to control the flip angle of the card, we also need to control the display of the card. Because our view level is two-dimensional, the view level is also two-dimensional, so no matter how flipped, only the view of the upper layer in the two-dimensional state will be displayed. So we need to add the following code to control the display of the switch.
if percent < 0.5 {
// show cardBgView
cardFrontView.isHidden = false
cardBackView.isHidden = true
} else {
// show cardBackView
cardFrontView.isHidden = true
cardBackView.isHidden = false
}

Flip by user’s tap

The flip effect of the operation under scrollViewDidScroll(_ scrollView: UIScrollView) should be able to handle all situations, but it is not. When the user tap button to switch to the security code input interface, we call func scrollRectToVisible(_ rect: CGRect, animated: Bool) to move to the appropriate location, but this method does not continuously trigger scrollViewDidScroll, It will only be called once. So using the scrollView's contentOffset to control the flipping effect is not suitable for this situation, so we also need a flip animation that plays directly. And this effect is actually easier to achieve:

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
})

First, we set the transform of the card view to CATransform3DIdentity, which is to reset the view's transform attribute to ensure stable state. Then there is a simple transition animation with two animation options, .transitionFlipFromRight for controlling the flip effect, and .showHideTransitionViews for controlling the view state. Since we don't have a button to return from the security code interface to the previous input interface but only scroll the input box directly, so this animation does not need to make a reverse effect.

Although the two implementations are separated, but the behavior is basically the same, there is no obvious difference in effect. Of course, if you can merge them into one animation effect, it will be easier for later maintenance. However, since the two separate implementations are relatively simple, forcing a method to achieve a level of complexity is higher than maintaining two simple effects, so for TinyCreditCard Implementing separately is also a desirable approach.

  • All code in this article can be found in the GitHub project.
  • If you have questions or suggestions, welcome to leave a comment.
  • If you feel this article is valuable, 👏 can make more people can see it.
  • If you like this type of content, welcome to follow my Medium and Twitter, I will keep posting useful content for everyone.

--

--

Galvin Li

A Tiny iOS developer who love to solve problems and make things better.