📜 ⬆️ ⬇️

Mobile bank for iOS: add block architecture to Cocoa MVC

If you are writing a mobile banking application for iOS, what are your priorities? I think there are two of them:

  1. Reliability;
  2. The rate of change.

The situation is such that you need to be able to make changes (and in particular roll out new banking products) really quickly. But at the same time, do not slip into the index and copy-paste (see point 1). All this despite the fact that the application is really great in functionality, at least in the idea (banks want a lot more than they can). Accordingly, in many cases these are projects for dozens of person-years. Those who participated in such projects probably already understood that the task is not trivial, and school knowledge will not help here.

What to do?


It is immediately clear that this is a task of architecture. Before writing the code, I saw the simple and beautiful block architecture of the front-end of our remote banking system. I have not seen anything like this in mobile applications before, but in the end I made a similar solution, quite effective and scalable, within the iOS SDK, without adding massive frameworks. I want to share the highlights.
')
Yes, about all these SDK frameworks, about the Apple shoal features of the MVC pattern and about the bydlock code in the official documentation, everyone who tried to write and maintain serious applications knows (or at least read Habr ) - I will not repeat. There will also be no basics of programming, files with examples and photos with cats.


The first guglokartinka on request "advanced architecture"

The essence of the decision


Operations consist of atoms.
An operation is an interface for performing a specific action in the ABS. For example, transfer between your accounts.

An atom is a stable “capsule” of MVC, a kind of brick from which you can build houses of any size and shape. Each brick has its own essence in the UI: an inscription, an input field, a picture. Each significant UI block is encapsulated into such an MVC brick. For example, UISegmentedControl encapsulated in SegmentedAtom .

These MVC building blocks are written once. Then each operation is built from these bricks. Putting a brick is one line. One line! Further to receive value is again one line. All the rest is solved by inheritance and polymorphism in subclasses of Atom. The task is to simplify as much as possible the code of the operations themselves, since they can change a lot. Bricks do not fundamentally change (or even do not change at all ).

An atom can be a more complex element. It can encapsulate some logic and ViewController ’s children. For example, an atom to select an account from the list (and by filter from the context). The main thing is that all the complexity remains inside the atom. Outside, it remains just as easy to use.


My concept is already used in construction

