- PVSM.RU - https://www.pvsm.ru -

Как написать свой компонент для iOS

image

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

Apple предоставляет в распоряжение разработчика около 20 различных UI-компонентов, в том числе UITextField, UIButton и UISwitch. Используя всю мощь стандартных элементов управления, можно создать множество различных вариантов интерфейса. Как бы то ни было, иногда возникает необходимость в чем-то, что реализовать силами стандартных компонентов невозможно.

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

image

Хоть такой вариант и будет работать, он не очень хорошо иллюстрирует понятие ценового диапазона. Гораздо лучше будет использовать один слайдер с двумя ползунками, отвечающими за минимум и максимум:

image

Такой элемент гораздо удобнее в использовании, пользователь сразу же понимает, что он задает диапазон значений, а не фиксированные числа. К сожалению, такой слайдер не входит в набор стандартных элементов. Чтобы реализовать его функционал, необходимо разработать кастомный элемент управления.

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

Как уже говорилось, кастомные компоненты — это элементы управления, созданные вами, они не являются частью UIKit Framework. Также, как и стандартные компоненты, они должны быть полностью универсальными и настраиваемыми под любые нужды. За последнее время сформировалось целое сообщество разработчиков, выкладывающих в открытый доступ свои компоненты.

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

Итак, достаточно теории! Пора начинать кастомизировать!

Начало

Этот раздел посвящен разработке основной структуры компонента, которой будет достаточно для вывода простого слайдера на экран. Запустите Xcode и выберите File -> New -> Project. В появившемся окне выберите iOS -> Application -> Single View Application и нажмите Next. На следующем экране в качестве имени проекта введите CERangeSlider и заполните остальные поля:

image

В этом проекте Storyboards не используются, так как оперируем мы всего одним экраном. В качестве префикса класса вы можете использовать любой другой — главное, не забывайте, что соответствующие изменения произойдут и в коде приложения. В поля «Organization Name» и «Company Identifier» можете вписать собственные значения. Когда закончите, нажмите Next. Выберите место хранения проекта и нажмите Create.

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

Если вы внимательно посмотрите на различные компоненты Apple UIKit, вы заметите, что множество элементов, таких как UILabel и UIWebView напрямую наследуют UIView. Но, как бы то ни было, есть и такие элементы, которые наследуют UIControl, как показано на этом рисунке:

image

Примечание: С подробной иерархией элементов интерфейса можно ознакомиться здесь: UIKit Framework Reference [1].

Класс UIControl реализует шаблон Target-Action, по сути являющийся способом уведомления об изменениях компонента. Также у UIControl есть несколько свойств, относящихся к контролю состояния объекта. Для создания нашего компонента вы будете использовать именно такой шаблон, так что UIControl послужит отличным стартом.

Кликните правой кнопкой на группе CERangeSlider в Project Navigator и выберите New File, затем iOS -> Cocoa Touch -> Objective-C class и нажмите Next. Назовите класс CERangeSlider, а в поле «subclass of» введите UIControl. Нажмите Next и Create для выбора места хранения класса.

Хотя написание кода само по себе является приятным процессом, вы скорее всего захотите наблюдать за тем, как элемент будет отображаться на экране. До того, как начать писать код, добавьте ваш свой компонент на View Controller.

Откройте CEViewController.m и вставьте следующую строку:

#import "CERangeSlider.h"

Далее в этом же файле добавьте переменную:

@implementation CEViewController
{
    CERangeSlider* _rangeSlider;
}

Замените стандартный viewDidLoad следующим блоком кода:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Do any additional setup after loading the view, typically from a nib.
 
    NSUInteger margin = 20;
    CGRect sliderFrame = CGRectMake(margin, margin, self.view.frame.size.width - margin * 2, 30);
    _rangeSlider = [[CERangeSlider alloc] initWithFrame:sliderFrame];
    _rangeSlider.backgroundColor = [UIColor redColor];
 
    [self.view addSubview:_rangeSlider];
}

Этот код создает объект вашего класса с требуемыми размерами и добавляет его на экран. Фоновый цвет компонента задан красным, чтобы его было заметно на основном фоне приложения. Если цвет не указывать, компонент останется прозрачным, и вы будете долго ломать голову над тем, куда он пропал. :]

Соберите и запустите приложение; вы должны будете увидеть следующий экран:

image

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

Примечание: API вашего компонента определяет методы и свойства, которые вы собираетесь предоставить другим разработчикам. Далее в статье вы прочитаете о структуре API — а пока что оставайтесь на связи!

Добавляем стандартные свойства компонента

Откройте CERangeSlider.h и добавьте следующие свойства между @interface и @end:

