Записки iOS разработчика: Делимся опытом, часть 1

    imageЗдравствуйте, дорогие читатели Хабра!

    Я разрабатываю приложения под iOS и Mac OS. Уже около года занимаюсь фрилансом и, переходя от клиента к клиенту, начал замечать, что в задаче разбираюсь одиножды; а при появлении похожего заказа, просто использую уже разработанные ранее модули. В серии статей «Записки iOS разработчика» я постараюсь осветить некоторые часто встречающиеся в заказах аспекты; напишу подобие шпаргалки, прочитав которую, вы сможете быстро и безболезненно внедрить новую технологию в свой проект. Мои заметки ни в коем случае не претендуют на глубокое понимание процессов, но описывают легкий способ закончить заказ в срок.

    Содержание:

    1. Часть 1: Работа с Файлами; Шаблон Singleton; Работа с Аудио; Работа с Видео; In-App Purchases
    2. Часть 2: Собственные всплывающие окна (Popups); Как использовать Modal Segue в Navigation Controller; Core Graphics; Работа с UIWebView и ScrollView
    3. Часть 3: Жизнь без Autolayout; Splash Screen; Работа с ориентацией девайса в iOS 6+; Сдвиг содержимого UITextField
    4. Часть 4: Google Analytics; Push Notifications; PSPDFKit; Вход в приложение через Facebook; Рассказать друзьям — Facebook, Twitter, Email
    5. Часть 5: Core Data; UITableView и UICollectionView

    Работа с Файлами


    Многие начинающие iOS разработчики натыкаются на следующую проблему: приложение корректно работает в симуляторе, но отказывается работать или работает некорректно на реальном устройстве. Одна из возможных проблем состоит в том, что приложение активно использует файловые ресурсы, неперенесенные в папку документов приложения. Политика Apple такова: вы не можете менять файлы приложения. Единственное место, где можно развлечься — это папка документов. Читать вы можете отовсюду (в рамках своего приложения), а вот писать только в папке документов.
    Значит, при первом запуске, нужно перенести все документы для записи в папку документов! Запросто! Начнем с создания файла Config.h, добавим следующую строчку в Your_App_Name-Prefix.pch (этот файл автоматически добавлен во все файлы вашего проекта):

    #import "Config.h"
    

    Отлично! Теперь все, что есть в Config.h, есть и во всем проекте! Давайте заполним этот файл:

    #define pathToApplicationDirectory [[NSBundle mainBundle] bundlePath] // Путь к папке приложения
    #define pathToDocuments [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] // Путь к документам
    
    #define pathToSettings [[pathToDocuments URLByAppendingPathComponent:@"settings.plist"] path] // Путь к файлу настроек
    #define pathToPopups [[NSBundle mainBundle] pathForResource:@"popups" ofType:@"plist"] // Путь к файлу со всплывающими окнами
    

    Все это делается исключительно для нашего с вами удобства в будущем (работать с файлами мы будем много и хорошо бы избавиться от магических строк).
    Теперь можно приступить к копированию файлов в папку с документами. Файлы settings.plist и popups.plist мы будем часто изменять, так что пункт с копированием необходим. Добавим следующий код в наш application: didFinishLaunchingWithOptions:

    // Я юзер, а не лузер, так что перемещу ресурсы к документам!
    [self placeResourcesToDocumentsDirectory:@{
         @"settings" : @"plist",
         @"popups" : @"plist"}];
    

    И, конечно же, сам метод placeResourcesToDocumentsDirectory:

    Жми меня!
    /*!
     Метод, копирующий файлы из словаря в папку с документами
     /param Словарь с именами файлов 
     */
    - (void)placeResourcesToDocumentsDirectory:(NSDictionary *)resources {
        // Проверим один из файлов, вдруг скопировано уже
        if (![[NSFileManager defaultManager] fileExistsAtPath:pathToSettings) {
            
            for (NSString *fileName in [resources allKeys]) {
                NSString *extension = resources[fileName];
                
                NSURL *storeURL = [pathToDocuments URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", fileName, extension]];
                NSURL *preloadURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:extension]];
                NSError *error = nil;
                [[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&error];
            }
        }
    }
    

    Вот и все! Как просто оказалось переместить файлы в нужную папку, не так ли? А получать доступ к файлу и работать с ним можно вот так:

    NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithContentsOfFile:pathToSettings];
    settings[@"isThisAppCool"] = @YES;
    [settings writeToFile:pathToSettings atomically:YES];
    

    Шаблон Singleton


    Это скорее небольшой сниппет, который может облегчить вам работу с одиночками.
    Singleton.h:

    Жми меня!
    <...>
    // Упростим доступ к синглтону
    #define coolSingleton [Singleton sharedSingleton]
    
    @interface Singleton : NSObject
    + (Singleton *)sharedSingleton;
    <...>
    

    Singleton.m:

    Жми меня!
    <...>
    @implementation Singleton
    
    + (Singleton *)sharedSingleton {
        static Singleton *sharedSingleton = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedSingleton = [[self alloc] init];
        });
        return sharedSingleton;
    }
    <...>
    

    Здесь все просто. Во-первых, мы упростили одной дефиницией работу с синглтоном. Во-вторых, есть один статичный объект класса Singleton. В-третьих, по вызову метода класса sharedSingleton мы получим либо уже существующий объект, либо инициализируем новый. Всегда будет только один объект класса Singleton. В-четвертых, наш синглтон потокобезопасен (спасибо danilNik и AndrewShmig за подсказку!).

    Работа с Аудио


    Здесь нам придется поработать с классом AVAudioPlayer из фреймворка AVFoundation. Принцип действия прост: создаем объект класса AVAudioPlayer с именем определенного файла, подготавливаем его к воспроизведению и запускаем, когда нужно. Создадим простой синглтон, который и будет содержать все наши аудиоплееры. У нас будет два аудиоплеера: один для фоновой музыки, второй для воспроизведения звука нажатия кнопки.

    Посмотрим на SimpleAudioPlayer.h:

    Жми меня!
    #import <Foundation/Foundation.h>
    #import <AVFoundation/AVFoundation.h>
    
    #define audioPlayer [SimpleAudioPlayer sharedAudioPlayer]
    
    @interface SimpleAudioPlayer : NSObject
    @property (nonatomic, retain) AVAudioPlayer *backgroundMusicPlayer;
    @property (nonatomic, retain) AVAudioPlayer *buttonSoundPlayer;
    
    + (SimpleAudioPlayer *)sharedAudioPlayer;
    
    @end
    

    И на SimpleAudioPlayer.m:

    Жми меня!
    #import "SimpleAudioPlayer.h"
    
    @implementation SimpleAudioPlayer
    
    static SimpleAudioPlayer *sharedAudioPlayer;
    
    + (SimpleAudioPlayer *)sharedAudioPlayer {
        static SimpleAudioPlayer *sharedAudioPlayer = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedAudioPlayer = [[self alloc] init];
        });
        return sharedAudioPlayer;
    }
    
    - (id)init {
        self = [super init];
        if (self) {
            [self initAudioPlayers];
        }
        return self;
    }
    
    /*!
      Метод, инициализирующий аудиоплееры
     */
    - (void)initAudioPlayers {
        NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:pathToBackgroundAudio];
        self.backgroundMusicPlayer = [[AVAudioPlayer alloc]
                            initWithContentsOfURL:fileURL error:nil];
        [self.backgroundMusicPlayer prepareToPlay];
        self.backgroundMusicPlayer.numberOfLoops = -1;
        
        fileURL = [[NSURL alloc] initFileURLWithPath:pathToButtonAudio];
        self.buttonSoundPlayer = [[AVAudioPlayer alloc]
                                  initWithContentsOfURL:fileURL error:nil];
        [self.buttonSoundPlayer prepareToPlay];
    }
    

    Вот и все. Дефиниции путей к аудиофайлам можно прописать в Config.h. Стоит отметить, что мы указали отрицательное число для количества повторений фоновой музыки. Если вы установите отрицательное число у этого свойства — то аудиофайл будет повторяться бесконечно. То, что надо! Так же не стоит забывать о методе prepareToPlay — если подготовить все аудиоплееры, как только приложение запущено, то не будет маленькой задержки перед первым воспроизведением аудиофайла. А пользоваться нашим аудиоплеером можно вот так:

    [audioPlayer.backgroundMusicPlayer play];
    <...>
    [audioPlayer.backgroundMusicPlayer stop];
    

    Работа с видео


    Поработаем немного с фреймворком MediaPlayer. Фактически, следующий код можно добавить даже в метод viewDidAppear:

    NSURL *url = [[NSURL alloc] initFileURLWithPath:pathToMovie];
    MPMoviePlayerViewController *movieController = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
    [self presentMoviePlayerViewControllerAnimated:movieController];
    [movieController.moviePlayer play];
    

    Этот код просто показывает видеофайл по данному URL. В видеопроигрывателе от Apple уже есть все нужные кнопочки, чтобы видеопроигрыватель закрыть. Так что четыре строчки выше — минимальный набор символов для включения видео в ваше приложение.

    In-App Purchases


    Ну, и самое интересное на сегодня: фреймворк StoreKit! Правда, работать мы будем не напрямую с ним, но с MKStoreKit. Огромное спасибо MugunthKumar за великолепный фреймворк!

    Все просто: отредактируйте файл MKStoreKitConfigs.plist под ваши нужды (там все интуитивно понятно) и используйте следующий код для проверки покупок:

    if ([MKStoreManager isFeaturePurchased:@"me.identifier.coolapp.somesinglefeature"]) {
    // Юзер уже купил эту фишку! Нужно ее ему отдать!
    }
    if ([MKStoreManager isSubscriptionActive:@"me.identifier.coolapp.somesubscription"]) {
    // Вау! Да мы можем тянуть деньги ежемесячно, еженедельно, ежедневно! Но сегодня юзер уже заплатил, отдадим ему заслуженную плюшку
    }
    

    Для осуществления покупок используйте следующее:

    [[MKStoreManager sharedManager] buyFeature:@"me.identifier.coolapp.somesinglefeature" 
                                    onComplete:^(NSString* purchasedFeature, 
                                                 NSData* purchasedReceipt, 
                                                 NSArray* availableDownloads) {
         NSLog(@"Юзер купил: %@", purchasedFeature);
     }
                                   onCancelled:^ {
         NSLog(@"Юзер отказался покупать :( печально.");
     }];
    

    Заключение


    Вот и все на сегодня. Эта часть была пилотной, в следующих статьях будем рассматривать примеры поинтереснее. Например, как создать такое всплывающее окно, чтобы наш клиент мог изменять его поведение прямо в .xib файлах, без вашего участия.

    Обо всех ошибках и неточностях статьи прошу писать в мой хабрацентр.

    P.S. Если вы хотите сотрудничать со мной, то мой профиль есть на одной из крупных бирж фрилансеров.

    Стоит ли продолжать серию статей «Записки iOS разработчика»? Выразите свое мнение в комментариях.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 30
    • +1
      Спасибо! Хотя тут сплошные синглтоны, но для быстрой разработки маленьких проектов пойдет — я сам так делал.
      Снипет для синглтону сразу режет глаз. Уже давно пользуюсь потокобезопасным определением синглтона через GCD:

      + (id)sharedManager {
          static MyManager *sharedMyManager = nil;
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              sharedMyManager = [[self alloc] init];
          });
          return sharedMyManager;
      }
      • –1
        Но в таком случае разве объект не будет обнуляться с каждым вызовом sharedManager?
        Можно изменить Singleton.m следующим образом:

        <...>
        @implementation Singleton
        
        static Singleton *sharedSingleton;
        
        + (Singleton *)sharedSingleton {
            if (sharedSingleton == nil) {
                static dispatch_once_t onceToken;
                dispatch_once(&onceToken, ^{
                    sharedSingleton = [[Singleton alloc] init];
                });
            }
            return sharedSingleton;
        }
        <...>
        
        • +2
          static же.
          • +2
            Спасибо за пояснение! Почитал про static, все стало понятно :)
            Поправил текст статьи, плюсанул в карму :)
            • 0
              И так можно.
              Где-то в includes.h

              // macros for singletone creation
              #define DEFINE_SHARED_INSTANCE_USING_BLOCK(block) \
              static dispatch_once_t pred = 0; \
              __strong static id _sharedObject = nil; \
              dispatch_once(&pred, ^{ \
              _sharedObject = block(); \
              }); \
              return _sharedObject; \
              
              


              Потом там, где нужно

              #pragma mark - get Instance
              +(ILStateManager *)sharedManager
              {
              	DEFINE_SHARED_INSTANCE_USING_BLOCK(^{
              		return [[ILStateManager alloc] init];
              	});
              }
              
              
      • +2
        Продолжайте делиться опытом!
        Хоть для меня новое было только MKStoreKit (еще не работал с микротранзакциями), но все же, может еще что нибудь новое увижу.
        • +4
          Это все конечно интересно, но Вы лучше расскажите о фрилансе. Где и как Вы находите клиентов? На сколько они адекватные? Какие подводные камни в ремесле фрилансера к контексте iOS/MacOS разработчика и т.п.
          • 0
            Интересный комментарий :)
            Работаю на elance.com, клиентура пока что только там. Все абсолютно адекватные, пичкают бонусами и платят хорошие деньги (а может это просто я так работаю). Подводные камни? Думаю, такие же, как и во всех других сферах фриланса :) Порой сложно работать дома. Если хорошо знать свое дело, деньги будут :)
            • +1
              Спасибо за информацию. Видел Ваш профайл на elanсe. А еще такие моменты интересны — железо (мак мини, хакинтош и т.п.), девайсы (iPhone, iPad и т.п.), Apple Developer Program, сколько часов в неделю работаете, уровень английского (я надеюсь не с СНГ работаете), сколько уходит времени на поиск заказчиков?
              • 0
                Macbook Air 2012, iPhone 5, iPad Mini. Лицензия на разработку есть. Работаю примерно 25 часов в неделю, час стоит $20. Уровень английского оцениваю как высокий, учусь в UBC. На поиск заказчиков уходит 1-2 дня.
                • +1
                  Бесценно! Спасибо еще раз. Air хватает? Я думал подтармаживать будет…
                  • 0
                    Подтормаживает иногда, но утилита «Free Memory» спасает.
                    • +1
                      Мне в последнее время стало казаться, что это как раз из-за нее подтормаживает, поэтому благополучно снес ее. Вроде лучше стало, но не существенно.
                      • 0
                        Подтормаживает обычно только тогда, когда эта утилита чистит память :)
                        При работе с iOS симулятором — очень спасает
          • +2
            Поделись, какие фреймворки используешь, кроме cocos2d и box2d если используешь их )

            Можешь написать о сроке реализации своих проектов?

            Тоже хотел фрилансить, но пока боюсь ) с одной стороны в фрилансе свобода, с другой стороны, работая на дядю есть стабильная зарплата.
            • +2
              Работая на дядю есть и «стабильный пендаль», которого нету когда работаешь дома. Для меня вот это более важно. :)
              • 0
                Какие придется, такие и использую :) А так, разобраться в различных фреймворках типа PSPDFKit или Phonegap не состовляет трудности.
                Срок реализации зависит от задачи. Я сам себе дядя с пендалем
              • +1
                Хороший пост, жду продолжения :)
                • 0
                  Скажите, а вы слышали о такой вещи как Объектно-Ориентированное Программирование?
                  • +3
                    Любитель религиозных войн?
                    • +2
                      Нет, не любитель. Просто достали такие советы, люди на таком учатся, потом пишут проекты в этом стиле, а эти проекты приходится поддерживать, и это очень сильно напрягает.
                    • +1
                      Можете указать на ошибки в моем ООП-фу?
                      • 0
                        Все ваши магические строки прекрасно прячутся в *.m файл класса в котором инкапсулируется вся логика (чтение, запись, etc.)
                        Кроме того, не понимаю зачем использовать словари для хранения настроек, если можно создать полноценный класс, с пачкой методов, а внутри спрятать логику, опять таки. Может быть в будущем вы решитесь использовать NSUserDefaults, вместо велосипеда с файлами.
                        Singleton — это скорее антипаттерн, чем паттерн приносящий пользу. Какой смысл создавать объекты которые используются пару раз, а висят в памяти постоянно? Если вам нужно из сотни мест достучаться до одного и того же объекта, то возможно у вас косяк в архитектуре.
                        Ну и дефайны. Зачем? Чем вас не устраивают константы?
                        • 0
                          Учту, спасибо.

                          1. NSUserDefaults использую обычно, но здесь была просто прихоть клиента — использовать plist файлы. Плюс любую настройку клиент мог сам изменить в папке документов приложения.
                          2. Ох уж этот постоянный холливар насчет синглтонов :) иногда это хорошее решение для архитектуры, иногда нет.
                          3. Дефайны, константы, компилятору же все равно, разве нет? Поправьте, если я не прав. А клиенту проще видеть привычные #define в своем проекте.
                          4. Магические строки использованы исключительно для упрощения примеров. При желании, программист сам от них избавится.
                          • 0
                            1. Можно использовать [NSUserDefaults registerDefaults:[NSDictionary dictionaryWithContentsOfURL:configurationSettingsFileURL]]
                            3. define можно передефайнить или сделать undef, а константу — нет
                            • 0
                              1. Спасибо, не знал.
                              3. В том то и дело, что дефайны можно передефайнить. Скажем, захочет клиент где-то передефайнить только в одном месте — берет и передефайнивает (гипотетически). Бывает по-разному, когда-то нужно использовать дефайны, а когда-то константы
                    • +1
                      Хорошо, а если более предметно? Что именно в коде не так?
                      • +3
                        Как фрилансер, думаю сюда надо добавить ещё CocoaPods, очень удобная вещь во многих смыслах и, в частности, в вопросе переиспользумого кода.
                        • 0
                          С CocoaPods еще не работал, поэтому не могу добавить ее в свои статьи :( Но вы можете написать свою статью!
                        • 0
                          [removed double comment]

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.