
Social network without a server. History of iOS client and backend development
Intro
I want to talk about the experience of developing an iOS client for a social network and backend implemented using BaaS Parse.com. Below is the architecture that we got, some tips & tricks and thoughts about working with parse.com.
Initially, the client thought about a server on RoR, but apparently they did not dare to invest a lot of money at once. We signed a strict NDA, so I can’t give a link to the Appstore. In the good tradition of all IT books, I want to express my gratitude to customer X and company Y for the fact that I had the opportunity to work on this project and draw on all this experience. Also, thanks A. for writing a part about the module for in-app purchases.
Architecture
Many times I saw and heard about projects that spanned between different developers and at the same time lost the integrity of the idea, so I decided to write a small document that outlines the basic principles by which I built the architecture of the application. I believe that, for the most part, it concentrated all the experience that I got on this project.
Screenshot of the Xcode project structure

Logical layers
Networking
In the application, we work with two services: PubNub and Parse . All interaction with the SDK of these services occurs on this layer.
- Message center
- PubNub SDK
- Parse Services
Parse has its own iOS SDK, but we did not want to get very attached to it, as the client said that they planned to launch their server later. Therefore, we used the Parse RESTfull API, which we interacted through AFNetworking. We transferred all the business logic that could be transferred to the server to the cloud code- it turned out that each request called the server code. In principle, it was possible to compose complex queries by stuffing the parameters into NSDictionary, but after I figured out the Backbone.js that the cloud code is written on, I began to do everything there - it is much more readable and better amenable to change. As a result, it turned out that for each user action, the application sent only one request to the server. Only on login and updating screens with different information were sent more requests.
UI
This layer is the simplest - there are only ViewControllers, Views, Cells and an additional controller that helps in navigation and implements various tricks with showing screens in the most unexpected places. For example, if a user logs in via facebook, then he displays a screen with the fields that the user should fill in if they are not in his facebook account. Also here are controllers that respond to push notification. We needed to make drag-n-drop for UICollectionView - as a result, we used a ready-made implementation: github.com/lxcid/LXReorderableCollectionViewFlowLayout . I had to play a little prank, but, in general, you can use the code.
I can also recommend MNMPullToRefreshif you need pull to refresh control for UITableView.
Data
To work with CoreData, we use Magic Recording: there are no other additional classes, except for the class that connects to the database of a specific user.
- Core data
- Magic recording
- DataBaseManager class
- DataSources
Separate classes for the UIViewController, which contains the logic for interacting with the database and server. - Models
We have two main types of models - local database models and intermediate models of information that comes to us from the server.
Synchronization layer
The logical layer on which we translate objects received from the server into database objects and vice versa (if necessary). If there is no object with the current id, add it to the database. If so, then just update the information from the server.
For models with a large number of fields, a generalized method of filling them with values using runtime functions is implemented:
watch code
-(id)syncLocal:(id)local withClass:(Class)localClass fromParse:(id)parse{
NSAssert([parse isKindOfClass:[UserStatistic class]], @"wrong class");
UserStatistic*stat =(UserStatistic*)parse;
LocalUserStatistics* newLocalStat = (LocalUserStatistics*)local;
if(!newLocalStat)
newLocalStat=[super syncLocal:local withClass:localClass fromParse:parse];
unsigned int outCount;
Protocol* protocol = objc_getProtocol("StatisticsProtocol");
objc_property_t *propList = protocol_copyPropertyList(protocol, &outCount);
NSArray* noSetProps = [self propertiesDontNeedToSet];
for (int i = 0; i < (int)outCount; i++) {
objc_property_t * oneProp = propList + i;
NSString *propName = [NSString stringWithUTF8String:property_getName(*oneProp)];
if([noSetProps indexOfObject:propName]==NSNotFound){
id newValue =[stat valueForKey:propName];
if(newValue && newValue!=[NSNull null]){
[newLocalStat setValue:newValue forKey:propName];
}
}
}
free(propList);
return newLocalStat;
}
- ParseObjectsSync
Layer for synchronizing arrays of objects - calls the corresponding class from the ParseObjectSync layer for the corresponding model - ParseObjectSync
The layer on which the logic is described, where we assign the Core Date model to each Parse model. We also convert the fields, if necessary. - Chat Engine
UI works with the network and database only through a layer of synchronization classes. The Chat Engine class also lies between the UI and Pubnub with CoreDate. It turned out to be large enough, but not so much that it was possible to distinguish from it a separate class, which would be called in accordance with the occupied layer. Although, most likely, I simply did not have the desire to do this.
In app purchase
In the application, from the very beginning, the internal currency was thought up - Coins, which the user can easily buy using the In-App Purchase mechanism, but there is always something to spend :). From Apple’s In-App Purchase point of view, they are a Consumable Product, i.e. you need to be very careful in recording and recording the income / consumption of coins, otherwise the user will lose money and be upset.
It was decided to make this thin layer without using third-party libraries. We decided to store Coins ourselves in the User model on parse.com, and not locally. This influenced how the transaction completion code works. After all, we must wait for the moment when the Coins changes are recorded on parse.com and only after that do finishTransaction. This is a great place to use Block, which holds the context to complete the transaction while we make a request to the server. This approach gave us the opportunity to log into the system from different devices and always have up-to-date Coins information for the current user.
Another thing that people usually don’t do: SKPaymentTransactionObserver (the class that implements this protocol) must be created at application startup and live its entire life, since an incomplete transaction that was not completed the last time the application was launched may arrive there. We did not create our Singleton here.
Work circle controller
During the development, more and more actions appeared that needed to be performed in a specific period of time and between such and such a request. For example, to maintain the consistency of the local database, it was necessary to load the user's pictures first, and only then the chats that link to these pictures. There were also many nuances in business logic: showing a screen with the required fields if the user registered via facebook and subscribing to push notifications after the login, it's not a fact that the login will happen simultaneously with the receipt of the token. You also need to unsubscribe from push notifications after logout, initialize some services immediately after launch, and others only after login. After all the logic of the service launch sequence and application life has been concentrated in a separate class, life has become much easier. The class, by the way, is not a singleton - it lives in AppDelegate. As a result, only 146 lines of code remained in AppDelegate.
Work with Parse.com
In general, I liked working with Parse, but it is still not clear how much traffic applications on it can work stably. At the moment, the service gives the following limits for one account (Pro plan):
Burst limit : 40 requests per second
API requests limit : 160 - you will not find this in the documentation (at the time of writing this is not)
Restriction on the execution of cloud code functions: 15 seconds
Limit on background job: 15 minutes
Our application has not yet gained a large number of users, and it is not entirely clear how it will behave in production, but there are doubts about this. The application is already reaching the limit on the number of API requests. I contacted the Parse team regarding the transition to the Enterprise plan with the following characteristics:
Total users: 1000
Users performing request in the same time: 500
API requests performing in the same time: 1000
API calls burst limit: 1000
Cloud code burst limit: 2000
They replied that 1,000 requests per second would cost $ 14,000 per month. After which I asked them how to reduce the number of requests, and described the operation of our application. They replied that 1000 requests per second for our application is justified, and less is unlikely to be done.
At Parse, it’s not yet possible to simply raise another environment for testing and development on the same database model. You have to create a new application and practically manually create the same data model.
In terms of limiting the number of requests, Parse loses Kinvey. I specifically learned about this restriction from Kinvey, and here is what they replied: "You are correct - we do not limit number of requests per second (or on total requests or API calls in any way)." For $ 1,400 per month, you can get BaaS, which can have 50,000 active users per month, 3 environments, and business logic is limited to 50 scripts. At the same time, one support script defined this: “BL scripts are written in their own containers within the Kinvey web console, so a BL script is defined as each chunk of JS code - certainly quite a lot can be fit into a single BL script if one so desires. ”How everything works in practice, I don’t know, but it looks attractive.
Cloud code on Backbone.js
As a person who wrote only in strongly typed languages and never touched the backend, I was very interested in studying backbone.js. The main difficulties encountered:
- Callback hell.
decided using the async.js library - Debugging
used Sublime to write code. Then I came across a post on how to configure the environment in Cloud9, and on the same day I found a message from the author of this instruction in which he shared the problem: after updating the service for deploying new code to the Parse server, everything stopped working for him, because the python version Cloud9 does not support some features. As a result, everything remained on Sublime, and debugging occurred only after deploy and code execution on the server - Testing.
See below. It deserves a separate chapter.
Integration testing
Preparing the testing environment
As one smart person said, the hardest part in tests is setting up the testing environment.
During development, more and more logic accumulated on the server code and more and more scripts appeared for testing. It became clear that without automatic tests, writing a cloud code would take a huge amount of time. As I wrote above, debugging was possible only after the deployment to the server, until the code was run it was impossible to find out even the presence of syntax errors, only if they were not directly related to the deployment.
As a result, we set up the testing environment. In tests, we use the Work Circle Controller to launch all the necessary services. Using the flags for the preprocessor, we set the code that we need to run for the test environment:
watch code
#import "WorkCircleController.h"
//UI
#if !TEST
#import "LoginViewsManager.h"
#endif
//Net
#import
#import "ParseRESTClient.h"
#import "StatisticService.h"
#import "ProfilePicturesSync.h"
#if !TEST
#import "ChatsEngine.h"
//Data
#import "DataBaseManager.h"
#import "LocalUser.h"
#import "LocalLockSlot.h"
#import "LocalPicture.h"
//Sync
#import "SyncManager.h"
#import "UserSync.h"
#else
#endif
#if !TEST
@interface WorkCircleController(){
ProfilePicturesSync* profilePicturesSync;
}
@property(nonatomic, weak) AppDelegate* appDelegate;
@property(nonatomic, strong) LoginViewsManager* loginManager;
@property(nonatomic, strong) NSData* deviceToken;
@end
#endif
@implementation WorkCircleController
#if !TEST
- (id)initWithDelegate:(AppDelegate*)appDelegate
{
self = [self init];
if (self) {
self.appDelegate = appDelegate;
profilePicturesSync = [ProfilePicturesSync new];
}
return self;
}
#pragma mark - Push notification
- (void)app:(UIApplication*)application didRegisterForRemoteNotificationsWithToken:(NSData*)deviceToken{
self.deviceToken = deviceToken;
if(self.state == LifeStateLoginDataAndNetLayersReady || self.state == LifeStateLoggedIn)
[self subscribeToPushes:deviceToken];
}
- (void)subscribeToPushes:(NSData *)deviceToken {
[self subscribeToParsePushes:deviceToken];
[self subscribeToChatPushes:deviceToken];
}
- (void)subscribeToParsePushes:(NSData *)deviceToken {
...
}
- (void)subscribeToChatPushes:(NSData *)deviceToken {
...
}
- (void)unsubscribeFromPushesInParseWithBlock:(void (^)(NSError *error))block {
...
}
#endif
#pragma mark - Pre Login Logic
-(void)setupPreLoginStateWithBlock:(void (^) (NSError* error))block{
[self prepareNetManagersForPreLoggin];
#if !TEST
[self performUIUpdatesForPreLogginWithBlock:^(NSError *error) {
if(block)
block(error);
}];
#else
finishWithErrorBlock(nil);
#endif
}
-(void)prepareNetManagersForPreLoggin{
[[ParseRESTClient sharedClient] startParseService];
}
#if !TEST
-(void)performUIUpdatesForPreLogginWithBlock:(void (^) (NSError* error))block
{
...
}
#endif
#pragma mark after register Login Logic
- (void)afterRegistrationWithBlock:(void (^) (NSError* error))block{
...
}
#pragma mark - Login Logic
-(void)loginWithBlock:(void (^) (NSError* error))block{
[self prepareDataManagersForLogginWithBlock:^(NSError *error) {
[self prepareNetManagersForLogginWithBlock:^(NSError *_error) {
#if !TEST
[self performUIUpdatesForLogginWithBlock:^(NSError *uIError) {
if(block)
block(uIError);
[self performAfterLoginUpdatesWithBlock:^(NSError *afterLoginError) {
}];
}];
#else
if(finishWithErrorBlock)
finishWithErrorBlock(error);
#endif
}];
}];
}
-(void)prepareDataManagersForLogginWithBlock:(void (^) (NSError* error))block{
#if !TEST
[DataBaseManager setupDataBaseWithUserId:[PFUser currentUser].email];
[[Settings defaultSettings] setUsername:[PFUser currentUser].email];
#endif
block(nil);
}
-(void)prepareNetManagersForLogginWithBlock:(void (^) (NSError* error))block{
[[ParseRESTClient sharedClient] updateToken];
#if !TEST
...
#endif
block(nil);
}
#if !TEST
- (void)performUIUpdatesForLogginWithBlock:(void (^) (NSError* error))block{
...
}
- (void)performAfterLoginUpdatesWithBlock:(void (^) (NSError* error))block {
...
}
#endif
#pragma mark - Logout Logic
-(void)logoutWithBlock:(void (^)(NSError *error))block{
#if !TEST
...
#else
[PFUser logOut];
[[ParseRESTClient sharedClient] closeClient];
block(nil);
#endif
}
- (void)deleteAccountWithBlock:(void (^)(NSError *error))block{
[[ParseRESTClient sharedClient] closeClient];
#if !TEST
...
#else
[PFUser logOut];
block(nil);
#endif
}
Next, we created separate classes in which we placed the implementation of different scripts that the user can execute.
In the base class, a single method is implemented:
- (void) setUpWithBlock:(void (^) (NSError* error))block{
[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:@"api.parse.com"]; //отключаем проверку сертификатов, иначе iOS не дает нам общаться через https в тестовой среде. Это приватное АПИ.
self.workCircleController = [WorkCircleController new];
[self.workCircleController setupPreLoginStateWithBlock:^(NSError *error) {
block(error);
}];
}
Next, we created an environment and logic for two users and for individual scenarios. Since the interaction with the server is implemented asynchronously, we used SRTAdditions.h to get callbacks and run tests correctly.
Development of the idea
The main goal of this approach was to reduce the time spent on testing different scenarios. I think it’s really possible to configure Jasmine or Mocha on Parse, so some cases would be easier to solve through unit tests, but on the whole integration tests proved to be justified: the builds became more stable, the time to develop new features on the cloud code decreased, and you can had to play tennis while all the tests are done.

After I began to play tennis too much time, the authorities became worried and decided to raise the serverhudson-ci.org , which takes the latest code from git, and runs sh scripts that run tests. To run the tests and beautifully display the logs, xtool was used . By the way, when running through the console, the tests do not start the simulator, and you can continue to work on the code while the tests are running.
Localization
As a result, I began to use a very simple tool for localization: agi18n
Tips & Tricks
- Why include warnings in draft article 1 , article 2
- Snippets ( sharing via dropbox ):
__weak typeof(self) weakSelf = self;
(void (^)(NSError *error))<#block#>
- New York Times Code Style
- Reveal . A very interesting tool for debugging UI. It is very convenient when it is unclear where I left the view or when they give support for a new large project and not enough time to figure out how the architecture of the view controllers is arranged and where which view is.
- convenient tool for generating countless icons of various sizes
www.gieson.com/Library/projects/utilities/icon_slayer