Как-то задумал я сделать UITextField с закругленными краями, такой же как поле поиска в мобильном Safari. К сожалению в iOS SDK нету такого типа поля, есть с недостаточно закругленными краями. Первое что приходит на ум — использовать UIImageView с картинкой, а поверх него положить UITextField. Решение подходит, если поле будет оставаться таких же размеров с какими было создано. Но мне нужно было выполнять анимации над полем, в таком случае пришлось бы пересчитывать размеры картинок в блоке анимации, что порождало бы лишний код. К тому же, хотелось чтобы текстовое поле существовало неразрывно с его закругленными краями. В общем, такое решение не годится. Попытки установить угол закругления через QuartzCore и CALayer так же не дали нужного результата.

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

Приступаем к решению

Для начала, я решил подготовить картинки этих самых закругленных полей. Тут очень помог сайт teehanlax.com с iOS 5 GUI PSD. В итоге я получил вот это:

Как видите, я выделил центральную часть, которая будет растягиваться и два закругленных края.

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

@interface RoundedTextField : UITextField
@end
Первым делом следует перекрыть метод setBorderStyle: так как он может повлиять на вид поля, а я этого очень не хотел.

-(void) setBorderStyle:(UITextBorderStyle)borderStyle
{
    [super setBorderStyle:UITextBorderStyleNone];
}
Если кто-то задумает изменить стиль границ поля (например, Interface Builder) — этого не получится.

Немного про Interface builder: так как я планировал добавлять это поле через Interface Builder и класс поля унаследован от UITextField — можно смело добавлять на вид стандартный UITextField, изменив ему Custom class.

На картинке вы видите текстовое поле с типом границ UITextBorderStyleRoundedRect, но это никак не влияет на наше поле т.к. мы перекрыли метод установки типа границ.

Отрисовка картинок

Закругленные края и центральную часть я решил рисовать в методе drawRect:, выглядело это так:

-(void) drawRect:(CGRect)rect
{
    UIImage* leftImage   = [UIImage imageNamed:@"leftRoundedField.png"];
    UIImage* rightImage  = [UIImage imageNamed:@"rightRoundedField.png"];
    UIImage* centerImage = [UIImage imageNamed:@"centerRoundedField.png"];

    // левая граница, рисуем сразу от начала
    [leftImage drawAtPoint:CGPointZero];
    // правая граница
    [rightImage drawAtPoint:CGPointMake(self.bounds.size.width - rightImage.size.width, 0)];
    // центральная часть
    CGRect centerImageRect = {
        {leftImage.size.width, 0}, // точка
        {self.bounds.size.width-leftImage.size.width-rightImage.size.width, centerImage.size.height} // размеры
    };
}
Получилось вот что:

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

[UIView animateWithDuration:1.0 animations:^{
    self.textField.frame = CGRectInset(self.textField.frame, -50.0, 0);
}];
И получил такой результат:

Очевидно, что нужно перерисовывать картинки при изменении фрейма, я перекрыл метод изменения фрейма. Сделал, чтобы при изменении фрейма содержимое рисовалось заново (вызывался метод drawRect:):

-(void) setFrame:(CGRect)frame
{
    [super setFrame:frame];
    [self setNeedsDisplay];
}
Результат был следующий, и он был таким как мне нужен. Но в процессе анимации поле выглядело деформированным:



Подумав, что это никак не исправить, я решил не использовать рисование в методе drawRect:. Я решил добавлять картинки границ используя UIImageView (уж они то точно не деформируются!), но не отдельно от UITextField, а на него. При таком подходе можно избавиться от перекрытия метода setFrame:.

-(void) configureTextField
{
    UIImage* leftImage   = [UIImage imageNamed:@"leftRoundedField.png"];
    UIImage* rightImage  = [UIImage imageNamed:@"rightRoundedField.png"];
    UIImage* centerImage = [UIImage imageNamed:@"centerRoundedField.png"];

    UIImageView* leftImageView = [[[UIImageView alloc] initWithImage:leftImage] autorelease];
    leftImageView.frame = CGRectMake(0, 0, leftImage.size.width, leftImage.size.height);
    UIImageView* rightImageView = [[[UIImageView alloc] initWithImage:rightImage] autorelease];
    rightImageView.frame = CGRectMake(self.bounds.size.width-rightImage.size.width, 0, rightImage.size.width, rightImage.size.height);
    rightImageView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
    UIImageView* centerImageView = [[[UIImageView alloc] initWithImage:centerImage] autorelease];
    centerImageView.frame = CGRectMake(leftImage.size.width, 0, self.bounds.size.width - leftImage.size.width - rightImage.size.width, self.bounds.size.height);
    centerImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    [self addSubview:centerImageView];
    [self addSubview:leftImageView];
    [self addSubview:rightImageView];
}
Этот метод следуюет вызывать, когда поле создается:

// если создается из Interface Builder
-(id) initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    [self configureTextField];
    return self;
}

// если создается из кода
-(id) initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    [self configureTextField];
    return self;
}
Результат анимации получился таким как и требовалось.

Уменьшаем границы области ввода

Осталась одна проблема — сделать так, чтобы текст не перекрывал закругленную границу. У UITextField существует набор методов, которые возвращают границы для рисования конкретных частей — области ввода, кнопки «Clear» и т.д. Эти методы не стоит вызывать напрямую, но всегда можно переопределить. Вот они:

- (CGRect)borderRectForBounds:(CGRect)bounds;
- (CGRect)textRectForBounds:(CGRect)bounds;
- (CGRect)placeholderRectForBounds:(CGRect)bounds;
- (CGRect)editingRectForBounds:(CGRect)bounds;
- (CGRect)clearButtonRectForBounds:(CGRect)bounds;
- (CGRect)leftViewRectForBounds:(CGRect)bounds;
- (CGRect)rightViewRectForBounds:(CGRect)bounds;
Нужно перекрыть нужные методы и возвращать уменьшенную область. Я определил метод, который из оригинальной области делает ту, которая не перекрывает закругленные края:

- (CGRect) adjustBoundsFromBounds:(CGRect)bounds
{
    return CGRectInset(bounds, 10.0, 0);
}
А далее просто перекрыл нужные методы:

- (CGRect)borderRectForBounds:(CGRect)bounds
{
    return [super borderRectForBounds:[self adjustBoundsFromBounds:bounds]];
}
- (CGRect)textRectForBounds:(CGRect)bounds
{
    return [super textRectForBounds:[self adjustBoundsFromBounds:bounds]];
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
    return [super editingRectForBounds:[self adjustBoundsFromBounds:bounds]];
}
- (CGRect)leftViewRectForBounds:(CGRect)bounds
{
    return [super leftViewRectForBounds:[self adjustBoundsFromBounds:bounds]];
}
- (CGRect)rightViewRectForBounds:(CGRect)bounds
{
    return [super rightViewRectForBounds:[self adjustBoundsFromBounds:bounds]];
}
Как видите, в каждом методе я вызываю метод родительского класса, чтобы он правильно посчитал область. Но область я передаю уже уменьшенную.

Получилось вот что (и кнопка «Clear» на месте!):

Велосипед

Как оказалось, я изобрел очередной велосипед с рисованием картинок. Подсказал в Twitter @talissman и @dlebedev в комментариях.

Можно использовать свойство background у UITextField. Для этого я подготовил новую картинку:

Изменился метод configureTextfield:

-(void) configureTextField
{
    UIImage* background = [[UIImage imageNamed:@"bg.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 15)];

    [self setBackground:background];
}
Обновленный проект с исходным кодом можно загрузить по этой ссылке.