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

в 17:00, , рубрики: iOS, objective-c, все читают теги, как быстро поднять, разработка под iOS, фриланс, шаблоны, шпаргалка, метки: , , , , , ,
Записки 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

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

Update: Господа минусующие! Пожалуйста, напишите в комментарии к статье, за что пошел минус.

Как использовать 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];
    
[webView loadHTMLString:htmlString baseURL:url];

Просто получили веб страничку с нашим стилем и html. Но какая неприятность! При попытке прокрутить страницу вниз, если включено «отпрыгивание», то при слишком сильном скроллинге, мы увидим серые (очень некрасивые!) поля оверскролла. Я предлагаю унаследовать оригинальный UIWebView и слегка изменить методы инициализации:

Жми меня!

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self hideOverscrollShadowsForWebView:self];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self hideOverscrollShadowsForWebView:self];
    }
    return self;
}

Нужно переписать оба метода инициализации, потому что в своем коде вы скорее будете использовать метод initWithFrame:; в то время, как UIWebView из Stroryboard будет вызывать initWithCoder:.
И, конечно же, сам метод, прячущий неприятные глазу серые тени:

/*!
 Метод, прячущий невероятно ужасные и некрасивые тени у данного 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.

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

Посмотрим сначала на реализацию 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.

Автор: backmeupplz

Источник

Поделиться