@property (nonatomic) float maximumValue;
@property (nonatomic) float minimumValue;
@property (nonatomic) float upperValue;
@property (nonatomic) float lowerValue;

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

Хорошо спроектированные элементы управления должны содержать стандартные настройки, иначе при отрисовке на экране они будут выглядеть несколько странно. Откройте CERangeSlider.m, найдите метод initWithFrame:, сгенерированный XCode, и замените его следующим кодом:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        _maximumValue = 10.0;
        _minimumValue = 0.0;
        _upperValue = 8.0;
        _lowerValue = 2.0;
    }
    return self;
}

Теперь пришло время поработать с интерактивными элементами компонента: ползунками и полоской прогресса, на которой они находятся.

Изображения vs. CoreGraphics

Есть два способа отображения элементов на экране:

  1. Использовать в качестве составных частей компонента изображения,
  2. Использовать комбинацию слоев и Core Graphics.

У каждого из способов есть свои плюсы и минусы.

  • Использовать изображения — самый простой вариант, по крайней мере, если вы умеете рисовать. Если вы предполагаете дать возможность другим разработчикам заменять эти изображения своими, то их можно задать как свойства UIImage.
  • Использование изображений обеспечивает компоненту достаточную гибкость. Сторонние разработчики могут изменять все, вплоть до каждого пикселя — но с другой стороны это требует определенных навыков рисования — а изменить что-то через код весьма затруднительно.

За отображение компонента средствами Core Graphics полностью отвечает код приложения, в следствие чего от программиста требуется значительно больше усилий. Но зато такой способ позволяет построить более гибкий API. Используя Core Graphics, вы можете задать практически любое свойство компонента — его цвета, толщину, искривление — да и вообще, любой параметр, отвечающий за визуализацию. Такой подход позволяет разработчикам, использующим ваш элемент управления, легко подстраивать его под свои нужды.

В этом уроке мы будем использовать второй подход — Core Graphics.

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

В Xcode перейдите в окно настроек проекта. Затем выберите вкладку Build Phases и секцию Link Binary With Libraries. Добавьте в список QuartzCore.framework. Классы и методы этого фреймворка будут использованы для ручной отрисовки компонента.

Этот скриншот наглядно показывает, как найти и добавить QuartzCore.framework, если вы запутались:

image

Откройте CERangeSlider.m и добавьте следующую строчку:

#import <QuartzCore/QuartzCore.h>

Добавьте следующие переменные в этот же файл, сразу после @implementation:

@implementation CERangeSlider
{
    CALayer* _trackLayer;
    CALayer* _upperKnobLayer;
    CALayer* _lowerKnobLayer;
 
    float _knobWidth;
    float _useableTrackLength;
}

Эти три слоя — _trackLayer, _upperKnobLayer и _lowerKnobLayer будут использованы для отображения различных элементов вашего компонента. Две переменные _knobWidth и _useableTrackLength используются для задания параметров этих элементов.

В CERangeSlider.m найдите метод initWithFrame: и добавьте следующий код в блок if (self) { }:

_trackLayer = [CALayer layer];
_trackLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layer addSublayer:_trackLayer];
 
_upperKnobLayer = [CALayer layer];
_upperKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_upperKnobLayer];
 
_lowerKnobLayer = [CALayer layer];
_lowerKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_lowerKnobLayer];
 
[self setLayerFrames];

Этот блок кода создает три слоя и добавляет их к основному в качестве дочерних. Теперь в этом же файле добавьте следующие методы:

- (void) setLayerFrames
{
    _trackLayer.frame = CGRectInset(self.bounds, 0, self.bounds.size.height / 3.5);
    [_trackLayer setNeedsDisplay];
 
    _knobWidth = self.bounds.size.height;
    _useableTrackLength = self.bounds.size.width - _knobWidth;
 
    float upperKnobCentre = [self positionForValue:_upperValue];
    _upperKnobLayer.frame = CGRectMake(upperKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);
 
    float lowerKnobCentre = [self positionForValue:_lowerValue];
    _lowerKnobLayer.frame = CGRectMake(lowerKnobCentre - _knobWidth / 2, 0, _knobWidth, _knobWidth);
 
    [_upperKnobLayer setNeedsDisplay];
    [_lowerKnobLayer setNeedsDisplay];
}
 
- (float) positionForValue:(float)value
{
    return _useableTrackLength * (value - _minimumValue) /
        (_maximumValue - _minimumValue) + (_knobWidth / 2);
}

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

Скомпилируйте и запустите приложение. Ваш слайдер начинает принимать форму! Он должен выглядеть примерно так:

image

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

Добавляем компоненту интерактивности

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

Добавьте новый файл в группу CERangeSlider: New File -> iOS -> Cocoa Touch -> Objective-C class, сделайте его сабклассом CALayer и назовите CERangeSliderKnobLayer.

Откройте только что созданный CERangeSliderKnobLayer.h и замените его содержимое следующим:

#import <QuartzCore/QuartzCore.h>
 
@class CERangeSlider;
 
@interface CERangeSliderKnobLayer : CALayer
 
@property BOOL highlighted;
@property (weak) CERangeSlider* slider;
 
@end

Этот код добавляет два свойства, одно из которых указывает, подсвечен ли ползунок, а другое указывает на родительский слайдер. Теперь откройте CERangeSliderKnobLayer.m и добавьте #import:

#import "CERangeSliderKnobLayer.h"

Затем измените тип _upperKnobLayer и _lowerKnobLayer в блоке @implementation:

CERangeSliderKnobLayer* _upperKnobLayer;
CERangeSliderKnobLayer* _lowerKnobLayer;

Эти слои теперь являются объектами только что созданного класса CERangeSliderKnobLayer.

В том же CERangeSlider.m найдите метод initWithFrame: и замените код инициализации upperKnobLayer и lowerKnobLayer на следующий блок кода:

_upperKnobLayer = [CERangeSliderKnobLayer layer];
_upperKnobLayer.slider = self;
_upperKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_upperKnobLayer];
 
_lowerKnobLayer = [CERangeSliderKnobLayer layer];
_lowerKnobLayer.slider = self;
_lowerKnobLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.layer addSublayer:_lowerKnobLayer];

Этот код использует только что созданный нами класс для инициализации слоев и устанавливает их свойство slider в значение self. Запустите свой проект и удостоверьтесь, что все выглядит так же, как на скриншоте:

image

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

Добавляем обработчики нажатий

Откройте CERangeSlider.m и добавьте следующий код под блоком объявления переменных:

CGPoint _previousTouchPoint;

Эта переменная будет использована для отслеживания координат нажатий. А как вы планируете отслеживать различные события нажатия и отжатия для вашего компонента?

UIControl предоставляет несколько методов для отслеживания нажатий. Сабклассы UIControl могут переопределять эти методы для реализации своей собственной логики. В вашем элементе управления вы переопределите следующие три метода: beginTrackingWithTouch, continueTrackingWithTouch и endTrackingWithTouch.

Добавьте следующий метод в CERangeSlider.m:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    _previousTouchPoint = [touch locationInView:self];
 
    // hit test the knob layers
    if(CGRectContainsPoint(_lowerKnobLayer.frame, _previousTouchPoint))
    {
        _lowerKnobLayer.highlighted = YES;
        [_lowerKnobLayer setNeedsDisplay];
    }
    else if(CGRectContainsPoint(_upperKnobLayer.frame, _previousTouchPoint))
    {
        _upperKnobLayer.highlighted = YES;
        [_upperKnobLayer setNeedsDisplay];
    }
    return _upperKnobLayer.highlighted || _lowerKnobLayer.highlighted;
}

Этот метод вызывается, когда пользователь первый раз дотрагивается до компонента. Он переводит событие нажатия в координатную систему компонента. Затем он проверяет каждый ползунок с целью определить, попадает ли нажатие в рамки одного из них. В итоге метод информирует свой родительский класс о том, нужно ли отслеживать текущее нажатие.

Отслеживание нажатия продолжается, если один из ползунков подсвечен. Вызов метода setNeedsDisplay позволяет удостовериться в том, что слои были обновлены — далее вы поймете, почему это важно.

Теперь, когда у вас есть обработчик первого нажатия, нужно обрабатывать события при передвижении пальца пользователя по экрану. Добавьте следующий метод в CERangeSlider.m:

#define BOUND(VALUE, UPPER, LOWER)	MIN(MAX(VALUE, LOWER), UPPER)
 
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint touchPoint = [touch locationInView:self];
 
    // 1. determine by how much the user has dragged
    float delta = touchPoint.x - _previousTouchPoint.x;
    float valueDelta = (_maximumValue - _minimumValue) * delta / _useableTrackLength;
 
    _previousTouchPoint = touchPoint;
 
    // 2. update the values
    if (_lowerKnobLayer.highlighted)
    {
        _lowerValue += valueDelta;
        _lowerValue = BOUND(_lowerValue, _upperValue, _minimumValue);
    }
    if (_upperKnobLayer.highlighted)
    {
        _upperValue += valueDelta;
        _upperValue = BOUND(_upperValue, _maximumValue, _lowerValue);
    }
 
    // 3. Update the UI state
    [CATransaction begin];
    [CATransaction setDisableActions:YES] ;
 
    [self setLayerFrames];
 
    [CATransaction commit];
 
    return YES;
}

