Mobile Banking for iOS: Adding 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. 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 Hindu code and copy-paste (see paragraph 1). All this despite the fact that the application is really huge in functionality, at least in the idea (banks want a lot more than they can). Accordingly, in many cases, these are projects for tens of man-years. Those who participated in such projects have probably already realized that the task is not trivial, and school knowledge will not help here.

    What to do?


    It’s immediately clear that this is an architecture task. Before writing the code, I saw a simple and beautiful block architecture of the frontend of our RBS. I have not seen this before in mobile applications, but in the end I made a similar solution, quite effective and scalable, within the framework of the iOS SDK, without adding massive frameworks. I want to share the main points.

    Yes, about all of these SDK frameworks, about Apple schools, the features of the MVC pattern and about quick code in the official documentation are known to everyone who tried to write and maintain serious applications (or at least read Habr ) - I won’t repeat it. There will also be no basics of programming, files with examples and photos with cats.


    The first google picture for “advanced architecture”

    Essence of the decision


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

    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: inscription, input field, picture. Each significant UI block is encapsulated in such an MVC brick. For example, UISegmentedControlencapsulated in SegmentedAtom.

    These MVC bricks are written once. Further, each operation is built from these bricks. Putting a brick is one line. One line! Next, get the value - this is again one line. Everything else is decided by inheritance and polymorphism in subclasses of Atom. The task is to simplify the code of the operations themselves, as they can vary greatly. 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 child ViewController's. For example, an atom for selecting an account from the list (moreover, by filter from the context). The main thing is that all 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 quick form” (from the code I hid all the points related to security, and yes, the rights to the code belong to Digital Technologies of the Future LLC):

    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 on the storyboard. Of course, you can inherit from existing ones.

    onAtomValueChanged()- this is the implementation of the protocol AtomValueSubscriber. We subscribed to the changes in the BIC text field and there we make a request that returns the name of the bank by BIC. The value commit == truefor the text field comes from the event UIControlEventEditingDidEnd.

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

    I do not provide class codeOperationbecause You may find a better solution. I decided (in order to save development time) to feed atomstabular ViewController'y. All the "bricks" are drawn in IB table cells, instantiated by cell id. The table provides automatic height conversion and other amenities. But it turns out that now the placement of atoms is possible only in the table. For an iPhone it’s good, for an 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 UICollectionView.

    Dependencies


    As you know, when entering any form, depending on the values ​​of some fields, other fields can change or hide, as we saw in the example of the BIC above. But there is one field, and in full payment there are many times more, so you need a simple mechanism that will require a minimum of gestures to dynamically show / hide fields. How easy is this to do? Atoms come to the rescue :) as well 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 some choice (for example, pay by meters or an arbitrary amount), the user fills in a different set of fields.

    Here the guys created a “certain framework” for these purposes, but somehow I had enough of a few lines.

    Option 2 , if you lack the predicate language, or you don’t know it, then we consider the dependencies as a function, for example, isDestInnCorrect ():

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

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

            destInnError.showIf("isDestInnCorrect == NO")

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

    Unfortunately, the compiler will not verify for you that this operation implements the method isDestInnCorrect(), although this can probably be done with macros.

    And actually setting the 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 *vars = [self allFieldsValues];
                @try {
                    shown = [predicate evaluateWithObject:self substitutionVariables:vars];
                }
                @catch (NSException *exception) {
                    // тут отладочная инфа
                }
                atom.shown = shown;
            }
        }
    }

    Let's see what other checks will be. Perhaps the operation should not be involved, i.e. everything will be rewritten for the option

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

    One more thing


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

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

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


    But all this is easily used from Swift code. The only thing is that the application does not crash due to an incorrect server response format, all NSObject-fields must be marked as < Optional> and checked for nil. If you want your property so that it does not interfere JSONModel, write them modifier < Ignore>, or situation (readonly).

    conclusions


    The concept of “atoms” really worked and allowed to create a good scalable architecture of the mobile bank. 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 the exhibitor into which many projects are rolling.

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

    And how do you fit your architecture into the iOS SDK on large projects?

    Also popular now: