АбраКодастр
23,2
рейтинг
31 августа 2013 в 21:00

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


Здравствуйте, дорогие читатели Хабра!
Эта статья — вторая часть серии «Записки 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

Разберемся с кастомизируемыми всплывающими окнами в конце статьи, так как это довольно обширная тема. А пока что — все остальное.

Как использовать Modal Segue в Navigation Controller


И так, перед вами стоит проблема: клиенту не нравится стандартный переход Navigation Controller'а — «нахлест справа» — и он хочет, чтобы новый экран, например, появлялся «поворотом экрана снизу». Сразу видим вариант решения задачи: поменять вид Segue на Modal. Все было бы хорошо, да вот только логика Navigation Controller'a с его иерархией экранов нарушается; и как следствие, приложение вылетает. То есть теперь нам нужна Modal Segue, но с функциями Push Segue. Подозреваю, что есть способы попроще решить эту задачу, но я предлагаю просто написать подкласс UIStoryboardSegue. Единственное, что нам предстоит поменять, это .m файл нашего класса. А точнее, метод perform:

Жми меня!
- (void) perform{
    // Получаем экраны, с которыми будем работать
    UIViewController *src = (UIViewController *) self.sourceViewController;
    UIViewController *dst = (UIViewController *) self.destinationViewController;

   // Осуществляем простой переход
    [UIView transitionFromView:src.view
                        toView:dst.view
                      duration:1
                       options:UIViewAnimationOptionTransitionFlipFromBottom
                    completion:nil];

    // Осуществляем переход для Navigation Controller'a
    [UIView transitionFromView:src.navigationItem.titleView
                        toView:dst.navigationItem.titleView
                      duration:1
                       options:UIViewAnimationOptionTransitionFlipFromBottom
                    completion:nil];

    // Добавляем Push нашей Segue
    [src.navigationController pushViewController:dst animated:NO];
}

Вместо UIViewAnimationOptionTransitionFlipFromBottom можно поставить любой близкий сердцу вашего клиента стиль перехода.
Вот и все! Как просто, скажете вы. Теперь мы можем указать для любой Storyboard Segue стиль Custom, указать наш новый класс и заполучить Navigation Controller Segue со своим типом перехода.

Core Graphics


В один прекрасный момент вашему клиенту надоело пинать дизайнера каждый раз, когда нужно изменить ширину кнопки на 5 пикселов (ведь для каждой кнопочки дизайнер нарисовал отдельную картинку). Сделаем собственную кнопку с закругленными краями и и рамкой вокруг при помощи QuartzCore.framework. Опять же, как и с синглтоном, это скорее сниппет, ускоряющий работу над проектом.

Вообще-то данный подход можно использовать с любым подклассом UIView (UIButton как-раз им и является). Мы переписываем метод awakeFromNib у нашего UIView:

- (void)awakeFromNib {
    [super awakeFromNib];
    
    self.layer.cornerRadius = 5.0f;
    self.layer.masksToBounds = YES;

    self.layer.borderColor = [UIColor whiteColor].CGColor;
    self.layer.borderWidth = 1.0f;
}

Все просто. Во-первых, при наследовании нам нужно выполнить наш код после того, как суперкласс закончит свою работу. Так что мы вызываем тот же метод, но у суперкласса. Во-вторых, мы задаем радиус закругления углов у слоя нашего вида и заставляем слой подчиняться указанной маске. В-третьих, мы задаем цвет рамки (CGColor, конечно) и ее толщину.
Я знаю, что некоторые ожидали глубокой работы с графическим контекстом, но на то они и быстрые шпаргалки фрилансера — когда появится новая задача с Core Graphics, тогда и будем писать статьи.

Работа с UIWebView и ScrollView


У нас уже есть UIWebView, давайте подгрузим в него контент:

NSString *htmlString;
NSString *cssString;

<...Инициализируем строки...>

htmlString = [NSString stringWithFormat:@"<style>%@</style>%@", cssString, htmlString];
NSURL *url = [[NSURL alloc] initFileURLWithPath:pathToApplicationDirectory];

[self hideOverscrollShadowsForWebView:webView];
    
[webView loadHTMLString:htmlString baseURL:url];

Просто получили веб страничку с нашим стилем и html. А чтобы скрыть неприятные серые тени при оверскроле UIWebView мы применили следующий метод:

/*!
 Метод, прячущий невероятно ужасные и некрасивые тени у данного UIWebView
 \param webView Собственно сам WebView, который нужно модифицировать
 */
- (void)hideOverscrollShadowsForWebView:(UIWebView *)webView {
    id scrollview = [webView.subviews objectAtIndex:0];
    for (UIView *subview in [scrollview subviews])
        if ([subview isKindOfClass:[UIImageView class]])
            subview.hidden = YES;
    
    webView.backgroundColor = [UIColor clearColor];
}

Мы прячем все дочерние виды у Scrollview, принадлежащего нашему UIWebView. А так же устанавливаем прозрачный задний фон.

О UIScrollView можно сказать только одно: никогда не забывайте устанавливать свойство contentSize, и будет вам счастье.

Собственные всплывающие окна (Popups)


Пришло время поговорить о гиганте этой статьи. В этот раз клиенту потребовалась возможность быстро и без вашего участия менять вид всплывающих окон. Что же, за дело!

Давайте поговорим немного о теории, о том, как мы все провернем.
В Storyboard у нас вряд ли получится реализовать полноценное всплывающее окно, а вот создать отдельный файл PopupView.xib мы можем!
А в качестве модели мы создадим три класса PopupView, PopupController, PopupControllerDelegate.

Сначала соберите макет своего всплывающего окна как на скриншоте. Учтите, что File's Owner в нашем случае будет объектом класса PopupController, а сам View будет объектом класса PopupView. Background View — это полупрозрачный светло-серый UIView.



Посмотрим сначала на реализацию PopupView.h:

Жми меня!
#import <UIKit/UIKit.h>
@interface PopupView : UIView

@property (strong, nonatomic) IBOutlet UIView *backgroundView;
@property (strong, nonatomic) IBOutlet UIView *innerPopupView;
@property (strong, nonatomic) IBOutlet UILabel *popupTitleLabel;
@property (strong, nonatomic) IBOutlet UILabel *popupTextLabel;
@property (strong, nonatomic) IBOutlet UIButton *popupButton;

@end

Мы просто зацепили все элементы пользовательского интерфейса в код; PopupView.m мы не изменяли, оставили стандартный сгенерированный код.
У нас есть шаблон всплывающего окна, теперь нам нужно умело его использовать. Создаем PopupController.

PopupController.h:

Жми меня!
#import <Foundation/Foundation.h>
#import "PopupControllerDelegate.h"
#import "PopupView.h"

@interface PopupController : NSObject

// UIViewController, который и будет разбираться с действиями в PopupView
@property (strong, nonatomic) UIViewController<PopupControllerDelegate> *delegate;
// Массив активных всплывающих окон. Их может быть несколько сразу!
@property (strong, nonatomic) NSMutableArray *activePopups;

- (IBAction)touchedButton:(UIButton *)sender;

- (id)initWithDelegate:(UIViewController<PopupControllerDelegate> *)delegate;
- (void)showHelloWorldPopup;
- (void)dismissAllPopups;

@end

Все что мы сделали — это установили нужные нам свойства и публичные методы инициализации, показа простенького всплывающего окна, закрытия всех всплывающих окон. А еще добавили небольшой Action для кнопочки внутри всплывающего окна в PopupController.

PopupController.m:

Жми меня!
#import "PopupController.h"

@implementation PopupController

- (id)initWithDelegate:(UIViewController<PopupControllerDelegate> *)delegate {
    self = [super init];
    if (self) {
        // Инициализируем массив всплывающих окон
        self.activePopups = [NSMutableArray array];
        
        // Установим себе делегата
        self.delegate = delegate;
    }
    return self;
}

- (void)showHelloWorldPopup {
    PopupView *popup = [self popupFromRestorationID:@"text"];
    [self configurePopup:popup];
    [self showPopup:popup];
}

- (IBAction)touchedButton:(UIButton *)sender {
    [self.delegate touchedPopupButton:sender];
}

- (void)dismissAllPopups {
    for (UIView *popup in activePopups) {
        [self hidePopup:popup];
    }
}
<...>