Проанализируем этот код, комментарий за комментарием:

  1. Сначала вы рассчитываете delta — количество пикселей на которые был передвинут палец. Затем вы конвертируете их в зависимости от минимального и максимального значений компонента.
  2. Здесь вы изменяете верхнюю и нижнюю границу в зависимости от того, куда пользователь передвинул ползунок. Заметьте, что вы используете макрос BOUND, который более удобен для чтения, нежели MIN/MAX.
  3. Этот блок кода устанавливает флаг disabledActions в CATransaction. Это позволяет удостовериться, что изменения границ каждого слоя применяются незамедлительно и не анимируются. В конце вызывается метод setLayerFrames, передвигающий ползунок в нужное место.

Вы реализовали перемещение ползунка — но вам все еще нужно обрабатывать окончание взаимодействия с компонентом. Добавьте следующий метод в CERangeSlider.m:

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    _lowerKnobLayer.highlighted = _upperKnobLayer.highlighted = NO;
    [_lowerKnobLayer setNeedsDisplay];
    [_upperKnobLayer setNeedsDisplay];
}

Этот код возвращает оба ползунка в неактивное состояние. Запустите свой проект и немного поиграйте со своим новым слайдером! Он должен выглядеть так же, как на скриншоте:

image

Вы можете заметить, что возможно передвинуть палец за границы компонента, а затем вернуться в его границы без прерывания исполнения метода. Это очень важное свойство юзабилити устройств с небольшими экранами и стилусами с низкой точностью (если вы не поняли, то речь идет о пальцах). :]

Уведомления об изменениях

Итак, у нас есть интерактивный элемент управления, с помощью которого пользователь может устанавливать верхнюю и нижнюю границы диапазона. Но как связать эти изменения с остальным кодом так, чтобы они могли использоваться приложением? Есть множество различных вариантов, которые можно использовать для уведомлений об изменениях: NSNotification, Key-Value-Observing (KVO), шаблон делегирования, шаблон Target-Action и другие.

Если вы посмотрите на компоненты UIKit, вы увидите, что они не используют NSNotification или KVO, так что для совместимости с UIKit мы должны отказаться от этих двух вариантов. Два других шаблона — делегирование и Target-Action — очень часто используются в UIKit.

Приведу детальный анализ обоих шаблонов:

  • Используя делегирование, вы создаете протокол, содержащий множество методов, используемых для разных уведомлений. У компонента есть свойство, обычно именуемое delegate, которое принимает любой класс, реализующий этот протокол. Классический пример — UITableView, предоставляющий протокол UITableViewDelegate. Обратите внимание, что элементы управления могут иметь только один объект delegate. Делегированный метод может принимать любое количество параметров, так что вы можете передавать столько информации, сколько потребуется.
  • Шаблон Target-Action реализуется базовым классом UIControl. При изменении состояния компонента цель (target) уведомляется о действии (action), которое описывается одним из enum значений UIControlEvents. Вы можете предоставлять множество целей для контроля всех действий, и, хотя существует возможность добавлять собственные события (UIControlEventApplicationReserver), их количество ограничено четырьмя. Эти действия не могут передавать какую-либо информацию вместе с событием. Следовательно, они не могут быть использованы для передачи дополнительных данных.

Основные различия между двумя шаблонами следующие:

  • Шаблон Target-Action широковещает о своих изменениях, в то время как делегирование ограничено единственным объектом.
  • Вы самостоятельно реализуете протоколы при использовании делегирования, то есть можете определить, какую конкретно информацию хотите передать. Target-Action же не дает возможности передачи дополнительных данных.

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

В этой ситуации использование шаблона Target-Action наиболее оправданно. Это и является одной из причин, по которым мы наследовали UIControl в самом начале урока.

Показания слайдера обновляются внутри метода continueTrackingWithTouch:withEvent:, так что именно здесь нужно реализовать механизм уведомлений. Откройте CERangeSlider.m, найдите метод continueTrackingWithTouch:withEvent и добавьте следующий код перед return YES:

[self sendActionsForControlEvents:UIControlEventValueChanged];

Это все, что от вас требуется для того, чтобы уведомить нужные цели об изменениях. Что ж, это было проще, чем ожидалось!

Откройте CEViewController.m и добавьте следующий код в конец метода viewDidLoad:

[_rangeSlider addTarget:self
                     action:@selector(slideValueChanged:)
           forControlEvents:UIControlEventValueChanged];

Этот код запускает метод slideValueChanged каждый раз, когда слайдер присылает событие UIControlEventValueChanged.

Теперь добавьте следующий метод в CEViewController.m:

- (void)slideValueChanged:(id)control
{
    NSLog(@"Slider value changed: (%.2f,%.2f)",
          _rangeSlider.lowerValue, _rangeSlider.upperValue);
}

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

image

Вы, наверное, уже устали от разноцветного интерфейса, похожего на ужасный фруктовый салат. Пора придать компоненту правильный внешний вид!

Изменяем внешний вид компонента с помощью Core Graphics

Для начала займемся обновлением внешнего вида полосы прогресса, по которой передвигаются ползунки. В группу CERangeSlider добавьте новый класс CERangeSliderTrackLayer, являющийся сабклассом CALayer.

Откройте CERangeSliderTrackLayer.h и замените его содержимое следующим:

#import <QuartzCore/QuartzCore.h>
 
@class CERangeSlider;
 
@interface CERangeSliderTrackLayer : CALayer
 
@property (weak) CERangeSlider* slider;
 
@end

Этот код добавляет ссылку на слайдер по образцу того, что мы делали для слоя ползунка. Откройте CERangeSlider.m и добавьте следующий #import:

#import "CERangeSliderTrackLayer.h"

Чуть ниже найдите переменную _trackLayer и измените ее тип на только что созданный класс:

CERangeSliderTrackLayer* _trackLayer;

Теперь найдите метод initWithFrame: и обновите код создания слоя:

_trackLayer = [CERangeSliderTrackLayer layer];
_trackLayer.slider = self;
[self.layer addSublayer:_trackLayer];
 
_upperKnobLayer = [CERangeSliderKnobLayer layer];
_upperKnobLayer.slider = self;
[self.layer addSublayer:_upperKnobLayer];
 
_lowerKnobLayer = [CERangeSliderKnobLayer layer];
_lowerKnobLayer.slider = self;
[self.layer addSublayer:_lowerKnobLayer];

Этот код позволяет удостовериться, что используется новая полоска, и вырвиглазные цвета больше не применяются к фону. :]

Осталось сделать еще немного — убрать красный фон у компонента. Откройте CEViewController.m, найдите следующую строку кода в методе viewDidLoad и сотрите ее:

_rangeSlider.backgroundColor = [UIColor redColor];

Соберите и запустите приложение… Что вы видите?

image

Ничего? Вот и отлично!

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

Так как большинство разработчиков любят, когда компоненты могут быть кастомизированы под стиль каждого конкретного приложения, добавим несколько свойств, отвечающих за внешний вид нашего слайдера. Откройте CERangeSlider.h и добавьте следующий код:

@property (nonatomic) UIColor* trackColour;
@property (nonatomic) UIColor* trackHighlightColour;
@property (nonatomic) UIColor* knobColour;
@property (nonatomic) float curvaceousness;
 
- (float) positionForValue:(float)value;

Назначение различных цветовых свойств достаточно очевидно. Что касается curvaceousness — узнаете чуть позже. И, наконец, positionForValue:. Этот метод мы уже успели реализовать, а сейчас делаем его доступным для различных слоев.

Вам нужно добавить начальные значения для цветовых свойств. Откройте CERangeSlider.m и добавьте следующий код в метод initWithFrame:, под блоком кода, отвечающим за инициализацию остальных переменных:

_trackHighlightColour = [UIColor colorWithRed:0.0 green:0.45 blue:0.94 alpha:1.0];
_trackColour = [UIColor colorWithWhite:0.9 alpha:1.0];
_knobColour = [UIColor whiteColor];
_curvaceousness = 1.0;
_maximumValue = 10.0;
_minimumValue = 0.0;

Теперь откройте CERangeSliderTrackLayer.m и добавьте следующий #import:

#import "CERangeSlider.h"

Этот слой отображает полоску, на которой находятся оба ползунка. В данный момент она наследует класс CALayer, который дает возможность использовать только сплошной цвет.

Для того, чтобы правильно отрисовывать полоску, вам нужно реализовать метод drawInContext: и использовать CoreGraphics API для рендеринга.

Примечание: Если вы хотите узнать больше о Core Graphics, то вам рекомендован курс Core Graphics 101 tutorial series [2], так как детальное рассмотрение Core Graphics находится вне рамок этого урока.

Добавьте следующий метод в CERangeSliderTrackLayer.m под @implementation:

- (void)drawInContext:(CGContextRef)ctx
{
    // clip
    float cornerRadius = self.bounds.size.height * self.slider.curvaceousness / 2.0;
    UIBezierPath *switchOutline = [UIBezierPath bezierPathWithRoundedRect:self.bounds
                                                             cornerRadius:cornerRadius];
	CGContextAddPath(ctx, switchOutline.CGPath);
    CGContextClip(ctx);
 
    // 1) fill the track
    CGContextSetFillColorWithColor(ctx, self.slider.trackColour.CGColor);
    CGContextAddPath(ctx, switchOutline.CGPath);
    CGContextFillPath(ctx);
 
    // 2) fill the highlighed range
    CGContextSetFillColorWithColor(ctx, self.slider.trackHighlightColour.CGColor);
    float lower = [self.slider positionForValue:self.slider.lowerValue];
    float upper = [self.slider positionForValue:self.slider.upperValue];
    CGContextFillRect(ctx, CGRectMake(lower, 0, upper - lower, self.bounds.size.height));
 
    // 3) add a highlight over the track
    CGRect highlight = CGRectMake(cornerRadius/2, self.bounds.size.height/2,
                                  self.bounds.size.width - cornerRadius, self.bounds.size.height/2);
    UIBezierPath *highlightPath = [UIBezierPath bezierPathWithRoundedRect:highlight
                                                             cornerRadius:highlight.size.height * self.slider.curvaceousness / 2.0];
    CGContextAddPath(ctx, highlightPath.CGPath);
    CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:1.0 alpha:0.4].CGColor);
    CGContextFillPath(ctx);
 
    // 4) inner shadow
    CGContextSetShadowWithColor(ctx, CGSizeMake(0, 2.0), 3.0, [UIColor grayColor].CGColor);
    CGContextAddPath(ctx, switchOutline.CGPath);
    CGContextSetStrokeColorWithColor(ctx, [UIColor grayColor].CGColor);
    CGContextStrokePath(ctx);
 
    // 5) outline the track
    CGContextAddPath(ctx, switchOutline.CGPath);
    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
    CGContextSetLineWidth(ctx, 0.5);
    CGContextStrokePath(ctx); 
}

На этом изображении вы можете посмотреть, каким образом сочетаются все блоки кода, разделенные комментариями:

image

Пронумерованные секции соответствуют пронумерованным комментариям:

  1. После того, как отрисовывается форма полоски, заполняется ее фон.
  2. Подсвечивается выбранный диапазон.
  3. Добавляется дополнительная подсветка длдя придания полоске объема.
  4. Отрисовывается тень.
  5. Отрисовываются объемные края полоски.

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

Запустите приложение. Оно должно выглядеть так:

image

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

Для отрисовки ползунков будем использовать аналогичный подход. Откройте CERangeSliderKnobLayer.m и добавьте следующий #import:

#import "CERangeSlider.h"

Добавьте следующий метод:

- (void)drawInContext:(CGContextRef)ctx
{
    CGRect knobFrame = CGRectInset(self.bounds, 2.0, 2.0);
 
    UIBezierPath *knobPath = [UIBezierPath bezierPathWithRoundedRect:knobFrame
                                                        cornerRadius:knobFrame.size.height * self.slider.curvaceousness / 2.0];
 
    // 1) fill - with a subtle shadow
    CGContextSetShadowWithColor(ctx, CGSizeMake(0, 1), 1.0, [UIColor grayColor].CGColor);
    CGContextSetFillColorWithColor(ctx, self.slider.knobColour.CGColor);
    CGContextAddPath(ctx, knobPath.CGPath);
    CGContextFillPath(ctx);
 
    // 2) outline
    CGContextSetStrokeColorWithColor(ctx, [UIColor grayColor].CGColor);
    CGContextSetLineWidth(ctx, 0.5);
    CGContextAddPath(ctx, knobPath.CGPath);
    CGContextStrokePath(ctx);
 
 
    // 3) inner gradient
    CGRect rect = CGRectInset(knobFrame, 2.0, 2.0);
    UIBezierPath *clipPath = [UIBezierPath bezierPathWithRoundedRect:rect
                                                        cornerRadius:rect.size.height * self.slider.curvaceousness / 2.0];
 
    CGGradientRef myGradient;
    CGColorSpaceRef myColorspace;
    size_t num_locations = 2;
    CGFloat locations[2] = { 0.0, 1.0 };
    CGFloat components[8] = { 0.0, 0.0, 0.0 , 0.15,  // Start color
        0.0, 0.0, 0.0, 0.05 }; // End color
 
    myColorspace = CGColorSpaceCreateDeviceRGB();
    myGradient = CGGradientCreateWithColorComponents (myColorspace, components,
                                                      locations, num_locations);
 
    CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
 
    CGContextSaveGState(ctx);
    CGContextAddPath(ctx, clipPath	.CGPath);
    CGContextClip(ctx);
    CGContextDrawLinearGradient(ctx, myGradient, startPoint, endPoint, 0);
 
    CGGradientRelease(myGradient);
    CGColorSpaceRelease(myColorspace);
    CGContextRestoreGState(ctx);
 
    // 4) highlight
    if (self.highlighted)
    {
        // fill
        CGContextSetFillColorWithColor(ctx, [UIColor colorWithWhite:0.0 alpha:0.1].CGColor);
        CGContextAddPath(ctx, knobPath.CGPath);
        CGContextFillPath(ctx);
    }
}

