To direct all the energy of the system in the desired direction, it is necessary to limit this system by the rules.Hi, Habr! We continue a
series of articles on the architectural design of mobile applications. Under the cut, let's talk about designing UI layers. Welcome!
Evolving, various types of living organisms faced with elementary tasks that needed to be solved to ensure their own survival.
')
Need to absorb energy from the external environment? - solution: photosynthesis.
Need to provide diversity in genes? - solution: separation by gender.
The more complex the organism became, the more high-level tasks it had to solve.
As time went. Species appeared in which the priority of survival issues was significantly reduced, and the resources of an individual were partially unused.
Evolution adopted a new vector. Most of the natural problems, be it food, protection from predators, weather protection and so on, were somehow solved and there was no place to develop further, it seemed like there was no place, but then fantasy played its part.
Further evolution did not pursue the goals of ensuring survival, and consciousness became the source of new tasks, giving rise to development vectors such as “culture” and “science”. In fact, education and upbringing are not directly factors that ensure the survival of an individual, it is quite possible to do without them while living in the forest.
Education and upbringing are not a natural goal, set by nature, they were created by man himself.
Further development of the species is ensured by the goals that this species invents for itself.And each of these goals is a limitation.
You can not talk loudly in the library. You can not slurp at the table. You need to train a lot: to write, draw, print, play the guitar - without any difficulty, and this is violence against yourself.
In order to develop, man himself puts himself in the framework, which he himself invents.
Want your software system to be at its peak?
Do you want your product to develop dynamically?
- set the project rigid frames and extension vectors.
The programming culture is that the engineer writes not in the way he wants, but in order to comply with the rules.
PsAnd yes, any rule - be it a three-layered architectural design or culture to hold cutlery - is invented by people, and therefore is not an objective in the last resort.
Nothing is true.
Everything is allowed.
What are we talking about?
Species separationIn the previous
article , devoted mainly to the architectural design of the service and transport layers of mobile applications (hereinafter referred to as MT), a hint was given about what preconditions should be guided during the design of the UI level.
Theoretical reasoning and practical conclusions will follow, which may make it possible to simplify your work, introducing a share of structuredness in the projects being developed.
NB Please note that the ideology of building a user interface in high-quality products is strictly tied to the application platform, and trying to achieve some kind of “cross-platform” when choosing solutions is, frankly, a silly exercise.
You can't just take it - and equate the systems originally designed to be different, even if Navigation Drawer can perform the same functions as the UITabBar.
This article mainly focuses on solutions applicable to the iOS platform.
Build UI
We continue to tell wonderful stories.Last time, we ended up with so-called
user stories , which supposedly allow us to divide the project into logical parts so that later its source code can be easily maintained.
Suppose that the design of your application was drawn not by an insane cretin, but by a more or less adequate artist who is familiar with the concept of “UX” and who is able to comfortably and organically enter all the controls in the chains of screens on a mobile device.
Using the example of a banking application, the interface is divided into typical
stories , like: “Welcome”, consisting of several slides representing the application; “Authorization” with login-password-emails and other fields; finally, the main screen consisting of several logical parts: “Home” (about your money), “Payments and transfers”, “Card” with ATMs and branches, and so on.
Each story includes several screens (or "pages") that allow the user to perform certain actions.
“Payments and Transfers”: enter the list of payments; go to the section "Mobile Communications"; select an operator; enter the phone number and the amount of the deposit, choose the source of money - your credit card; to pay.
At the service level, there are all the necessary tools that can provide the UI with the necessary data and leverage on the back-end.
To separate the entities, I prefer not to use classical MVC, but its modification MVP, which allows creating more abstract controllers, and scatter the code associated with the data model into classes that are directly “consumers” of this data.
Three key kinds of entities can be distinguished for each user story. These are
View ,
ViewController and
Helper .
ViewController - classes acting as controllers of separate logical units of the interface; As a rule, the application page is a logical unit, but for a more complex layout, it is useful to highlight on the page child controllers for unloading the parent class.
View - these are classes directly represented on your storyboards: successors of UIView, UITableViewCell, UILabel, UIButton, and so on.
Helper - utility classes that perform individual or delegated duties. These include tools for formatting strings, classes, the following protocols: UITableViewDataSource and UITableViewDelegate, and so on.
Let's talk about the particulars.
Viewcontroller
Learning from competitorsIn general, iOS developers should learn a lot from their colleagues who make Android software.
The latter, to some extent, inherited all the stiffness and architecture that are inherent in classical Java engineers, and therefore the architectural design of really cool Android applications looks much more slender than the architectural design of cool iOS applications.
The first restriction that Android colleagues have introduced is the inability to transfer complex objects between controllers (activity) - for this, it is additionally necessary to provide serialization of these objects.
Yes, now everyone uses fragments, and this whole story has sunk into oblivion, but the initial message was correct: each controller should be as
separate as possible.
Do not pass whole entities between controllers - pass their identifiers. Let each controller itself knock on the service level, receive from there detailed information on this entity (through its identifier) ​​- and thus you will provide your application with a minimum of side effects inherent in imperative programming.
And, of course, the second limitation that should be learned from Android developers is that they use the “Adapter” as the table delegate in the SDK - and this is a separate class, not a protocol, so merging things like the view controller and the UITableViewDelegate / UITableViewDataSource - simply impossible.
NB Open Text. UITableViewController is a direct violation of the principles of SOLID, and the engineer at Apple who invented this class made a serious mistake, due to which now thousands of developers around the world consider it normal to hang a dozen protocols on the view controller, like the UITableViewDataSource and UITableViewDelegate.
View
Abstract and divide by stylesAbove, I have already talked about using MVP, and now I will tell you why.
So, there are two fragments responsible for filling the cell with data:
MVC:
RMREntity * entity = [self entityForIndexPath: indexPath];
cell.title = entity.name;
cell.subtitle = entity.shortDescription;
MVP:
RMREntity * entity = [self entityForIndexPath: indexPath];
[cell fillWithEntity: entity];
The second approach allows you to abstract from the structure of the UI, concentrating all the logic of controllers and helper'ov around the processed data type - RMREntity.
Thus, behind the –fillWithEntity interface: you can hide a dozen inheritance classes of RMRTableViewCell, each of which will be able to draw an entity in its own way. At the same time, for each UITableView will use
the same UITableViewDataSource, allowing significant savings in writing the boilerplate code.
Total
First : do not forget: the view can be inherited not only from the SDK classes, but also from each other.
“The architectural design of the application should shout about what kind of application. If you see the drawings of the library, you know for sure that this is a library: there are reading rooms and shelves with books in it! ”Do you have a Message data type in your application? Make an abstract cell MessageCell under it - and inherit other cells from it! Each class of your code must perform its function deterministically, and not allow logic, which should not be in it.
The second . Remember, I mentioned a designer who is familiar with "UX"?
A good designer is a bit like a good programmer.
He has a set of typical patterns that can be adapted to certain needs.
A good designer will first work out the color palette, and then it will be consistently applied to draw the project.
In addition to colors, a good designer will always have a “stylesheet” for each project:
Title: Helvetica Bold, 36pt.
Subtitle: Helvetica Medium, 24pt.
Amount of Money: Helvetica Light, 18pt, Light Blue.
And so on.
To you, as an adequate developer, nothing prevents you from
directly transferring this style sheet to your code.
For example, you can create an abstract Label class, which during any initialization will poll the factory –fontStyle method that returns the font style — and apply this style.
The heirs, in turn, will simply return the necessary writing style. Thus, you will have classes at hand: HeaderLabel, SubheaderLabel, MoneyAmountLabel ... and, lo and behold! You can simply substitute them in the “Class” field right inside Interface Builder, applicable to the created layout.
Helper
Tested designIt has already been said about those things that controllers should not do.
So who should? - Answer: utility.
The most important thing to remember and to follow is a simple rule: utilitarian classes should not carry any information. They should not have public properties, and all their logic should be “transparent” with a minimum of void-methods, as if we are writing in a functional style: the method should always return something.
Due to this “lack of state”, the utilities are safe enough to use on different layers of application logic, but you shouldn’t be lazy to create separate helpers for controllers, views, business logic, and so on.
Utilitarian classes — or heplers — are the first candidates for the role of entities under test.
Formatting dates, sorting arrays, creating images from other images are all algorithms that easily fall into the sequence of “arrange, act, assert”, and can be easily sacrificed to automatic tests.
Conclusion
ResultsIn this article, I did not want to give anyone
instructions .
Each has its own head on its shoulders, and each is able to draw its own conclusions. Or do not draw conclusions at all.
Of course, it would be possible to write more specifics: what the project structure should look like, where to put the files and directories, where the resources should be.
In what sequence to implement methods in classes, how to name properties.
However, I deliberately did not do this: the material is already intentionally simple, because the source code
should not be complicated .
Stay with us.