Во время инициализации мы сразу устанавливаем делегат нашего класса.
Код метода showHelloWorldPopup я разделил на три других метода для облегчения чтения: мы инициализируем всплывающее окно, настраиваем его и показываем.
Когда пользователь дотрагивается до кнопки на всплывающем окне, мы уведомляем об этом наш делегат.
Метод dismissAllPopups просто проходится по всем всплывающим окнам в общем массиве и закрывает каждое.
Допищем недостающие методы:

Жми меня!
<...>
- (PopupView *)popupFromRestorationID:(NSString *)restorationID {
    // Заполучаем все виды из нашего .xib файла
    NSArray *allViews = [[NSBundle mainBundle] loadNibNamed:@"PopupView.xib" owner:self options:nil];

    // Пройдемся по всем видам
    for (PopupView *view in allViews) {
        // Если restorationIdentifier тот, что нам нужен, то возвращаем окно, делаем его прозрачным и добавляем к делегату
        if ([view.restorationIdentifier isEqualToString:restorationID]) {
            view.alpha = 0.0f;
            [self.delegate.view addSubview:view];
            return view;
        }
    }
    // Не нашли окно! Вернем пустоту
    return nil;
}

- (void)showPopup:(PopupView *)popup {
    // Уменьшим innerPopupView до 50%
    [popup.innerPopupView setTransform:CGAffineTransformMakeScale(0.5, 0.5)];

    // Начинаем анимацию
    [UIView animateWithDuration:0.2f
                     animations:^{
                         // Возвращаем видимость всплывающего окна
                         popup.alpha = 1.0f; 

                         // Возвращаем размер всплывающему окну до 100%
                         [popup.innerPopupView setTransform:CGAffineTransformMakeScale(1.0, 1.0)];
                     }];
    
    // Добавим селектор нажатию на задний фон
    [popup.backgroundView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissAllPopups)]];
    
    // Добавим всплывающее окно в массив всплывающих окон
    [activePopups addObject:popup];
}

- (void)hidePopup:(UIView *)popup {
    // Начинаем анимацию
    [UIView animateWithDuration:0.2f
                     animations:^{
                         // Делаем всплывающее окно прозрачным
                         popup.alpha = 0.0f;
                     }
                     completion:^(BOOL finished){
                         // Удаляем всплывающее окно из нашего ViewController'a
                         [popup removeFromSuperview];
                     }];
    
    // Удаляем последний указатель на всплывающее окно из массива, автоматически выгружаем из памяти
    [activePopups removeObject:popup];
}

- (void)configurePopup:(PopupView *)popup forName:(NSString *)name {
    // Установим заголовок
    popup.popupTitleLabel.text = @"Popup Title";
    
    // Вставим текст
    popup.popupTextLabel.text = @"Hello World!";

    // Установим текст для кнопки
    [self setTitle:@"Okay"];
}

- (void)setTitle:(NSString *)title forButton:(UIButton *)button {
    [button setTitle:title forState:UIControlStateNormal];
    [button setTitle:title forState:UIControlStateSelected];
    [button setTitle:title forState:UIControlStateHighlighted];
    [button setTitle:title forState:UIControlStateDisabled];
}

@end

Все хитрости поочередно:

  1. Анимации при помощи UIView: все, что написано в блоке animations:, будет постепенно выполняться в течение установленного времени. Код из блока completion: выполнится мгновенно, сразу после блока animations:
  2. Когда пользователь нажимает на серое затемнение, всплывающее окно сразу исчезает
  3. Если просто установить текст кнопки для одного состояния, то для других текст не изменится, поэтому метод setTitle: forButton: меняет текст кнопки сразу для всех состояний

Создав подобную архитектуру всплывающих окон, мы даем возможность клиенту менять внешний вид окон без нашего участия. Пускай клиент меняет цвет, размер, расположение элементов — пока он не трогает связи с кодом, все будет работать, как надо.

Ну, и на последок, покажу, как работает наш класс. Допишем PopupControllerDelegate.h:

#import <Foundation/Foundation.h>
@protocol PopupControllerDelegate

@required
- (void)touchedPopupButton:(UIButton *)sender;