Снова разберем результаты работы этого метода:

  1. Как только форма ползунков готова, заполняется их фон.
  2. Дальше отрисовываются границы ползунка.
  3. Добавляется легкий градиент.
  4. И последнее — если ползунок подсвечен — он таким и остается, если его передвигают — он становится серым.

Запустите приложение еще раз:

image

Вы видите, что отрисовка компонента средствами Core Graphics стоит своих усилий. Использование этого фреймворка позволяет создать гораздо более кастомизируемый и гибкий элемент управления, чем при использовании графических ресурсов.

Обрабатываем изменения свойств компонента

Итак, что нам осталось сделать? Компонент выглядит достаточно броско, внешний вид — полностью настраиваемый и он поддерживает шаблон Target-Action.

Задумайтесь о том, что случится, если одно из свойств слайдера будет изменено в коде приложения уже после того, как он отрисован на экране. К примеру, вы можете захотеть изменить диапазон значений слайдера или изменить подсветку ползунка. На данный момент поддержка этих возможностей не реализована. Вам нужно добавить это в свой код.

Для определения того, какие свойства компонента задаются извне, вам нужно будет написать свой метод setter. Вашим первым предположением, скорее всего, будет такой код:

- (void)setTrackColour:(UIColor *)trackColour
{
    if (_trackColour != trackColour) {
        _trackColour = trackColour;
        [_trackLayer setNeedsDisplay];
    }
}

Когда изменяется свойство trackColor, этот блок кода уведомляет слой полосы слайдера о том, что ему нужно обновиться. Но, учитывая тот факт, что в API слайдера используются восемь переменных, переписывать столько раз один и тот же код — не самое лучшее решение.

Похоже на возможность использовать макрос! Откройте CERangeSlider.m и добавьте следующий код над методом initWithFrame:

#define GENERATE_SETTER(PROPERTY, TYPE, SETTER, UPDATER) 
- (void)SETTER:(TYPE)PROPERTY { 
    if (_##PROPERTY != PROPERTY) { 
        _##PROPERTY = PROPERTY; 
        [self UPDATER]; 
    } 
}

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

GENERATE_SETTER(trackHighlightColour, UIColor*, setTrackHighlightColour, redrawLayers)
 
GENERATE_SETTER(trackColour, UIColor*, setTrackColour, redrawLayers)
 
GENERATE_SETTER(curvaceousness, float, setCurvaceousness, redrawLayers)
 
GENERATE_SETTER(knobColour, UIColor*, setKnobColour, redrawLayers)
 
GENERATE_SETTER(maximumValue, float, setMaximumValue, setLayerFrames)
 
GENERATE_SETTER(minimumValue, float, setMinimumValue, setLayerFrames)
 
GENERATE_SETTER(lowerValue, float, setLowerValue, setLayerFrames)
 
GENERATE_SETTER(upperValue, float, setUpperValue, setLayerFrames)
 
- (void) redrawLayers
{
    [_upperKnobLayer setNeedsDisplay];
    [_lowerKnobLayer setNeedsDisplay];
    [_trackLayer setNeedsDisplay];
}

Мы генерируем методы setter для всех переменных одним махом. Метод redrawLayers вызывается для переменных, связанных с внешним видом компонента, а setLayerFrames — для отвечающих за разметку.

Это все, что требуется для того, чтобы слайдер адекватно реагировал на изменение своих свойств.

Как бы то ни было, нужно добавить еще немного кода для того, чтобы протестировать новые макросы и быть уверенными в том, что все работает как надо. Откройте CEViewController.m и добавьте следующий код в конец метода viewDidLoad:

[self performSelector:@selector(updateState) withObject:nil afterDelay:1.0f];

Эта строка вызовет метод updateState после секундной задержки. Добавим этот метод в CEViewController.m:

- (void)updateState
{
    _rangeSlider.trackHighlightColour = [UIColor redColor];
    _rangeSlider.curvaceousness = 0.0;
}

Этот метод изменяет цвет полоски с синего на красный, а форму ползунков — на квадратную. Запустите проект и посмотрите, как слайдер изменит свою форму с этой:

image

на эту:

