Tag: iOS

  • iOS – How to remove “Back” text from the back button

    The best solution is to create base classes for UIViewController and UITableViewController and override their init methods. This will allow you to add ViewControllers to the NavigationController programmatically after a rootViewController. If you put this code in the ViewDidLoad method, it will not be called in this case and the next controller will be pushed with a back text.

    BaseViewController in Objective C:

    #import "BaseViewController.h"
    
    @interface BaseViewController ()
    
    @end
    
    @implementation BaseViewController
    
    - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
    {
     self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
     if (self) {
     self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
     }
     return self;
    }
    
    @end
    

    BaseTableViewController:

    #import "BaseTableViewController.h"
    
    @interface BaseTableViewController ()
    
    @end
    
    @implementation BaseTableViewController
    
    - (instancetype)initWithStyle:(UITableViewStyle)style
    {
     self = [super initWithStyle:style];
     if (self) {
     self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil];
     }
     return self;
    }
    
    @end
    
  • Some useful features of a CocoaPods podfile

    You can specify build settings for all pods or only for one pod. Here is an example:

    
    post_install do | installer |
        installer.pods_project.targets.each do |target|
            target.build_configurations.each do |config|
                config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf'
                config.build_settings['ENABLE_BITCODE'] = 'YES'
                
                if target.name == 'GPUImage2'
                    target.build_configurations.each do |config|
                        config.build_settings['SWIFT_VERSION'] = '3.0'
                    end
                end
            end
        end
    end
    
    

    You can create a podspec for a git repo, that is not a pod. Add this podspec to you project root and make a link for an added pod to specify a podspec. This is a better way to create pods for projects that do not have a pod, than creating a fork with a podspec, since you get latest updates more easily.

    pod 'GPUImage2', :podspec => './GPUImage2.podspec'
    
  • Updating View With Two Data Sources in iOS

    In this article I am going to describe a robust, simple and clean app architecture that allows to update a single iOS tableView from two different data sources asynchronously.

    Let’s say, we have two sources of data, that need to be displayed in a tableView. When user pulls to refresh a tableView, two separate asynchronous requests are performed. While updating continues we need to show activity indicator and we can dismiss it only when both requests are finished.

    This can be cleanly accomplished using ReactiveCocoa + MVVM. One of the benefits of ReactiveCocoa is in the easiness of synchronization of asynchronous data streams.

    One would say, that this task can be also performed by using of dispatch groups. I agree, but it is more complex and not easy to do when the nature of data sources is different. For example, when one of the datasources is HTTP API and the second is a WebSocket.

    Architecture looks like this:

    In a ViewModel we have 3 arrays – cloudData1 (or any other name you want, that better describes your domain), cloudData2 and a union single datasource of a TableView – tableData. When cloudData1 or cloudData2 updates, the update of tableData is triggered and then it triggers an update of the TableView itself by bindings.

    In the init method of a ViewModel we create a binding of cloudData1 and cloudData2 to tableData:

    - (instancetype)init
    {
        self = [super init];
        
        if (self) {
            RAC(self, tableData) = [RACSignal combineLatest:@[RACObserve(self, cloudData1), RACObserve(self, cloudData2)]
                                                     reduce:^id _Nullable (NSArray *cloudData1, NSArray *cloudData2) {
                                                         NSMutableArray *temp = [NSMutableArray new];
                                                         [temp addObjectsFromArray:cloudData1];
                                                         [temp addObjectsFromArray:cloudData2];
                                                         return [NSArray arrayWithArray:temp];
                                                     }];
            
            RAC(self, inProgress) = [RACSignal combineLatest:@[RACObserve(self, cloud1RequestInProgress), RACObserve(self, cloud2RequestInProgress)]
                                                      reduce:^id _Nullable (NSNumber *cloud1RequestInProgress, NSNumber *cloud2RequestInProgress) {
                                                          return @(cloud1RequestInProgress.boolValue || cloud2RequestInProgress.boolValue);
                                                      }];
        }
        
        return self;
    }
    

    Also we combine two flags of progress cloud1RequestInProgress and cloud2RequestInProgress to a single one – inProgress.

    LoadData method of a ViewModel starts the loading of data from each of two datasources:

    - (void)loadData
    {
        [self loadCloudData1];
        
        [self loadCloudData2];
    }
    

    And in a TableView we create a binding between ViewModels tableData and a View:

    [RACObserve(self, viewModel.tableData)
         subscribeNext:^(id  _Nullable x) {
             [self.tableView reloadData];
         }];
    

    Pull to refresh triggers loadData of a ViewModel:

    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max) {
            self.tableView.refreshControl = [UIRefreshControl new];
            [self.tableView.refreshControl addTarget:self.viewModel
                                              action:@selector(loadData)
                                    forControlEvents:UIControlEventValueChanged];
        }
    

    In a view we also create a binding between inProgress property of a ViewModel and the need to dismiss refreshing indicator:

    [[RACObserve(self, viewModel.inProgress)
          filter:^BOOL(NSNumber*  _Nullable inProgress) {
              return ! inProgress.boolValue;
          }]
         subscribeNext:^(id  _Nullable x) {
             [self.tableView.refreshControl endRefreshing];
         }];
    

    Simple and brief -> easy to support, extend and reuse.

  • iOS Unit Test Examples / Snippets

    In this article I will post common iOS Unit testing examples for testing of:

    1. Asynchronous loading of data using REST API
    2. Loading of a UIView
    3. Method call after UITableViewCell is clicked
    4. Static UITableView sections have correct number of rows
    5. Loading of a UIView from UIStoryboard
    6. IBOutlet connection
    7. Model logic
    8. UITableViewCell subclass
    9. IBAction / Method is implemented
    10. Testing bundle name

    Asynchronous loading of data using REST API

    // Testing REST API GET /api/cities/ that returns an array of cities with id and name
    - (void)testCitiesAPI
    {
        // We create XCTestExpectation object to test asynchronous loading
        XCTestExpectation *testExpectation = [self expectationWithDescription:@"testCitiesAPI"];
        
        NSString *URLString = [NSString stringWithFormat:@"/api/cities/"];
        
        [[RSHTTPClient sharedClient] GET:URLString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
            
            NSArray *jsonArray = (NSArray *)responseObject;
            
            // Checking that jsonArray is not empty
            XCTAssertNotNil(jsonArray);
            
            for (NSDictionary *dic in jsonArray) {
                RSCity *city = [RSCity objectWithProperties:dic];
                
                // Checking consistency of a model object created from JSON dic
                XCTAssertNotNil(city.name);
                XCTAssertNotEqual(city.sid, 0);
            }
            
            [testExpectation fulfill];
            
        } failure:^(NSURLSessionTask *task, NSError *error) {
            
            NSLog(@"Error: %@", error);
            XCTFail(@"Error in GET /api/cities/");
            
        }];
        
        // Waiting for 10 seconds untim fulfill is called when task is finished
        [self waitForExpectationsWithTimeout:10 handler:^(NSError *error) {
            
        }];
    }
    

    Here [RSHTTPClient sharedClient] is an instance of AFHTTPSessionManager subclass. But it doesn’t matter, the main is the principle.

    Loading of a UIView

    @interface RSBaseViewControllerTests : XCTestCase
    
    @property (nonatomic, strong) RSBaseViewController *vc;
    
    @end
    
    @implementation RSBaseViewControllerTests
    
    - (void)setUp {
        [super setUp];
        
        self.vc = [[RSBaseViewController alloc] init];
        [self.vc view];
    }
    
    - (void)tearDown {
        self.vc = nil;
        [super tearDown];
    }
    
    - (void)testThatViewLoads {
        XCTAssertNotNil(self.vc.view);
    }
    
    @end
    

    Method call after UITableViewCell is clicked

    #import "OCMock.h"
    
    # Mock objects for Objective-C
    pod 'OCMock', '~> 3.4'
    
    - (void)testThatHomekitRestoreCalled
    {
        id mock = [OCMockObject partialMockForObject:self.vc];
        
        [[mock expect] restoreHomeKitClicked];
        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:CCSettingsSectionRestoreHomeKit];
        [self.vc tableView:self.vc.tableView didSelectRowAtIndexPath:indexPath];
        
        [mock verify];
    }
    

    Here we verify that method restoreHomeKitClicked is called when user selects a specific row in UITableView.

    Static UITableView sections have correct number of rows

    - (void)testThatSectionsHaveCorrentNumberOfRows
    {
        XCTAssertEqual([self.vc tableView:self.vc.tableView numberOfRowsInSection:CCSettingsSectionUpdate], 1);
        XCTAssertEqual([self.vc tableView:self.vc.tableView numberOfRowsInSection:CCSettingsSectionGeneral], 2);
        XCTAssertEqual([self.vc tableView:self.vc.tableView numberOfRowsInSection:CCSettingsSectionAdditional], 3);
        XCTAssertEqual([self.vc tableView:self.vc.tableView numberOfRowsInSection:CCSettingsSectionRestoreHomeKit], 1);
    }
    

    Loading of a UIView from UIStoryboard

    - (void)setUp
    {
        [super setUp];
        
        UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
        self.vc = [storyboard instantiateViewControllerWithIdentifier:@"RECameraListViewController"];
        [self.vc performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];
        [self.vc view];
    }
    
    
    - (void)tearDown
    {
        self.vc = nil;
        [super tearDown];
    }
    
    
    #pragma mark - View loading tests
    
    - (void)testThatViewLoads
    {
        XCTAssertNotNil(self.vc.view, @"View not initiated properly");
    }
    
    

    IBOutlet connection

    - (void)testOutlets {
        XCTAssertNotNil(self.vc.vkButton);
        XCTAssertNotNil(self.vc.fbButton);
        XCTAssertNotNil(self.vc.googleButton);
        XCTAssertNotNil(self.vc.agreementTextView);
    }
    

    Model logic

    - (void)testBalanceArrayCount
    {
        CSAccount *account = [CSAccount sharedAccount];
        NSArray *balanceArray = [account balanceArrayForNumberOfMonths:12];
        
        XCTAssertEqual([balanceArray count], 13);
    }
    

    Here we test that an array, that is formed inside our model has correct number of items. Model logic testing obviously can be different depending of logic itself.

    UITableViewCell subclass

    - (void)testThatServerCellHasCorrectOutlets
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        REServerCell *cell = (REServerCell *)[self.vc tableView:self.vc.tableView cellForRowAtIndexPath:indexPath];
        XCTAssertNotNil(cell.cloudServerLabel, @"Should connect cloudServerLabel IBOutlet.");
        XCTAssertNotNil(cell.controlCenterServerLabel, @"Should connect controlCenterServerLabel IBOutlet.");
    }
    
    
    - (void)testThatServerCellHasCorrectContents
    {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        REServerCell *cell = (REServerCell *)[self.vc tableView:self.vc.tableView cellForRowAtIndexPath:indexPath];
        
        REServerItem *item = self.vc.tableData[0];
        
        NSString *cloudServerURL = cell.cloudServerLabel.text;
        NSString *controlCenterServerURL = cell.controlCenterServerLabel.text;
        
        XCTAssertTrue([cloudServerURL isEqualToString:item.cloudServer]);
        XCTAssertTrue([controlCenterServerURL isEqualToString:item.controlCenterServer]);
    }
    

    IBAction / Method is implemented

    - (void)testThatHelperHasCustomizeAppearanceMethod
    {
        XCTAssertTrue([self.helper respondsToSelector:@selector(customizeAppearance)]);
    }
    

    Testing bundle name

    - (void)testThatBundleNameIsCorrect
    {
        NSString *correctBundleName = @"YourBundleName";
        NSDictionary *infoDic = [[NSBundle mainBundle] infoDictionary];
        NSString *bundleName = infoDic[@"CFBundleName"];
        XCTAssertTrue([bundleName isEqualToString:correctBundleName], @"Strings are not equal %@ %@", correctBundleName, bundleName);
    }
    

    The last is a rare case, but helps to prevent someone changing a bundle name of project accidentally.

  • How to create semi-transparent outside border for UIImageView

    Just in case if somebody looks for an UIImage with transparent outside border. If you simply set a layer border, you will get a transparent border, but you will see inside image behind that border, not outside image. I managed to create an ImageView with transparent outside border.

    It looks like this:
    cool image

    The idea is simple. I save UIImage from UIImageView. Then I remove UIImage and set initial layer as border layer. Then I put a new smaller sublayer above it and set a saved UIImage as contents of it.

        #import 
        
        @interface SSCircleImageView : UIImageView
        
        
        @end
    
    --------------------
    
        
        #import "SSCircleImageView.h"
        
        
        
        @implementation SSCircleImageView
        
        
        const CGFloat borderWidth = 5.0f;
        const CGFloat borderAlpha = 0.3f;
        
        
        - (void)awakeFromNib
        {
            UIImage *image = self.image;
            self.image = nil;
            
            self.clipsToBounds = YES;
            self.layer.cornerRadius = self.frame.size.width / 2.0;
            self.layer.borderWidth = borderWidth;
            self.layer.borderColor = [[UIColor colorWithWhite:1.0f alpha:borderAlpha] CGColor];
            
            CALayer *subLayer = [CALayer layer];
            subLayer.frame =  CGRectInset(self.bounds, self.layer.borderWidth, self.layer.borderWidth);
            subLayer.cornerRadius = subLayer.frame.size.width / 2.0;
            subLayer.contents = (id)image.CGImage;
            [self.layer addSublayer:subLayer];
        }
        
        
        
        @end
    

    The same approach can be used for UIButton or UIView.

  • Что нового в iOS 9

    Новинки в iOS 9:

    1. Новое встроенное приложение News, улучшения Notes, Maps, Wallet, CarPlay.

    2. На iPad появится Split View, можно будет пользоваться несколькими приложениями одновременно, например, Safari и Twitter.

    3. Siri станет умнее. Например, можно будет попросить Siri напомнить о чем-то, когда ты будешь дома. Она сама создает уведомление с привязкой к локализации.

    4. Улучшения производительности. Батарея на 1 час дольше будет держаться. Встроенные приложения начнут использовать Metal, то есть по сути графический процессор. Для обновления системы достаточно будет не 4, а 1 гб свободной памяти.

    http://www.apple.com/ios/ios9-preview/

    #iOS9, #Apple

  • iOS : Creating Random E-mail and Phone Number

    roulette_lg

    This is a sample code how to create random e-mail and phone number. I used it in my unit tests of authorization.

    - (NSString *)randomEmail
    {
        //Format:xxxxx@rdw.ru
        NSString *alphabet  = @"abcdefghijklmnopqrstuvwxyz0123456789";
        NSString *randomString = [self randomStringWithAlphabet:alphabet numberOfChars:5];
        NSString *randomEmail = [NSString stringWithFormat:@"%@@rdw.ru", randomString];
        NSLog(@"randomEmail: %@", randomEmail);
        
        return randomEmail;
    }
    
    
    - (NSString *)randomPhoneNumber
    {
        //Format:89xxxxxxxxx
        NSString *alphabet  = @"0123456789";
        NSString *randomString = [self randomStringWithAlphabet:alphabet numberOfChars:9];
        NSString *randomPhoneNumber = [NSString stringWithFormat:@"89%@", randomString];
        NSLog(@"randomPhoneNumber: %@", randomPhoneNumber);
        
        return randomPhoneNumber;
    }
    
    
    - (NSString *)randomStringWithAlphabet:(NSString *)alphabet numberOfChars:(NSInteger)numberOfChars
    {
        NSMutableString *s = [NSMutableString stringWithCapacity:numberOfChars];
        for (NSUInteger i = 0U; i < numberOfChars; i++) {
            u_int32_t r = arc4random() % [alphabet length];
            unichar c = [alphabet characterAtIndex:r];
            [s appendFormat:@"%C", c];
        }
        
        return s;
    }
    
  • Solution for iOS Developer Technical Interview Problem

    I have found an interesting iOS Developer Technical Interview Problem and solved it.

    “I also have the ultimate iOS Developer technical test you can assign a potential hire. It should take 1-3 hours. It is easy to communicate, allows a lot of freedom of implementation so you can really get a better picture into how a developer thinks, and will make sure this developer knows the absolute fundamentals. Ready for it?

    Calculate the and display each Fibonacci number from 1 -> max N possible on an iPhone with unsigned integers, and display each F(n) in a table view. The UITableView scrolling MUST remain smooth.

    That’s it. You’ll be amazed at how profoundly simple this task sounds and yet how much iOS knowledge can be demonstrated. Not just what they know, but how they structure their work. You can assess their APIs, their separation of concerns when designing classes, the considerations they’ve made for performance, and their knowledge of concurrency. (Not to mention their knowledge of recursive functions.) It is ok to give them the formula, and allow them to use Google. F(n) = F(n-1) + F(n-2).”

    This is the result:
    IMG_0051

    GitHub repository with a solution is here.

    I used recursion and concurrency. I added a little delay of 0.1 s to make a delay visible but scrolling is still smooth. This is my solution:

    //
    //  FiboTableViewController.m
    //  fibo
    //
    //  Created by Dennis Kutlubaev on 05.11.14.
    //  Copyright (c) 2014 Alwawee. All rights reserved.
    //
    
    #import "FiboTableViewController.h"
    
    
    @interface FiboTableViewController ()
    {
        NSInteger _maxN;
        dispatch_queue_t _backgroundQueue;
    }
    
    @property (nonatomic, strong) NSMutableArray *tableData;
    
    
    @end
    
    
    @implementation FiboTableViewController
    
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"Max Integer: %ld", NSIntegerMax);
        
        _maxN = 10000;
        
        _backgroundQueue = dispatch_queue_create("background queue", nil);
        
        [self loadData];
    }
    
    
    - (void)loadData
    {
        _tableData = [NSMutableArray arrayWithArray:@[@(1),@(2)]];
    }
    
    
    #pragma mark - Table view data source
    
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
        return 1;
    }
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return _maxN;
    }
    
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
        
        NSInteger n = indexPath.row;
        __block NSInteger fibo;
        if ( n < [_tableData count] ) {
            fibo = [_tableData[n] integerValue];
            if (fibo > 0) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.textLabel.text = [NSString stringWithFormat:@"%ld", (long)fibo];
                });
            }
        }
        else {
            dispatch_async(_backgroundQueue, ^{
                
                fibo = [self fibo:n];
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (fibo > 0) {
                        cell.textLabel.text = [NSString stringWithFormat:@"%ld", (long)fibo];
                    }
                    else {
                        [self.tableView reloadData];
                        //cell.textLabel.text = [NSString stringWithFormat:@"Max integer reached: %ld", NSIntegerMax];
                    }
                });
                
            });
        }
        
        return cell;
    }
    
    
    //F(n) = F(n-1) + F(n-2)
    - (NSInteger)fibo:(NSInteger)n
    {
        NSInteger f;
        
        if ( n < [_tableData count] ) {
            f = [_tableData[n] integerValue];
        }
        else {
            [NSThread sleepForTimeInterval:0.1];
            
            f = [self fibo:(n-2)] + [self fibo:(n-1)];
            
            if ((f < NSIntegerMax)&&(f > 0)) {
                [_tableData addObject:@(f)];
            }
            else {
                f = 0;
                _maxN = n-1;
            }
        }
        
        return f;
    }
    
    
    @end
    
    

    It scrolls without lags on iPhone 6 Plus. I do not calculate twice a fibonacci number if already have calculated it, I store a new fibonacci number in a NSMutableArray.

  • Using dispatch group to wait until multiple operations executed – including completion block (AFNetworking)

    // Загрузка дополнительных данных с сервера по API для форм
    - (void)loadAdditionalInformationFromServer
    {
        loadingError = nil;
        success = YES;
        
        group = dispatch_group_create();
        
        [self startOfferEducationsRetrieveOperation];
        
        [self startOfferLanguageLevelsRetrieveOperation];
        
        [self startOfferTradesRetrieveOperation];
        
        [self startOfferLanguagesRetrieveOperation];
        
        [self startOperatingScheduleRetrieveOperation];
        
        [self startRegionsRetrieveOperation];
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
         [self.delegate resumeManagerDidFinishLoadingAdditionalInformationWithSuccess:success error:loadingError];
        });
        
        //dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
        dispatch_release(group);
    }
    
    
    // Запрос на получение уровней образования
    - (void)startOfferEducationsRetrieveOperation
    {
        if ([[RRSession sharedSession] offerEducations] == nil) {
            
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RROfferEducation *obj = [[RROfferEducation alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setOfferEducations:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodOfferEducations successBlock:successBlock];
        }
    }
    
    
    // Запрос на получение уровней владения языком
    - (void)startOfferLanguageLevelsRetrieveOperation
    {
        if ([[RRSession sharedSession] offerLanguageLevels] == nil) {
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RROfferLanguageLevel *obj = [[RROfferLanguageLevel alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setOfferLanguageLevels:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodOfferLanguageLevels successBlock:successBlock];
        }
    }
    
    
    // Запрос на получение языков
    - (void)startOfferLanguagesRetrieveOperation
    {
        if ([[RRSession sharedSession] offerLanguages] == nil) {
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RROfferLanguage *obj = [[RROfferLanguage alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setOfferLanguages:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodOfferLanguages successBlock:successBlock];
        }
    }
    
    
    // Запрос на получение графиков работы
    - (void)startOperatingScheduleRetrieveOperation
    {
        if ([[RRSession sharedSession] operatingSchedules] == nil) {
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RROperatingSchedule *obj = [[RROperatingSchedule alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setOperatingSchedules:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodOperatingSchedules successBlock:successBlock];
        }
    }
    
    
    // Запрос на получение регионов
    - (void)startRegionsRetrieveOperation
    {
        if ([[RRSession sharedSession] regions] == nil) {
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RRRegion *obj = [[RRRegion alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setRegions:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodRegions successBlock:successBlock];
        }
    }
    
    
    // Запрос на получение рубрик вакансий
    - (void)startOfferTradesRetrieveOperation
    {
        if ([[RRSession sharedSession] offerTrades] == nil) {
            // Блок успешного завершения запроса
            SuccessBlock successBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
                
                NSArray *jsonArray = (NSArray *)JSON;
                NSMutableArray *temp = [NSMutableArray new];
                for (NSDictionary *jsonDic in jsonArray) {
                    RROfferTrade *obj = [[RROfferTrade alloc] initFromDic:jsonDic];
                    [temp addObject:obj];
                }
                [[RRSession sharedSession] setOfferTrades:temp];
                
                dispatch_group_leave(group);
            };
            
            // Добавление запроса на загрузку в очередь
            [self addOperationForMethod:RRApiMethodOfferTrades successBlock:successBlock];
        }
    }
    
    
    // Добавление запроса на загрузку в очередь
    - (void)addOperationForMethod:(NSString *)methodName successBlock:(SuccessBlock)successBlock
    {
        dispatch_group_enter(group);
        
        RRHTTPClient *client = [RRHTTPClient sharedClient];
        NSString *apiMethodPath = [NSString stringWithFormat:@"%@%@", [RRHelper apiDomain], methodName];
        NSMutableURLRequest *request = [client requestWithMethod:@"GET" path:apiMethodPath parameters:nil];
        AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:successBlock failure:[self getStandartFailureBlock]];
        [operation start];
        
        //[[RRServerConnection sharedConnection] addCustomOperation:operation];
    }
    
    
    // Блок обработки ошибки при запросе
    - (FailureBlock)getStandartFailureBlock
    {
        FailureBlock failureBlock = ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
            
            success = NO;
            loadingError = error;
            
            NSLog(@"Error: %@", error);
            [RRHelper showServerErrorMessage];
            
            dispatch_group_leave(group);
            
        };
        
        return failureBlock;
    }
    
  • Sorting objects in Objective-C in Alphabet Order by Name

    It is a very easy but a common task. There is no need to create any custom NSSortDescriptors. Let’s say, we have an unsorted array of RRRegion objects, that have a name property. This is how we can sort this array:

      NSArray *regionArray = [[RRSession sharedSession] regions];
      // Sorting in alphabet order by name
      NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
      regionArray = [regionArray sortedArrayUsingDescriptors:@[sortDescriptor]];