@end

Любой UIViewController, на котором может появиться всплывающее окно, должен иметь возможность обрабатывать события этого окна, отвечать на протокол PopupControllerDelegate.

Например, мы хотим показать HelloWorld окошко после загрузки нашего вида, который отвечает нужному протоколу и у которого уже есть инициализированный объект popupController. Добавим следующий код в viewDidAppear:

[popupController showHelloWorldPopup];

И добавим обработчик события всплывающего окна:

- (void)touchedPopupButton:(UIButton *)sender {
    // Просто закроем все всплывающие окна
    [popupController dismissAllPopups];
}

Заключение


Спасибо за то, что дочитали до конца!

Вторую часть статьи написал в аэропорту Домодедово перед вылетом в Ванкувер. Уже поздняя ночь, у меня кончается кофе. Прошу понять простить, и если нашли какую-либо опечатку\неточность в тексте\коде, пожалуйста, сообщите в мой хабрацентр, не спешите ставить минус или писать гневный комментарий.

В следующих статьях будут темы еще интереснее: расшаривание контента в социальных сетях, PSPDFKit, Push Notifications.
Nikita Kolmogorov @backmeupplz
карма
14,0
рейтинг 23,2
АбраКодастр
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (31)

  • 0
    Судя по всему, людям не нравится эта серия статей. Похоже, пора заканчивать :( а то ни кармы, ни времени на эти статьи не наберешься :(
    • +1
      Простите, но пока что это похоже на сборник вредных советов.
      drawRect: предназначен для рисования, и использовать его для других целей слегка алогично, для вашей задачи больше подошел бы init* или awakeFromNib.
      Бегать по сабвью тоже не самый лучший вариант, т.к. структура контрола от версии к версии может меняться, и при обновлении могут вылезти удивительные артефакты.
      • 0
        С drawRect: согласен, это плохо получилось.
        А можете подсказать способ получше, дабы выудить определенный UIView из xib файла?
        • 0
          Я не до конца понял проблему которую вы решаете, возможно лучшего варианта и нет, но это жесткий хак, и по возможности таких решений нужно избегать.
          • 0
            По возможности нужно избегать, да. Но иногда из-за прихотей клиента приходится идти на такие уступки
      • 0
        Исправил часть с drawRect:, теперь используется awakeFromNib, спасибо.
        Возможно, я что-нибудь еще упустил
  • +4
    и надо было написать не «записки фрилансера», а «записки разработчика под IOS» тогда уже, ибо лично я шел посмотреть, например, что нового во фрилансе я могу открыть для себя со стороны «надоело в офисе», а не мануал по разработке под неинтересную для меня платформу.
    • –1
      Может вы и правы. Хотя я фрилансер, а это мои записки. Похоже, если бы я разрабатывал на python или js, меня бы восприняли по-другому :(
      Да и в первой части из серии я написал, почему это именно «Записки Фрилансера» и подробно описал, что занимаюсь Objective-C разработкой
      • +1
        Я хотел сделать ударение на том, что «фрилансер» далеко не только разработчик под iOS. У меня вот больше половины знакомых фрилансеров, но IOS'ом занимаются единицы. Так что серию статей стоит переименовать и уточнить что разработка под IOS, а не фриланс как таковой в независимости от направленности.
        • –1
          А я хотел сделать ударение на том, что фрилансер-разработчик под iOS — это тоже фрилансер. Про iOS я уточнил в первой части серии, в первом абзаце.
      • +1
        ну а я «Близнецы» и это мой комментарий.
    • +1
      Так уж и быть :) поменял название. Спасибо за совет!
      • 0
        В таком случае уберите и картинку, вводящую в заблуждение, что пост о фрилансе.
  • +2
    Аутлиты в PopupView.h нарушают инкапсуляцию, лучше бы там оставить property для всех title типа NSString
    вызывать setTitle:forState: достаточно только для UIControlStateNormal

    Я что-то не очень понял момент про restorationID, мне показалось, что он используется не по назначению. Имхо можно было завести аутлит на popup в контроллере и просто загрузить нужный bundle.

    PopupController.m

    @ interface PopupController()
    @ property (nonatomic, strong) PopupView *popupView;
    @ end

    //…
    @implementation PopupController

    — (PopupView *)popupView
    {
    [[NSBundle mainBundle] loadNibNamed:@«PopupView.xib» owner:self options:nil];
    PopupView *popup = self.popupView;
    self.popupView = nil;
    return popup;
    }

    @ end

    аутлит заполнится, мы берем из него view, заниливаем аутлит для последующих загрузок и возвращаем popup.

    ps. почему-то не получается заюзать тег source
    • 0
      Аутлиты в PopupView.h нарушают инкапсуляцию, лучше бы там оставить property для всех title типа NSString

      Всеми руками за! Немножко упростил код для показательности.

      вызывать setTitle:forState: достаточно только для UIControlStateNormal

      Это правдиво, если клиент не захочет случайно поменять текст кнопки в Storyboard для состояния, скажем, Selected. Я уже привык такое перепроверять и предугадывать заезды клиентов :)

      Я что-то не очень понял момент про restorationID, мне показалось, что он используется не по назначению. Имхо можно было завести аутлит на popup в контроллере и просто загрузить нужный bundle.

      Позже можно добавлять другие типы всплывающих окон (например, с двумя кнопками) в тот же самый xib файл, но с другими restorationIdentifier.

      Спасибо за комментарий!
      • +1
        >> Позже можно добавлять другие типы всплывающих окон (например, с двумя кнопками) в тот же самый xib файл, но с другими restorationIdentifier

        В таком случае я бы всё равно сделал несколько аутлитов, всё равно все вьюхи будут загружены. А так можно будет избавиться от строковых идентификаторов, которые постоянно норовят перестать совпадать.
        • 0
          Хорошая идея, спасибо! Это вариант решения проблемы с меньшим количеством магических строк и без перебора вьюх внутри контроллера.
          Учту.
      • +1
        Вы уверены, что делегат должен быть strong? Это же 99% приведет к reference cycle (контроллер ссылается на вид, вид на контроллер). По идее, здесь нужен weak.
        @property (strong, nonatomic) UIViewController *delegate;
        • 0
          Конечно же, weak :) это мое упущение
    • +1
      на всякий случай поясню, что до вызова [[NSBundle mainBundle] loadNibNamed:...]; self.popupView был nil, а после загрузки бандла аутлит заполнится загруженной вьюхой
  • +2
    Из документации:
    The UIWebView class should not be subclassed.
    Игнорирование этого ограничения может привести к отклонению приложения. Не нужно давать вредных советов.
    • 0
      Спасибо! Поправил в статье
  • +1
    Не останавливайтесь, очень полезно как раз для разбирающихся в Objective C.

    Нет планов какой-нибудь свой блог вести, я бы с удовольствием читал бы.
  • +1
    То что у Вас называется Core Graphics на самом деле самый настоящий Core Animation.
    • 0
      Но CGColor-то точно из раздела Core Graphics :)
  • 0
    Ну и еще сильная ссылка на delegate — нужно объяснить, почему она сильная и почему это не приведет к retain cycles.

    Анимации при помощи UIView: все, что написано в блоке animations:, будет постепенно выполняться в течение установленного времени.


    АААээээээ. Код в блоке animations выполнится мгновенно, только вот его выполнение приведет к созданию implicit animations, которые как раз и будут выполняться заданное время.
    • –1
      1. Я писал, что вдаваться в детали особо не буду, это серия статей о том, как быстро закончить заказ, а не как его полностью понять :) Про retain cycles уже написано достаточно литературы
      2. Я упростил объяснение с уровня «знаю, как» до уровня «знаю, что»
      • +2
        Т.е. посты о том как схалтурить и сделать г***о на выходе? Очень полезное чтиво, да.
        Жгите еще.
        • –1
          Не иронизируйте так сильно. Эта серия статей нужна, чтобы дать идею реализации механизмов разработчикам. Когда будет нужно, программист сам разберется в используемых технологиях.
          Когда вы читаете туториал о синтаксисе языка, я не думаю, что вы ожидаете объяснения, как происходят арифметические операции на низком уровне.
  • +1
    Как насчет продолжения? Очень ждем, интересные темы.
    • 0
      Полностью поддерживаю!

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