image

Примечание: Код, который вы только что добавили, наглядно иллюстрирует одну из самых интересных (и, к слову, часто забываемых разработчиками) сторон разработки кастомных компонентов — их тестирование.

Когда вы разрабатываете свой элемент управления, проверить все возможные значения его свойств и их влияние на внешний вид компонента — ваша ответственность. Хорошим способом будет добавить несколько кнопок и слайдеров, каждый из которых отвечает за какое-либо свойство компонента. Таким образом можно будет тестировать элемент управления, не отвлекаясь на изменение кода.

Что дальше?

Теперь ваш слайдер полностью функционален и готов к использованию в любых приложениях! Одно из ключевых преимуществ кастомных компонентов — возможность их использования в различных приложениях различными разработчиками.

Ну как, готовы к премьере вашего слайдера?

Не совсем. Осталось завершить еще несколько задач:

Документация. Любимое занятие каждого программиста. :] Хоть вы и считаете, что ваш код идеален и не требует дополнительной документации, мнение других разработчиков может кардинально отличаться. Хорошей практикой является наличие документации для всего открытого другим разработчикам кода. Как минимум, это описание всех публично доступных классов и их свойств. К примеру, ваш CERangeSlider требует следующей документации — объяснение назначения переменных max, min, upper, lower.

Надежность. Что случится, если вы установите значение upperValue больше, чем maximumValue? Конечно, сами вы этого никогда не сделаете — это как минимум глупо. Но нельзя гарантировать того, что кто-то другой попробует! Вам нужно удостовериться, что компонент всегда работает исправно — вне зависимости от уровня глупости разработчика.

Структура API. Предыдущий пункт, касающийся надежности, тесно связан с гораздо более широкой темой — структурой API. Создание гибкой, интуитивно понятной и надежной структуры поможет компоненту стать широко используемым и популярным. В моей компании, ShinobiControls, мы часами можем обсуждать каждую мелкую деталь наших API! Структура API — очень глубокая тема, которая выходит за рамки этого урока. Если вам стала интересна эта тема, рекомендую прочесть Matt Gemmell’s 25 rules of API design [3].

Есть множество мест, с которых можно начать распространение своего компонента. Вот несколько вариантов:

  • GitHub [4]. GitHub стал одной из самых популярных площадок для размещения компонентов с открытым исходным кодом. Сейчас на GitHub представлено бесчисленное множество различных элементов управления для iOS. Что особенно хорошо в GitHub, так это то, что он дает возможность быстро и удобно просмотреть код компонента, использовать его в других проектах и оставлять различные замечания.
  • CocoaPods [5]. Чтобы позволить людям с легкостью добавить ваш компонент в свой проект, вы можете добавить его в CocoaPods — менеджер расширений для iOS и OSX.
  • Cocoa Controls [6]. Сайт содержит как бесплатные, так и коммерческие компоненты. Большая часть из представленных проектов находятся на GitHub, что существенно упрощает взаимодействие с ними.
  • Binpress [7]. Равно как и предыдущий, содержит бесплатные и платные компоненты. Скорее всего вы сможете найти здесь компонент под свои нужды, а если нет — почему бы не написать его самому и выставить на продажу? Кто знает, возможно, кто-то будет готов заплатить за чистый и удобный в использовании API.

Надеюсь, вам было интересно разрабатывать кастомный элемент управления, и, возможно, вы вдохновились идеей создания своего собственного компонента.

Исходный код проекта опубликован на GitHub [8] с историей коммитов, соответствующей стадиям разработки. Если вы потеряетесь, то сможете с легкостью продолжить работу с последнего пройденного этапа! :]

Весь проект вы можете скачать по этой ссылке [9].

Автор: YourDestiny

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/uikit/54664

Ссылки в тексте:

[1] UIKit Framework Reference: https://developer.apple.com/library/ios/documentation/uikit/reference/UIKit_Framework/Introduction/Introduction.html

[2] Core Graphics 101 tutorial series: http://www.raywenderlich.com/2033/core-graphics-tutorial-for-ios-lines-rectangles-and-gradients

[3] Matt Gemmell’s 25 rules of API design: http://mattgemmell.com/api-design/

[4] GitHub: https://github.com/

[5] CocoaPods: http://cocoapods.org/

[6] Cocoa Controls: http://www.cocoacontrols.com/

[7] Binpress: http://www.binpress.com/

[8] опубликован на GitHub: https://github.com/ColinEberhardt/CERangeSlider

[9] по этой ссылке: http://cdn4.raywenderlich.com/wp-content/uploads/2013/04/CERangeSlider.zip

[10] Источник: http://habrahabr.ru/post/212111/