For example, the operation of sending a payment order "in a quick form" (from the code hid all the moments related to security, and yes, the rights to the code belong to LLC Digital Technologies of the Future):

 class PayPPQuickOperation : Operation, AtomValueSubscriber { private let dataSource = PayPPDataSource() private var srcTitle, destBicInfo: TextAtom! private var destName, destInn, destKpp, destAccount, destBic, destDesc, amount: TextInputAtom! private var btnSend: ButtonAtom! override init() { super.init() create() initUI() } func create() { srcAccount = AccountPickerAtom(title: "  ") destName = TextInputAtom(caption: "    ") destInn = TextInputAtom(caption: " ", type: .DigitsOnly) destKpp = TextInputAtom(caption: " ", type: .DigitsOnly) destAccount = TextInputAtom(caption: " ", type: .DigitsOnly) destBic = TextInputAtom(caption: "", type: .DigitsOnly) destBicInfo = TextAtom(caption: " ,    ") destDesc = TextInputAtom(caption: " ") amount = TextInputAtom(caption: ", â‚˝", type: .Number) btnSend = ButtonAtom(caption: "") //      : atoms = [ srcAccount, destName, destInn, destKpp, destAccount, destBic, destBicInfo, destDesc, amount, btnSend, ] destInn!.optional = true destKpp!.optional = true btnSend.controlDelegate = self //    onButtonTap  destBic.subscribeToChanges(self) } func initUI() { destName.wideInput = true destAccount.wideInput = true destDesc.wideInput = true destBicInfo.fontSize = COMMENT_FONT_SIZE destName.capitalizeSentences = true destDesc.capitalizeSentences = true } func onAtomValueChanged(sender: OperationAtom!, commit: Bool) { if (sender == destBic) && commit { dataSource.queryBicInfo(sender.stringValue, success: { bicInfo in self.destBicInfo.caption = bicInfo?.data.name }, failure: { error in //  ,      self.destBicInfo.caption = "" }) } } func onButtonTap(sender: AnyObject?) { //   ,    sender    var hasError = false for atom in atoms { if atom.needsAttention { atom.errorView = true hasError = true } } if !hasError { var params: [String : AnyObject] = [ "operation" : "pp", "from" : srcAccount.account, "name" : destName.stringValue, "kpp" : destKpp.stringValue, "inn" : destInn.stringValue, ... "amount" : NSNumber(double: amount.doubleValue), ] self.showSignVC(params) } } } 

And this is the whole operation code !!! Nothing more is needed, provided that you have all the atoms you need. In this case, I already had them. If not, write a new one and draw in a storyboard. Of course, you can inherit from existing ones.

onAtomValueChanged() is the implementation of the AtomValueSubscriber protocol. We subscribed to the BIK text field changes and there we make a request that returns the name of the bank on the BIC. The commit == true value for the text field comes from the event UIControlEventEditingDidEnd .

The last line showSignVC() is to show the ViewController for the signing operation, which is just another operation that consists of the same simple atoms (formed from the elements of the security matrix that come from the server).

I do not provide Operation class code, since You can find a better solution. I decided (in order to save development time) to feed the atoms table ViewController . All “bricks” are drawn in IB by cells of tables, instantiated by cell id. The table gives automatic height recalculation and other amenities. But it turns out that now I can only place atoms in the table. For the iPhone it is good, for the iPad it may not be very. So far, only a few banks in the Russian appstore correctly use the space on the tablets, the rest stupidly copy the UI of iPhones and only add the left menu. But ideally, yes, you need to redo it on a UICollectionView .

Dependencies


As you know, when entering any form, depending on the values ​​of some fields, other fields can be modified or hidden, as we saw in the example of the BIC above. But there is one field, and in full payment there are many times more, therefore, a simple mechanism is needed, which will require a minimum of gestures to dynamically show / hide fields. How easy is it to do? Atoms come to the rescue again :) and also NSPredicate .

Option 1 :

  func createDependencies() { // ppType -   , -    (EnumAtom) ppType.atomId = "type" ppType.subscribeToChanges(self) let taxAndCustoms = "($type == 1) || ($type == 2)" ... //    : field106.dependency = taxAndCustoms field107.dependency = taxAndCustoms field108.dependency = taxAndCustoms ... 

This is an example for payments. A similar system works for electronic payments in favor of various providers, where, depending on a certain choice (for example, to pay on counters or an arbitrary amount), the user fills in a different set of fields.

Here, the guys created for these purposes "a certain framework", but I somehow had a few lines.

Option 2 , if you do not have enough predicate language, or you do not know it, then we consider dependencies as a function, for example, isDestInnCorrect ():

  destInn.subscribeToChanges(self) destInnError.dependency = "isDestInnCorrect == NO" 

I think it may be more beautiful if you rename the property:

  destInnError.showIf("isDestInnCorrect == NO") 

The text atom destInnError , in case of incorrect input, tells the user how to correctly fill out the TIN according to the 107th order.

Unfortunately, the compiler does not check for you that this operation implements the isDestInnCorrect() method, although you can probably do this with macros.

And the actual installation of visibility in the base class Operation (sorry for Objective C):

 - (void)recalcDependencies { for (OperationAtom *atom in atoms) { if (atom.dependency) { NSPredicate *predicate = [NSPredicate predicateWithFormat:atom.dependency]; BOOL shown = YES; NSDictionary<NSString*, id> *vars = [self allFieldsValues]; @try { shown = [predicate evaluateWithObject:self substitutionVariables:vars]; } @catch (NSException *exception) { //    } atom.shown = shown; } } } 

Let's see what else will be checking. Perhaps the operation should not be involved, i.e. everything will overwrite the option

 let verifications = Verifications.instance ... if let shown = predicate.evaluateWithObject(verifications, substitutionVariables: vars) { ... 

One more thing


In general, with regard to the object patterns in the client-server interaction, I can not fail to note the exceptional convenience when working with JSON, which is provided by the JSONModel library. Dot notation instead of square brackets for received JSON objects, and immediately typing of fields, incl. arrays and dictionaries. As a result, a strong increase in readability (and as a result of reliability) of the code for large objects.

We take the JSONModel class, inherit ServerReply from it (because each answer contains a basic set of fields), from ServerReply inherit server responses to specific types of requests. The main disadvantage of the library is that it is not on Swift (since it works on some language lifehacks), and for the same reason the syntax is weird ...

Piece of example
 @class OperationData; @protocol OperationOption; #pragma mark OperationsList @interface OperationsList : ServerReply @property (readonly) NSUInteger count; @property OperationData<Optional> *data; ... @end #pragma mark OperationData @interface OperationData : JSONModel @property NSArray<OperationOption, Optional> *options; @end #pragma mark OperationOption @interface OperationOption : JSONModel @property NSString<Optional> *account; @property NSString<Optional> *currency; @property BOOL isowner; @property BOOL disabled; ... 


But all this is easy from Swift-code. The only thing is that the application does not fall due to the wrong format of the server response, all NSObject fields must be marked as < Optional > and checked for nil yourself. If we want our property so that it does not interfere with JSONModel , we write their modifier < Ignore > or, as JSONModel , (readonly) .

findings


The concept of "atoms" really worked and allowed to create a good scalable mobile bank architecture. The operation code is very simple to understand and modify, and its support, while maintaining the original idea, will take O (N) time. And not an exhibitor, in which many projects roll.

The disadvantage of this implementation is that the display is done simply through UITableView , on smartphones it works, and for tablets, in the case of an “advanced” design, you need to redo the View and partially Presenter (just a general solution for smartphones and tablets).

How do you fit your architecture in the iOS SDK on large projects?

Source: https://habr.com/ru/post/318200/


All Articles