7 April 2013

Системы частиц в UIKit и iOS 5

Let's end this feast with a tasty snack - UIKit Particle Systems in iOS 5!

Скорее всего вы уже видели системы частиц, используемые в различных приложениях и играх на iOS, эмитирующие взрывы, огонь, дождь, снег и др. Чаще всего вы могли видеть эти эффекты в играх, потому что до iOS 5 UIKit не имел встроенной возможности создавать и использовать системы частиц.

Начиная с iOS 5, вы можете создавать системы частиц прямо в UIKit и существенно изменить внешний вид своего приложения, добавив в него системы частиц.

В этом уроке мы научимся рисовать “огнем”. Используя системы частиц, будем рисовать на экране различные рисунки.

Вы пройдете весь курс создания систем частиц и сможете использовать полученные навыки в своих приложениях. После завершения урока, вы с легкостью сможете нарисовать вопросительный знак “огнем”:

UIKit Particle Systems in iOS 5 Example

Описание

Для создания системы частиц, мы будем использовать два класса из фреймворка QuartzCore – CAEmitterLayer и CAEmitterCell.

Основная идея состоит в том, что вы создаете CAEmitterLayer и добавляете на него один или несколько CAEmitterCells. Каждая из CAEmitterCells будет отвечать за одну систему частиц.

CAEmitterLayer наследуется от CALayer, поэтому вы с легкостью можете добавить его ваш UIKit.

Без сомнения, самая шикарная вещь о системах частиц в UIKit – возможность CAEmitterLayer содержания в себе нескольких CAEmitterCells. Это позволяет создавать по-настоящему сложные и красивые визуальные эффекты.

Начинаем

Создайте новый проект в xCode FileNewNew Project. Выберите iOSApplicationSingle View Application и нажмите Next. Назовите проект DrawWithFire и поставьте DWF в поле “Class Prefix”, убедитесь, что поставлена галочка у “Use automatic reference counting”.

Выберите ваш проект в Project Navigator-е, выберите DrawWithFire в Targets. Прокрутите вниз до секции “Linked Frameworks and Libraries”. Нажмите на “+” и добавить QuartzCore.framework.

Создайте новый файл в проекте File->New->File… Cocoa TouchObjective-C class. Назовите его DWFParticleView и укажите “Subclass of” – UIView.

Откройте DWFParticleView.m и замените его содержимое на:

#import "DWFParticleView.h"
#import <QuartzCore/QuartzCore.h>
 
@implementation DWFParticleView
{
    CAEmitterLayer* fireEmitter; //1
}
 
-(void)awakeFromNib
{
    //set ref to the layer
    fireEmitter = (CAEmitterLayer*)self.layer; //2
}
 
+ (Class) layerClass //3
{
    //configure the UIView to have emitter layer
    return [CAEmitterLayer class];
}
 
@end
  • Мы создаем приватную переменную для хранения нашего CAEmitterLayer.
  • В методе awakeFromNib мы устанавливаем что fireEmitter – layer текущего вида. Мы храним это значение в fireEmitter, для будущих изменений.
  • +(Class)layerClass – стандартный метод для UIView, который отсылает UIKit сообщение, которое содержит информацию о том, какой класс используется для первичного CALayer в виде.

Теперь установим, что первичный вид в DWFViewController является DWFParticleView. Откройте DWFViewController.xib. Нажмите на Utilities, если она еще не нажата. Выберите ваш вид (серая область). Нажмите на третью вкладку (Identity Inspector). Вставьте в поле “Class” DWFParticleView:

Setting a custom class in the Identity Inspector in Interface Builder

На данном этапе мы настроили наш вид. Давайте начнем экспериментировать.

Системы частиц

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

An example particle

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

Добавьте свой или мой рисунок в проект и убедитесь, что он назван Particles_fire.png

Имитация!

Откройте DWFParticleView.m и добавьте в конец метода awakeFromNib:

//configure the emitter layer
fireEmitter.emitterPosition = CGPointMake(50, 50);
fireEmitter.emitterSize = CGSizeMake(10, 10);

Данный код настраивает местоположение эмитера (в локальных координатах) и размеры частиц.

Теперь добавьте еще кода в конец awakeFromNib:

CAEmitterCell* fire = [CAEmitterCell emitterCell];
fire.birthRate = 200;
fire.lifetime = 3.0;
fire.lifetimeRange = 0.5;
fire.color = [[UIColor colorWithRed:0.8 green:0.4 blue:0.2 alpha:0.1] 
  CGColor];
fire.contents = (id)[[UIImage imageNamed:@"Particles_fire.png"] CGImage];
[fire setName:@"fire"];
 
//add the cell to the layer and we're done
fireEmitter.emitterCells = [NSArray arrayWithObject:fire];

Мы создаем ячейку, в которой будет работать система частиц, и настраиваем ее свойства.

  • birthRate: количество имитируемых частиц в секунду. Для хорошей имитации огня этого значения достаточно около 200.
  • lifetime: количество секунд до того, как пропадет каждая из частиц.
  • lifetimeRange: значение на которое будет варьироваться lifetime. Система будет выдвать случайное значение для lifetime в пределах [lifetime-lifetimeRange;lifetime+lifetimeRange]
  • color: цвет для каждой цастицы
  • contents: содержимое для каждой ячейки систем частиц, обычно используется CGImage
  • name: вы можете установить имя для возможности будущих изменений

После этого мы устанавливаем свойство emitterCells на наш слой, которое является массивом из ячеек систем частиц. После установки данного свойства, layer начинает имитировать систему частиц.

Скомпилируйте и установите проект:

A simple UIKit Particle Effect

Хорошо, все работает, но выглядит, как непонятное коричневое пятно. Давайте дополним наш код, чтобы система частиц вела себя более динамично. Добавьте данные код в метод -(void)awakeFromNib до метода [fire setName:@"fire"]:

fire.velocity = 10;
fire.velocityRange = 20;
fire.emissionRange = M_PI_2;

На данном этапе мы устанавливаем следующие свойства нашей CAEmitterCell:

  • velocity: скорость частиц за каждую секунду. Это свойство позволит нашей ячейке имитировать движение частиц со скоростью 10 к правой части экрана.
  • velocityRange: значение аналогичное lifetimeRange, с разницей тог, что оно относится к velocity.
  • emissionRange: Угол, в радианах, на который будут имитированны частицы. В данном случае M_PI_2 = 45, следовательно, частицы будут разлетаться под углом [-45;45]

Запустите проект и посмотрите, что у нас вышло:

An expanding fan of particles

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

Добавьте еще две строки в метод -(void)awakeFromNib до метода [fire setName:@"fire"]:

fire.scaleSpeed = 0.3;
fire.spin = 0.5;
  • scaleSpeed: скорость изменения масштаба частицы в секунду. В данном случае частицы будут расти на протяжении своего lifetime
  • spin: скорость вращения каждой частицы.

Запустите проект:

An even cooler fan of particles

На данном этапе наша система частиц выглядит как дым. CAEmitterCell имеет в своем распоряжении еще очень много свойств, но мы оставим все как есть и добавим еще одно, последнее свойство к CAEmitterLayer. Перед fireEmitter.emitterCells = [NSArray arrayWithObject:fire] в -(void)awakeFromNib добавьте:

fireEmitter.renderMode = kCAEmitterLayerAdditive;

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

Changing the render mode to additive results in a cool glowing effect

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

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

Играем с огнем!

Для реализации рисования касаниями, нам необходимо менять положение имитатора, согласно касаниям пользователя.

Объявите в DWFParticleView.h метод:

-(void)setEmitterPositionFromTouch: (UITouch*)t;

В DWFParticleView.m добаьте:

-(void)setEmitterPositionFromTouch: (UITouch*)t
{
    //change the emitter's position
    fireEmitter.emitterPosition = [t locationInView:self];
}

Этот метод получает касание и изменяет положение имитатора внутри вида.

Следующее, что нам необходимо – IBOutlet для вида в котором происходят касания. Откройте DWFViewController.h и замените его содержимое на:

#import <UIKit/UIKit.h>
#import "DWFParticleView.h"
 
@interface DWFViewController : UIViewController
{
    IBOutlet DWFParticleView* fireView;
}
@end

На данном этапе мы создаем переменную fireView типа DWFParticleView. Далее откройте DWFViewController.xib и удерживая Ctrl, перетащите связь от File’s Owner к серому виду, затем выберите и выпадающего списка fireView.

Connecting the view to an outlet in Interface Builder

Теперь мы у нас есть доступ к DWFParticleView из DWFViewController. Откройте DWFViewController.m и уберите все методы между @implementation DWFViewController и @end, после чего добавьте следующиц код перед @end:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [fireView setEmitterPositionFromTouch: [touches anyObject]];
}

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

A cool trail of fire

Динамическое изменение системы частиц

На данном этапе, имитатор создает частицы постоянно, что не очень похоже на то, процесс рисования на экране. Изменим нашу систему частиц таким образом, чтобы только при косании начинала работать система частиц, а при их остутсвии – переставала.

Начнем с того, что изменим свойство birthRate на 0 в DWFParticleView.m и методе – (void)awakeFromNib:

fire.birthRate = 0;

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

-(void)setIsEmitting:(BOOL)isEmitting;

А так же его реализацию в DWFParticleView.m:

-(void)setIsEmitting:(BOOL)isEmitting
{
    //turn on/off the emitting of particles
    [fireEmitter setValue:[NSNumber numberWithInt:isEmitting?200:0] 
      forKeyPath:@"emitterCells.fire.birthRate"];
}

Здесь мы используем метод setValue:forKeyPath:, который позволяет изменять ячейки с системой частиц, которые уже были добавлены в имитатор ранее. Мы используем конструкцию “emitterCells.fire.birthRate” для forKeyPath, означающую, что свойство “birthRate” находится в ячейке с именем “fire”, в массиве ячеек fireEmitter.

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [fireView setEmitterPositionFromTouch: [touches anyObject]];
    [fireView setIsEmitting:YES];
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [fireView setIsEmitting:NO];
}
 
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [fireView setIsEmitting:NO];
}

Наконец скомпилируйте и запустите проект!

An apple made with fire!  :]

Что дальше?

Здесь находится исходный проект данного урока.

Если вам понравился данный урок, у вас есть целое поле для экспериментов:

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

Таги: , , , , ,

28 March 2013

Создаем кастомные UIView в iOS 5: 5-ти звездочный рейтинг

L'Escapadou took all the stars, so I was just left with this Kermit ;]

L’Escapadou took all the stars, so I was just left with this Kermit :)

В данном уроке мы создадим элемент управления с 5-ти звездочным рейтингом, а так же узнаем основные шаги создания видов (UIView

Начинаем

Создаем новый проект iOS/Application -> Single View Application и называем его CustomView. Далее убедитесь, что стоят галочки Use Storyboard и Use Automatic Reference Counting, а так же приложение будет под iPhone.

Project settings in Xcode

Теперь создайте новый файл  iOS/Cocoa Touch и назовите его RateView, убедитесь, что он наследует UIView. Замените содержимое заголовочного файла RateView.h на:

#import <UIKit/UIKit.h>
 
@class RateView;
 
@protocol RateViewDelegate
- (void)rateView:(RateView *)rateView ratingDidChange:(float)rating;
@end
 
@interface RateView : UIView
 
@property (strong, nonatomic) UIImage *notSelectedImage;
@property (strong, nonatomic) UIImage *halfSelectedImage;
@property (strong, nonatomic) UIImage *fullSelectedImage;
@property (assign, nonatomic) float rating;
@property (assign) BOOL editable;
@property (strong) NSMutableArray * imageViews;
@property (assign, nonatomic) int maxRating;
@property (assign) int midMargin;
@property (assign) int leftMargin;
@property (assign) CGSize minImageSize;
@property (assign) id <RateViewDelegate> delegate;
 
@end

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

Далее мы создали целую кучу свойств:

  • 3 UIImage для отображения не выбранного, выбранного наполовину или полностью выбранного состояний.
  • Переменную, хранящую текущий рейтинг.
  • Логический параметр, отвечающий за возможность изменения рейтига. Например, может быть ситуация, когда нам необходимо всего лишь отобразить рейтинг, не позволяя его изменять.
  • Массив для хранения 5 картинок, отвечающих за отображения рейтинга
  • Максимальное значение рейтинга. Этот параметр может быть полезным, если мы хотим отобразить 3-х или 10-ти звездочный рейтинг.
  • Переменные для хранения расстояний, в случае, если пользователь изменяет рейтинг.
  • И, наконец, переменная, хранящая наш делегейт.

Инициализация

Замените RateView.m на:

#import "RateView.h"
 
@implementation RateView
 
@synthesize notSelectedImage = _notSelectedImage;
@synthesize halfSelectedImage = _halfSelectedImage;
@synthesize fullSelectedImage = _fullSelectedImage;
@synthesize rating = _rating;
@synthesize editable = _editable;
@synthesize imageViews = _imageViews;
@synthesize maxRating = _maxRating;
@synthesize midMargin = _midMargin;
@synthesize leftMargin = _leftMargin;
@synthesize minImageSize = _minImageSize;
@synthesize delegate = _delegate;
 
- (void)baseInit {
    _notSelectedImage = nil;
    _halfSelectedImage = nil;
    _fullSelectedImage = nil;
    _rating = 0;
    _editable = NO;    
    _imageViews = [[NSMutableArray alloc] init];
    _maxRating = 5;
    _midMargin = 5;
    _leftMargin = 0;
    _minImageSize = CGSizeMake(5, 5);
    _delegate = nil;    
}
 
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self baseInit];
    }
    return self;
}
 
- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self baseInit];
    }
    return self;
}
 
@end

Здесь все довольно просто. Мы инициализируем наши свойства и придаем им значения по-умолчанию. Обратите внимание, что у нас в коде имеются 2 метода, – (id)initWithFrame:(CGRect)frame и – (id)initWithCoder:(NSCoder *)aDecoder, что позволит нам создавать данный вид через интерфейс или же программно.

Обновление вида

Представьте, что наш контролер создал экземпляр переменных с действующими картинками, рейтингом, максимальным рейтингом и т.д. Нам необходимо создать метод для обновления состояния нашего вида, на основе заданного рейтинга. Добавьте этот метод в RateView.m:

- (void)refresh {
    for(int i = 0; i < self.imageViews.count; ++i) {
        UIImageView *imageView = [self.imageViews objectAtIndex:i];
        if (self.rating >= i+1) {
            imageView.image = self.fullSelectedImage;
        } else if (self.rating > i) {
            imageView.image = self.halfSelectedImage;
        } else {
            imageView.image = self.notSelectedImage;
        }
    }
}

Все довольно просто. Мы проходимся по массиву и выставляем соответствующие картинки исходя из установленного рейтинга.

Размещение подвидов

Одной из самых важных функций в нашем виде – это реализация layoutSubviews. Этот метод вызывается каждый раз когда происходит изменение размеров вида и нам необходимо изменить положения и размеры всех подвидов исходя из отведенного нам места. Добавьте эту функцию в RateView.m:

- (void)layoutSubviews {
    [super layoutSubviews];
 
    if (self.notSelectedImage == nil) return;
 
    float desiredImageWidth = (self.frame.size.width - (self.leftMargin*2) - (self.midMargin*self.imageViews.count)) / self.imageViews.count;
    float imageWidth = MAX(self.minImageSize.width, desiredImageWidth);
    float imageHeight = MAX(self.minImageSize.height, self.frame.size.height);
 
    for (int i = 0; i < self.imageViews.count; ++i) {
 
        UIImageView *imageView = [self.imageViews objectAtIndex:i];
        CGRect imageFrame = CGRectMake(self.leftMargin + i*(self.midMargin+imageWidth), 0, imageWidth, imageHeight);
        imageView.frame = imageFrame;
 
    }    
 
}

Если еще не была создана переменная notSelectedImage, мы просто прекращаем выполнение данного метода.

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

Изображения расположены так, чтобы заполнить все предоставленное место: левая граница(переменная leftMargin) – изоображение №1,… средняя граница (midMargin)…, изоображение №n – правая граница. Таким образом, если мы знаем весь размер нашего вида, мы может вычесть границы и поделить на количество изображений, для того, чтобы узнать ширину каждого из UIImageView.

После того, как мы узнали нужные размеры для каждого UIImageView, мы просто проходим по массиву, изменяя размеры и положение каждого элемента.

Настройка свойств

Добавьте следующее методы в RateView.m:

- (void)setMaxRating:(int)maxRating {
    _maxRating = maxRating;
 
    // Remove old image views
    for(int i = 0; i < self.imageViews.count; ++i) {
        UIImageView *imageView = (UIImageView *) [self.imageViews objectAtIndex:i];
        [imageView removeFromSuperview];
    }
    [self.imageViews removeAllObjects];
 
    // Add new image views
    for(int i = 0; i < maxRating; ++i) {
        UIImageView *imageView = [[UIImageView alloc] init];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [self.imageViews addObject:imageView];
        [self addSubview:imageView];
    }
 
    // Relayout and refresh
    [self setNeedsLayout];
    [self refresh];
}
 
- (void)setNotSelectedImage:(UIImage *)image {
    _notSelectedImage = image;
    [self refresh];
}
 
- (void)setHalfSelectedImage:(UIImage *)image {
    _halfSelectedImage = image;
    [self refresh];
}
 
- (void)setFullSelectedImage:(UIImage *)image {
    _fullSelectedImage = image;
    [self refresh];
}
 
- (void)setRating:(float)rating {
    _rating = rating;
    [self refresh];
}

Наиболее важный метод для нас – setMaxRating, потому что он определяет количество UIImageView, необходимое для отображения рейтинга, поэтому когда происходит изменение переменной maxRating, мы убираем все UIImageView с нашего вида и создаем новые, согласно нужному количеству. После этого нам необходимо обновить отображение,размеры и положение наших видов, поэтому мы вызываем метод setNeedsLayout и refresh.

Аналогично, когда одно из изображений или рейтинг изменились, мы должны вызвать метод refresh.

Обработка касаний

Наконец, обработка касаний. Добавьте данный код в RateView.m:

- (void)handleTouchAtLocation:(CGPoint)touchLocation {
    if (!self.editable) return;
 
    int newRating = 0;
    for(int i = self.imageViews.count - 1; i >= 0; i--) {
        UIImageView *imageView = [self.imageViews objectAtIndex:i];        
        if (touchLocation.x > imageView.frame.origin.x) {
            newRating = i+1;
            break;
        }
    }
 
    self.rating = newRating;
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInView:self];
    [self handleTouchAtLocation:touchLocation];
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInView:self];
    [self handleTouchAtLocation:touchLocation];
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.delegate rateView:self ratingDidChange:self.rating];
}

Наш основной код находится в методе – (void)handleTouchAtLocation:(CGPoint)touchLocation, остальные методы либо вызывают его, либо говорят делегейту, что что-то изменилось.

В – (void)handleTouchAtLocation:(CGPoint)touchLocation мы просто проходимся по массиву с UIImageView и сравниваем координаиы ‘x’ касания и ‘x’ UIImageView. Если координата ‘x’ больше, чем текущая координата ‘x’ подвида UIImageView, то рейтинг равен индекс подвида + 1. Обратите внимание, что данная функция не поддерживает звезды заполненные наполовину, но эти изменения несущественны и легко выполнимы.

Использование 5-ти звездочного рейтинга

Откройте MainStoryboard.storyboard и поставьте галочку в Editor -> Canvas -> Show Bounds Rectangles. Это облегчит представление того, что мы будем делать.
После этого перетащите UIView и UILabel из Objects Library на вид контролера и измените их размеры, для того эти элементы выглядели примерно так:
Adding a UIView and a status label
После этого выберите UIView, который вы только что добавили и перейдите в Identity Inspector. Поменяйте класс на RateView:
Setting the class name for a UIView
Теперь соединим данный вид с нашим контролером. Сделайте активным Assistant Editor в горизональном положении (View -> Assistant Editor -> Show Assistant Editor/Assistors Editor on Bottom) и убедитесь, что выбран ViewController.h в нижнем окне. Выберите RateView в редакторе и с задержанным Ctrl перетащиет связь в любую из строк между @interface и @end и отпустите. Назовите переменную rateView.
Connecting view to an outlet

Повторите тоже самое для UILabel и назовите переменную statusLabel.

Нам необходимо, чтобы текущий UIViewController получал сообщения из делегейта RateViewDelegate. Для этого добавьте заголовочный файл RateView.h и добавьте сразу за @interface ViewController : UIViewController

#import <UIKit/UIKit.h>
#import "RateView.h"
 
@interface ViewController : UIViewController <RateViewDelegate>
 
@property (weak, nonatomic) IBOutlet RateView *rateView;
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
 
@end

Теперь переключитесь на ViewController.m и замените метод – (void)viewDidLoad на:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.rateView.notSelectedImage = [UIImage imageNamed:@"kermit_empty.png"];
    self.rateView.halfSelectedImage = [UIImage imageNamed:@"kermit_half.png"];
    self.rateView.fullSelectedImage = [UIImage imageNamed:@"kermit_full.png"];
    self.rateView.rating = 0;
    self.rateView.editable = YES;
    self.rateView.maxRating = 5;
    self.rateView.delegate = self;
}

И добавьте еще один метод в этот файл

- (void)rateView:(RateView *)rateView ratingDidChange:(float)rating {
    self.statusLabel.text = [NSString stringWithFormat:@"Rating: %f", rating];
}

Скачайте данные картинки и добавьте их в проект. Аналогичные действия вы можете сделать и со своими картинками.

Скомпилируйте проект и если все было сделано верно, то наш рейтинг будет работать так, как мы представляли себе.
4 out of 5 Stars says Kermit!

Что дальше?

Здесь находится исходный код данного урока.

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

Что не было рассмотренно в этом уроке – так это отрисовка вида в -(void)drawRect. Возможно, данная техника будет рассмотрена в будущих уроках.

Таги: , , , ,

Локализация приложений в iOS

Me Gusta Localization!

Me Gusta Localization!

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

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

В данном уроке мы сделаем простое приложение “iLikeIt”, которое будет содержать одну кнопку “You like?”, после нажатия на которую будет появляться и затем пропадать картинка.

Начинаем

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

Скомпилируйте и запустите. Результат должен быть следующим:
Non-localized iPhone App

У нас есть 3 вещи:

  • Текст: ‘Hello!’
  • Кнопка: ‘You like?’
  • Картинка с текстом.

Отделяем текст от кода

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

Для того, чтобы это сделать, необходимо нужно создать “.strings” файл, в котором будут все строки, необходимые для перевода.

Создайте новый файл. Выберите iOSResource->Strings File и нажмите Next. Назовите файл Localizable.strings и нажмите Save.
Adding a .strings file to your Xcode project

Localizable.strings – стандартное имя файла, в котором iOS ищет переводы текста на нужный язык. Формат строк в данном файле следующий:

"KEY" = "CONTENT";

Для текста ‘Hello!’ строка будет иметь вид:

"TITLE" = "Hello!";

Теперь переключитесь на iLikeItViewController.m и замените содержимое метода viewDidLoad на:

self.titleLabel.text = NSLocalizedString(@"TITLE", nil);

Если вам не понятно, как работает макрос NSLocalizedString, нажмите на него правой кнопкой мыши и нажмите Jump to Definition. Вы увидите следующее:

#define NSLocalizedString(key, comment) 
    [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

Данный макрос использует метод localizedStringForKey для того, чтобы найти строку для данного ключа и для данного языка. В данном методе используется nil для параметра table, что указывает операционной системе, что поиск будет производится в файле по умолчанию (Localizable.strings).

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

Скомпилируйте и запустите проект. Ничего не должно было поменяться в плане вывода информации относительно первоначального проекта.

Добавление локализаций

Для того, чтобы добавить другую локализацию, выберите ваш проект и в вкладке Info в секции Localizations нажмите на “+” и добавьте Russian (ru). В следующем шаге оставьте галочку у тех фалов, которые подвергнуться локализации и нажмите Finish.

The location of the info pane in Xcode 4

The location of the info pane in Xcode 4

На данном этапе xCode создал два различных наборов файлов для каждого языка. В Finder-е вы увидите две папки в которых находятся файлы для каждого языка.

Localized subfolders in project folder

Вернитесь в xCode и выберите Localizable.strings, который вы создали ранее. Справа, в вкладке File Inspector, нажмите на кнопку “Make localized…” в секции Localiztion и подтвердите локализацию английского языка:

Multiple versions of file - one for each localization

Multiple versions of file - one for each localization

Теперь нам снова необходимо выбрать Localizable.strings и справа, в вкладке File Inspector, в секции Localiztion поставить галочки у нужных нам локализаций:

Multiple versions of file - one for each localization

Нажмите на стрелочку возле Localizable.strings, чтобы развернуть Localizable.strings. Вы увидите файлы для каждого из языков.

Multiple versions of file - one for each localization

Выберите Localizable.strings (Russian) и измените текст на следующий:

"TITLE" = "Привет!";

Проверим как это работает. Для того, чтобы поменять язык на симуляторе, зайдите в Settings->General->International->Language->Русский

Удалите приложение и выберите Product->Clean для того, чтобы установить свежую версию приложения. Скомпилируйте и запустите проект и вы должны увидеть следующее:
Localized UILabel

Настройка графических элементов

Следующее что мы переведем – текст на кнопке. Для русского языка – поставим текст “Нравится?”.

Если мы просто изменим текст в методе viewDidLoad, может произойти случай, когда наш текст не поместится на кнопке и появится многоточие, что не соответствует нашим представлениям, поэтому выберите русскую версию iLikeItViewController.xib (Russian) и измените текст на кнопке на “Нравится?”.

Скомпилируйте и запустите проект и вы должны увидеть следующее:
Localized text doesn't fit issue

Картинки

Так как в проекте имеются картинки нам необходимо локализировать и их. Выберите картику ilike.png. Затем перейдите в первую вкладку (File Inspector) и нажмите на кнопку Make Localized.

Me Gusta!

Добавьте английскую и русскую локализации.

Me Gusta!

Теперь для того, чтобы изменить картинку, достаточно ее просто заменить на свою в папке ru.lproj в Finder (не в xCode).

Замените картинку ilike.png в папке ru.lproj на ilike.png

Me Gusta!

Выберите Product->Clean, затем перекомпилируйте и запустите проект. Вы должны будете увидеть полностью локализированное приложение:

Localized iPhone App

Напоследок

Бонусом будет перевод названия приложения на русский язык. Файл Info.plist имеет свой файл InfoPlist.strings, отвечающий за локализации. Для того, чтобы ваше приложение по-разному называлось на Springboard-е для английского и русского языков, вам необходимо создать файл InfoPlist.strings, если его нет, локализовать его и добавить в InfoPlist.strings (Russian) строку:

"CFBundleDisplayName" = "Нравится";

Эта строка изменит имя вашего приложения на Springboard-е, при изменении языка на устройстве.

Что дальше?

Здесь находится исходный код текущего урока.

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

Для перевода вы можете использовать бесплатный сервис переводов от Google - http://www.google.com/translate, но его использование я рекомендую только для перевода простых слов в кнопках или метках.

Таги: , , ,

12 March 2013

Core Data в iOS 5: С чего начать

Core Data Failed Banks Model Diagram

Core Data Failed Banks диаграмма модели

Обновлено 4/17/12: Полностью обновлено для iOS 5 (оригинальн статьи Ray Wenderlich,обновлено Adam Burkepile).

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

Однако, изучение Core Data может занять достаточно много времени. Именно для этого и предназначена серия уроков этого направления – целью его является увеличение скорости изучения основ Core Data.

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

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

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

Прежде чем перейти к уроку, я рекомендую вам заглянуть в серию уроков SQLite для iPhone разработчиков . То же самое, только используется SQLite, это поможет вам понять то как это всё работаето. Плюс, приложение, которое мы делаем является тем же самом, только теперь используя Core Data!

Создаём Core Data проект

И так приступим! Откройте Xcode и создайте новый проект с помощью шаблона Master-Detail Application.

На следующем экране введите FailedBankCD как название проекта, всё что вы пожелаете в строку  Company Identifier, and FBCD в Class Prefix.

 

Выберите  iPhone  как семью устройств и убедитесь, что выбраны галочки возле Use Storyboards, Use Core Data, и Use Automatic Reference Counting. Выберите дальше, затем Create.

Прежде чем мы начнём, мы уберём некоторый шаблонный код. Выберите следующие файлы в навигаторе проекта:

  • FBCDMasterViewController.h
  • FBCDMasterViewController.m
  • FBCDDetailViewController.h
  • FBCDDetailViewController.m

и удалите их. Когда xCode спросит вас “Remove References” или “Move to Trash”,выберите “Move to Trash”(переместить в корзину).

 

Теперь правый клик по FailedBankCD группе папок и выберите New File. Выберите шаблон iOSObjective-C Class и нажмите далее. Назовите класс FBCDMasterViewController и убедитесь, что выбран саб класс UITableViewController. Убедитесь также , что галочки у обоих чекбоксов ОТСУТСТВУЮТ. Нажмите далее и Create.

Выберите FBCDMasterViewController.h в навигаторе проекта и добавьте следующие строки прямо под @end:

@property (nonatomic,strong) NSManagedObjectContext* managedObjectContext;

Теперь переключитесь в .m файл и добавьте свойство synthesize прямо после @implementation :

@synthesize managedObjectContext;

Не беспокойтесь если вы не знаете, что такое “NSManagedObjectContext” , мы поговорим об этом когда настанет время.

 

Но первое,  что нам нужно настроить, это отключить у  Storyboard в последствии использовании detail view controller (так как мы прежде удалили его).И так откройте MainStoryboard.storyboard и удалите detail view controller:

Далее нажимаем по FailedBanksCD.xcdatamodel. Вы увидите визуальный редактор – это то, что мы будем использовать, чтобы создать диаграмму нашей объектной модели. Идём дальше, выберите пузырь по середине, на котором написано “Entity” (сущность) (или “Event”(событие), зависит от вашей версии иксКода) и удалите его.

 

Если ваш экран не выглядит подобно моему,  попробуйте переключить Editor Style (редактора стиль) (в правом нижнем углу экрана) на visual view(визуальное отображение).

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

Откройте FailedBanksCDAppDelegate.m. Вы увидите там парочку новых функций, которые имплементированы для нас,  чтобы настроить Core Data “stack”(стек). Одна из них создаёт managed object context, другая создаеёт managed object model, и последняя создаеёт  persistent store coordinator. эм??

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

  • Managed Object Model(управляемая объектная модель): Вы можете думать об этом как об схеме базы данных. Это класс, который содержит  определения для каждого из объектов (также называется “Entities”(сущности)) которые вы храните в базе данных. Обычно, вы будете использовать визуальный редактор, который вы только что видели, чтобы настроить какие объекты в базе данных, какие у них атрибуты,  и как они относятся друг к другу. Однако, вы можете делать это также в коде!
  • Persistent Store Coordinator(координатор персистентного хранилища): Вы можете думать об этом как о соединении с базой данных. Это то место, в котором вы настраиваете актуальные имени и локации, для которых база данных будет использоваться для хранения объектов, и каждый раз когда managed object context(управляемый объектный контекст) нужно сохранить что-либо, он проходит через этот координатор.
  • Managed Object Context(управляемый объектный контекст): Вы можете думать об этом как о  “scratch pad”(блокноте) для объектов которые приходят из базы данных. Это также для нас наиболее важный из этих трёх методов, потому что с ним нам придётся работать гораздо больше, нежели с другими. В основном, когда бы ни пришлось получить объекты из бд, вставить объекты(insert), или удалить объекты, мы можете вызвать метод managed object context (или по крайней мере в большую часть случаев!)

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

Определяем нашу модель

Когда мы создаём таблицы  в нашей базе данных в SQLite уроке, у нас есть одна таблица, содержащая всю информацию о  банках банкротах. Дабы уменьшить количество информации в памяти за раз ( в целях обучения) мы просто вытащим подмножество полей, которые нужно отобразить в нашем первом табличном отображении.

 

И так вы должно быть искушены настроить нашу модель таким же образом в Core Data. Однако, с Core data вы не можете извлекать только определённые атрибуты объекта  - вы можете лишь извлечь целый объект. Хотя, если мы поделим объект на две части – FailedBankInfo часть и часть FailedBankDetails – мы сможем выполнять точно таким же образом.

И так давайте взглянем на то, как это работает . Откройте визуальный редактор модели (выберите FailedBanksCD.xcodedatamodel).

Приступим создав объект в нашей модели – говоря языком Core Data “сущность”(Entity). В нижней панели инструментов, нажмите на плюсик, чтобы добавить новую сущность.

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

Назовите сущность FailedBankInfo.  Затем нажмите на сущности и убедитесь, что выбрана третья закладка в секции утилитов ( Utilities section)  (the Data Model Inspector). Вы увидите нынешний список как сабкласс  объекта NSManagedObject. Это класс для сущностей по умолчанию, который мы будем использовать в данный момент – затем мы вернёмся и настроим кастомный объект.

И так добавим пару атрибутов. Сперва, убедитесь, что выбрана ваша сущность нажатием на имя сущности в левой панели, или на диаграмму для сущности на отображении диаграмм. На центральной панели, нажмите и держите кнопку плюса и затем выберите  “Add Attribute” (добавить атрибут) из выпадающего списка как указано на рисунке:

В контроллере данных модели справа,  впишите name в атрибут  “name” и укажите тип  “String” как показано на рисунке:

Теперь, повторите это действие, чтобы добавить еще два атрибута, “city” и “state”, также оба строки (“strings”).

Далее, нам нужно создать сущность для FailedBankDetails. Создайте сущность таким же образом, как вы делали ранее, назовите её FailedBankDetails, и добавьте следующие атрибуты к ней: zip типа  Integer 32, closeDate типа Date, и updateDate типа Date.

В результате, мы можем связать эти два типа. Выберите FailedBankInfo, нажмите и держите кнопку плюс на средней панеле, но на этот раз из выпадающего меню выберите  “Add relationship”(добавить отношение):

Назовите связь “details”, и установите destination(назначение) “FailedBankDetails.”

Хорошо, и так, что мы только что сделали ? Мы просто настроили связь в Core Data, которая объединяет одну сущность с другой сущностью. В таком случае, мы устанавливаем один-к-одному связь – каждаый FailedBankInfo будет иметь ровно 1 FailedBankDetails. За кулисами, Core Data настроит нашу базу данных таким образом, что таблица FailedBankInfo имеет поле для соответствующего объекта FailedBankDetails.

Apple рекомендует каждый раз когда создаётся связь между одним объектом к другому, создавать ссылку с другого объекта идущую обратно таким же образом. Давайте так и поступим.

Добавьте отношение к “FailedBankDetails” и назовите его  “info”, установите destination “FailedBankInfo”, и укажите  invers(обратную связь) к “details”.

Также, поставьте правило удаления для обоих связей  “cascade”. Это означает, что удаление одного объекта в Core Data, также повлечёт за собой удаление ассоциативных объектов. Это имеет смысл в данном случае, потому что FailedBankDetails не будет значить ничего без соответствующих значений в таблице FailedBankInfo.

Запустите приложение! Что за ….? Ошибка?

Хорошо, такое порой случается. Смотрите… когда вы меняете вашу модель хранения (Managed Object Model) и при этом также было создано персистентное хранилище, система не знает как это прочесть. Это всё равно что использовать секретный декодер, чтобы написать секретное сообщение, а затем сменить этот декодер. Когда вы попытаетесь прочесть сообщение после изменения, у вас, естественно, ничего не выйдет.

Вы можете выполнить обновление модели, но оставим это решение на другой раз. На данный момент просто удалите существующее персистентное хранилище данных. Самый просто путь сделать это, просто удалить приложение из вашего iPhone или simulator, чтобы вы там не использовали. Нажмите и держите на приложении, будто вы удаляете его на телефоне. Затем запустите еще раз и всё должно быть хорошо.

Тестируем вашу модель

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

 

Для начала, давайте проверим добавление тестового объекта в нашу базу данных. Откройте FailedBanksCDAppDelegate.m и добавьте следующее сверху application:didFinishLaunchingWithOptions:

NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *failedBankInfo = [NSEntityDescription
    insertNewObjectForEntityForName:@"FailedBankInfo"
    inManagedObjectContext:context];
[failedBankInfo setValue:@"Test Bank" forKey:@"name"];
[failedBankInfo setValue:@"Testville" forKey:@"city"];
[failedBankInfo setValue:@"Testland" forKey:@"state"];
NSManagedObject *failedBankDetails = [NSEntityDescription
    insertNewObjectForEntityForName:@"FailedBankDetails"
    inManagedObjectContext:context];
[failedBankDetails setValue:[NSDate date] forKey:@"closeDate"];
[failedBankDetails setValue:[NSDate date] forKey:@"updateDate"];
[failedBankDetails setValue:[NSNumber numberWithInt:12345] forKey:@"zip"];
[failedBankDetails setValue:failedBankInfo forKey:@"info"];
[failedBankInfo setValue:failedBankDetails forKey:@"details"];
NSError *error;
if (![context save:&amp;error]) {
    NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
}

В первой строке мы берём указатель к нашему управляемому контексту объекта, используя вспомогающую функцию, которая включена в шаблон.

 

Затем мы создадим новый экземпляр объекта NSManagedObject для нашего отношения FailedBankInfo , вызвав метод insertNewObjectForEntityForName. Каждый объект, который хранит Core Data наследуется от NSManagedObject. Как только вы получаете экземпляр объекта, вы можете вызвать  setValue(установить значение)  для любого атрибута, который вы объявили в визуальном редакторе, чтобы настроить объект.

И так идём дальше и настроим тестовый банк,  для обеих таблиц FailedBankInfo и FailedBankDetails. На данный момент объекты просто изменились в памяти – чтобы сохранить их обратно в базе данных, нам нужно вызвать сохранение на  managedObjectContext.

И никакого SQL кода для инсерта данных не требуется!

Прежде, чем мы это попробуем, давайте добавим немного кода сюда, чтобы Before we try this out, let’s add some more code in there to list out all the objects currently in the database:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
    entityForName:@"FailedBankInfo" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&amp;error];
for (NSManagedObject *info in fetchedObjects) {
    NSLog(@"Name: %@", [info valueForKey:@"name"]);
    NSManagedObject *details = [info valueForKey:@"details"];
    NSLog(@"Zip: %@", [details valueForKey:@"zip"]);
}

Здесь мы создаём новый объект под названием  fetch request. Представьте, что fetch request(выполнить запрос) это условие  SELECT. Мы вызываем entityForName дабы получить указатель к отношению FailedBankInfo, который мы хотим извлечь, и затем используем setEntity чтобы сказать нашему fetch request что за отношение мы хотим получить.

 

Мы затем вызываем executeFetchRequest на managed object context, чтобы вытащить все объекты из таблицы в наш блокнот “scratch pad”. Затем  мы повторяем итерацию через каждый объект NSManagedObject, и используем valueForKey, чтобы вытащить различные куски.

Отметим, что даже несмотря на то, что мы вытаскиваем объекты из таблицы FailedBankInfo , мы по прежнему имеем доступ к  связанному с ним объекту FailedBankDetails путём доступа через свойство details в связи FAiledBankInfo.

Как это всё работает ? За кулисами, когда вы обращаетесь к свойству, Core Data уведомляет, что здесь до сих пор нету данных в контексте, и “падает”, что по сути означает пробег по базе данных и впихивание в базу информации прямо так как вам это нужно. Довольно удобно!

Запустите приложение и взгляните что получилось.

Просмотр SQL запросов

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

 

Еще раз, Apple снабдил нас довольно простым решением по этой проблеме. Откройте выпадающий список схем в XCode и выберите ‘Edit Scheme…’.  Затем  ’Run’ схему и выберите  ’Arguments’ tab.  Добавьте следующий аргумент: “-com.apple.CoreData.SQLDebug 1”. Когда вы проделаете следующие операции, это должно выглядеть примерно следующим образом:

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

И так здесь мы видим то, что и ожидали увидеть. Первые два селекта и обновление, Core Data ведёт учет всего происходящего, учитывая ID для связей.

Затем идут инсерты в таблицы details и info. После чего, мы делаем выборку всей таблицы bank info в нашем запросе. Затем так как мы каждый раз проходим по результату, постоянно мы получаем доступ к переменной details, за кулисами Core Data падает и получает запрос на выборку из другой таблицы для получает информации из таблицы ZFAILEDBANKDETAILS .

Авто генерирующиеся файлы модели

До сих пор мы использовали NSManagedObject для работы с нашими отношениями. Это не самый лучший способ, потому что NSManagedObject не строго типизированный класс. Как вы можете видеть, вы обращаетесь к атрибутам в отношениях через строки(strings), и здесь довольно просто допустить ошибку и вписать имя атрибута неверно, или установить неверный тип информации.

 

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

Давайте попробуем. Откройте FailedBanksCD.xcdatamodel, нажмите на FailedBankInfo связи, и перейдите в FileNewFile. Выберите шаблон Core DataNSManagedObject , и нажмите далее, и затем нажмите снова создать на следующем отображении.

Вы увидите пару новых файлов добавленных в ваш проект: FailedBankInfo.h/m and FailedBankDetails.h/m. Эти классы достаточно простые, и просто объявляют свойства основанные на атрибутах, которые вы добавили в сущности. Также обратите внимание, что свойства объявлены как динамичные внутри .m файлов – это потому что Core Data связывает свойства автоматически.

Проделайте те же шаги с сущностью FailedBankDetails.

Здесь есть одна проблемы с автогенерирующимися классами, которую мы собираемся пофиксить. Если вы взглянете на FailedBankDetails.h, вы заметите, что переменная variable объявлена как класс FailedBankInfo, но в FailedBankInfo.h переменная  details объявлена как  NSManagedObject (но это должен быть объек FailedBankDetails ).

Такое поведение происходит потому что вы запустили генератор для FailedBankInfo до того как запустили генератор для FailedBankDetails,  следовательно он не знал, что в последствии будет доступен класс FailedBankDetails .

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

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

Теперь, давайте общипаем наш тестовый код FBCDAppDelegate.m , чтобы использовать существующие сабклассы NSManagedObject. Сперва добавим в верх метода :

#import "FailedBankInfo.h"
#import "FailedBankDetails.h"

Затем изменим код как написано далее:

NSManagedObjectContext *context = [self managedObjectContext];
FailedBankInfo *failedBankInfo = [NSEntityDescription
    insertNewObjectForEntityForName:@"FailedBankInfo"
    inManagedObjectContext:context];
failedBankInfo.name = @"Test Bank";
failedBankInfo.city = @"Testville";
failedBankInfo.state = @"Testland";
FailedBankDetails *failedBankDetails = [NSEntityDescription
    insertNewObjectForEntityForName:@"FailedBankDetails"
    inManagedObjectContext:context];
failedBankDetails.closeDate = [NSDate date];
failedBankDetails.updateDate = [NSDate date];
failedBankDetails.zip = [NSNumber numberWithInt:12345];
failedBankDetails.info = failedBankInfo;
failedBankInfo.details = failedBankDetails;
NSError *error;
if (![context save:&amp;error]) {
    NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
}
// Тестируем пролистывая все FailedBankInfos из хранилища
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo"
    inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&amp;error];
for (FailedBankInfo *info in fetchedObjects) {
    NSLog(@"Name: %@", info.name);
    FailedBankDetails *details = info.details;
    NSLog(@"Zip: %@", details.zip);
}

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

Создание табличного отображения

Откройте FBCDMasterViewController.h и добавьте следующую переменную для failedBankInfos, которую мы будем отображать в табличке.

@property (nonatomic, strong) NSArray *failedBankInfos;

Отметим, что у нас уже есть managed object context, который мы будем использовать для извлечения списка, который мы настроим чуть позже. Если вы взгляните вниз метода application:didFinishLaunchingWithOptions в FBCDAppDelegate.m, вы увидите, что там устанавливается managed object context.

 

Переключитесь на FBCDMasterViewController.m и сделайте следующие изменения and make the following changes:

// в самом верху, в секцию импорта
#import "FailedBankInfo.h"
// Вверху, над @implementation
@synthesize failedBankInfos;

Затем измените метод viewDidLoad, чтобы он выглядел следующим образом:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription
                                   entityForName:@"FailedBankInfo" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSError *error;
    self.failedBankInfos = [managedObjectContext executeFetchRequest:fetchRequest error:&amp;error];
    self.title = @"Failed Banks";
}

Этот код должен быть вам знаком, так как ранее уже встречались с ним, когда писали тестовый код ранее. Мы просто создаём  fetch request, чтобы получить все FailedBankInfos , содержащиеся в базе данных и сохраняем это в нашей переменной.

 

Остальные изменения в точности такие же как мы делали в уроке по SQLite. Для быстрого понимания, я приведу необходимые шаги снова:

Возвращаем 1 для numberOfSectionsInTableView:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

Заменяем numberOfRowsInSection со следующим:

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    return [failedBankInfos count];
}

Модифицируем cellForRowAtIndexPath, чтобы выглядело следующим образом:

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    // настраиваем ячейку...
    FailedBankInfo *info = [failedBankInfos objectAtIndex:indexPath.row];
    cell.textLabel.text = info.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@",
        info.city, info.state];
    return cell;
}

В конце концов выбираем MainStoryboard.storyboard и затем выберите Master View Controller. Выберите ячейку tableview(cell) в таблице и установите стиль Subtitle.

 

Скомпилируйте и запустите проект,  и если вы всё делали правильно, то увидите следующий результат.

Куда дальше?

Здесь лежит сэмпл кода для нашего проекта (прямая загрузка).

 

Чем дальше – тем лучше, за исключением потери фактических данных из failed banks. И так далее в серии уроков мы раскроем тему как загрузить/импортировать существующие данные!

Таги: , , ,

5 January 2013

Использование системы контроля версий Git в Xcode для iOS 6, Часть 2

Если новая ветка не выбрана, выберите ее. Затем выберите MainStoryboard.storyboard, и из object library перетащите UILabel на главный вид.

Сохраните и запустите, для проверки, что все OK и сделайте коммит File->Source Control->Commit. Добавьте к коммиту комментарий.

Переключитесь на ветку master (ствол) выбрав ее в органайзере, выбрав папку проекта в левой панели, нажав на иконку “Switch Branch” на нижней панели, выбрав ветку “master” и нажмите “OK.”

После переключения, вы увидите, что Current Branch изменилась на master в правом верхнем углу, как на картинке.

swith to trunk

Запустите приложение. Добавленного UILabel’а нет. Очевидно, это поскольку вы сменили рабочую ветку.

Теперь соединим новую ветвь со стволом.

Откройте File->Source Control->Merge. Новый диалог спросит: какую ветку соединить с текущей? Выберите новую ветку и жмите “Choose.”

merge branch

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

Ну и наконец, жмите кнопку “Merge”(Соединить) :] Здесь вас наверняка спросят о создании автоснапшотов. Поскольку ваши изменения хранятся в репозитории Git, снапшоты излишни. Выберите “Disable.”

Теперь вы увидите изменения (UILabel) из новой ветки в интерфейсе MainStoryboard.storyboard или когда запустите приложение. Ваши изменения в стволе, потому что вы их соединили! Продолжайте читать!

31 December 2012

Как сделать игру с тайловой графикой с помощью Cocos2D

Ммм, вкусные арбузы

Mmm, tasty melons!

В этом туториале, состоящем из двух частей, мы узнаем как создать игру с тайловой графикой с помощью Cocos2D и редактора карт Tiled. Мы создадим простую игру с тайловой графикой, в которой ниндзя будет исследовать пустыню в поисках вкусных арбузов!

В этой части туториала мы узнаем как создать карту с помощью Tiled, как добавить карту в игру, как выпонить скроллинг карты, чтобы следовать за игроком и как использовать слои объектов.

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

Возможно вам следует вначале ознакомиться со следующим туториалом – Как сделать простую игру для iPhone с помощью Cocos2d, если вы еще этого не сделали, поскольку там обсуждаются основы, которые мы будем использовать здесь.

Ок, давайте немного повеселимся с тайловыми картами!

Создание основы проекта

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

Запустите Xcode, выберите “FileNew Project…”, выберите шаблон cocos2d Application и создайте проект под названием “TileGame”.

Далее, скачайте зип файл отсюда resources for the game. Этот зип файл содержит следующее:

  • Спрайт для игрока. Похож на тот, который использовался в туториале Как сделать простую игру для iPhone с помощью Cocos2d!
  • Несколько звуковых эффектов, которые я сделал с помощью этой прекрасной cxfr утилиты.
  • Фоновая музыка, которую я сделал спомощью Garage Band (гляньте вот этот пост чтобы узнать больше).
  • Набор тайлов, который мы будем использовать – он, вообще-то, относится к редактору карт, но я подумал, что будет проще включить его вместе со всем остальным.
  • Некоторые дополнительные “специальные” тайлы, о них будет рассказано чуть позже.

Как только вы скачали эти ресурсы, распакуйте их и перетащите в группу “Resources” в ваш проект. Убедитесь, что выбрана опция “Copy items into destination group’s folder (if needed)” и нажмите ОК.

Если все сделано правильно, все файлы должны быть доступны в проекте. Этого достаточно, пора создавать карту!

Создание карты с помощью Tiled

Cocos2D поддерживает карты, созданные с помощью редактора карт c открытым кодом Tiled, и сохраненные в формате ТМХ.

Если вы перейдете по ссылке, приведенной выше, то вы увидите, что существует две версии Tiled – одна написана с помощью Qt фреймверка, а другая с помощью Java. Две версии существуют потому, что сначала Tiled был написан на Java, а сейчас портируется на Qt.

Какую версию вам использовать – решайте сами. В этом туториале мы будем использовать Qt версию, потому что именно она предлагается в качестве основной, но некоторые предпочитают использовать Java версию, так как пока что не все старые возможности редактора были портированы.

В любом случае, если вы хотите следовать этому туториалу лучшим образом – скачайте Qt версию, установите ее и запустите. Нажмите Файл/Создать и заполните диалоговое окно следующим образом:

Диалоговое окно создания новой карты в редакторе карт Tiled

В секции “Ориентация” можно выбирать между ортогональной (как в Legend of Zelda) или изометрической (как в Disgaea). Мы выберем здесь ортогональную ориентацию.

Далее нужно установить размер карты. Обратите внимание, что он задается в тайлах, а не в пикселях. Мы собираемся делать маленькую карту, так что выберите здесь 50х50.

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

Далее, нам необходимо добавить набор тайлов, который мы будем использовать для рисования нашей карты. Кликните на “Карта” в меню, затем кликните “Новый набор тайлов” и заполните диалоговое окно следующим образом:

Диалог добавления нового набора тайлов в редакторе карт Tiled

Чтобы получить изображение, кликните “Обзор”, перейдите к вашей папке TileGame и выберите файл tmw_desert_spacing.png, который вы добавили в ваш проект из скачанной зип папки. Поле “Имя” автоматически заполнится именем файла.

Ширину и высоту можно оставить 32х32, поскольку это и есть размер тайлов. Что касается полей “Отступ” и “Промежуток”, мне не удалось отыскать какую либо хорошую документацию, в которой бы объяснялось их назначение, но вот что по моему мнению они означают:

  • Отступ это количество пикселей в ширину и в высоту по краям тайла
  • Промежуток это количество пикселей в ширину и в высоту между тайлами.

Если вы посмотрите на tmw_desert_spacing.png, то вы увидите, что каждый тайл имеет черную рамку шириной в один пиксель, вот почему мы задаем значение 1 для отступа и промежутка.

Увеличенное изображение тайлов

После того, как вы кликнете на ОК, вы увидите, что тайлы появились в окне “Наборы тайлов”. Теперь можно начинать рисовать. Просто кликните иконку “Штамп” в панели инструментов вверху, затем кликните на каком-нибудь тайле, а затем кликните где-нибудь на карте, в том месте, где бы вы хотели поместить тайл.

Использование инструмента

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

Рисование карты в Tiled

Несколько советов:

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

Когда вы закончили рисовать карту, в панели “Слои” найдите слой который по идее сейчас называется “Слой тайлов 1″ кликните на нем два раза и измените его имя на “Background”. Затем кликните “Файл/Сохранить” и сохраните этот файл в папке Resources вашего проекта TiledMap под названием “TileMap.tmx”.

Мы сделаем еще кое-что с Tiled позже, но сейчас давайте поработаем с картой в нашей игре!

Добавление тайловой карты к сцене в Cocos2d

Первым делом, кликните правой кнопкой на Resources, кликните “AddExisting Files…” и добавьте новый файл TileMap.tmx, который вы только что создали, в ваш проект.

Откройте файл HelloWorldScene.h (Примечание переводчика: в новых шаблонах Cocos2d вместо HelloWorldScene.h скорее всего будет присутствовать файл HelloWorldLayer.h, что сути не меняет) и добавьте пару переменных, которые нам понадобятся:

// Внутри объявления класса HelloWorld
CCTMXTiledMap *_tileMap;
CCTMXLayer *_background;
 
// После объвления класса
@property (nonatomic, retain) CCTMXTiledMap *tileMap;
@property (nonatomic, retain) CCTMXLayer *background;

Затем сделайте следующие изменения в HelloWorldScene.m:

// В самом верху, в начале implementation
@synthesize tileMap = _tileMap;
@synthesize background = _background;
 
// В dealloc
self.tileMap = nil;
self.background = nil;
 
// Замените метод init следующим:
-(id) init
{
    if( (self=[super init] )) {
 
        self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"TileMap.tmx"];
        self.background = [_tileMap layerNamed:@"Background"];
 
        [self addChild:_tileMap z:-1];
 
    }
    return self;
}

Здесь мы обращаемся к файлу CCTMXTiledMap, указывая ему создать карту из файла карты, который мы создали с помощью Tiled.

Немного о CCTMXTiledMap. Этот класс наследует от CCNode, так что вы можете задать ему позицию, масштаб и т.д. Его детьми являются слои карты, также он имеет вспомогательную функцию, с помощью которой вы можете получить их по имени – именно то, что мы сделали, чтобы получить бэкграунд. Каждый слой является потомком класса CCSpriteSheet,  это означает, что вы можете иметь только один набор тайлов на слой.

В общем, все, что мы здесь делаем – сохраняем ссылки на на тайловую карту и слой бэкграунда, и добавляем тайловую карту к слою HelloWorld.

И это все! Откомпилируйте и запустите код, и вы увидите нижний левый угол вашей карты:

Изображение края карты

Неплохо! Но, чтобы сделать это игрой, нам необходимы три вещи: а) игрок, б) стартовая точка для игрока, и в) передвижение карты так, чтобы мы всегда смотрели на игрока.

И вот здесь все запутывается. Давайте займемся этим далее!

Слои объектов и настройка позиции тайловой карты

Tiled поддерживает два вида слоев – слои  тайлов (с которыми мы уже работали) и слои объектов.

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

В меню Tiled выберите “СлойДобавить слой объектов”, назовите выбранный слой “Objects”, а в панели инструментов выберите “Вставить объект”. Попробуйте выделить мышкой область на карте и вы увидите странную серую форму, которую вы можете увеличивать, чтобы покрыть множество тайлов.

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

Создание слоя объекта в Tiled

Затем кликните правой кнопкой на сером объекте, который вы только что добавили, и затем кликните на “Свойства объекта”. Назовите объект “SpawnPoint” и кликните ОК:

Свойства объекта в Tiled

Вероятно, вам было бы интересно задать тип объекта как имя класса Cocos2D, и это создало бы объект такого типа (например CCSprite), такая возможность присутствовала в предыдущей версии Cocos2D, но была удалена в связи с рядом проблем.

В общем – мы просто оставим тип пустым, это создаст для нас NSMutableDictionary, где мы сможем иметь доступ к различным аспектам объекта, включая х и у координаты.

Сохраните карту и отправляйтесь обратно в Xcode. Сделайте следующие изменения в HelloWorldScene.h:

// Внутри объявления класса HelloWorld
CCSprite *_player;
 
// После объявления класса
@property (nonatomic, retain) CCSprite *player;

Затем измените HelloWorldScene.m:

// В самом начале implementation
@synthesize player = _player;
 
// В dealloc
self.player = nil;
 
// Внутри метода init, после того, как задан self.background
CCTMXObjectGroup *objects = [_tileMap objectGroupNamed:@"Objects"];
NSAssert(objects != nil, @"'Objects' object group not found");
NSMutableDictionary *spawnPoint = [objects objectNamed:@"SpawnPoint"];        
NSAssert(spawnPoint != nil, @"SpawnPoint object not found");
int x = [[spawnPoint valueForKey:@"x"] intValue];
int y = [[spawnPoint valueForKey:@"y"] intValue];
 
self.player = [CCSprite spriteWithFile:@"Player.png"];
_player.position = ccp(x, y);
[self addChild:_player]; 
 
[self setViewpointCenter:_player.position];

ОК, давайте остановимся на секунду и разберемся со слоем объектов и группами объектов. В первую очередь, обратите внимание, что вы получаете слои объектов с помощью метода objectGroupNamed объекта CCTMXTiledMap (а не layerNamed ). Он возвращает объект класса CCTMXObjectGroup.

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

И наконец, мы хотим сделать так, чтобы игрок всегда находился в поле зрения. Добавьте новый метод:

-(void)setViewpointCenter:(CGPoint) position {
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
 
    int x = MAX(position.x, winSize.width / 2);
    int y = MAX(position.y, winSize.height / 2);
    x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width) 
        - winSize.width / 2);
    y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height) 
        - winSize.height/2);
    CGPoint actualPosition = ccp(x, y);
 
    CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
    CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
    self.position = viewPoint;
 
}

Давайте с этим тоже немного разберемся. Представьте себе, что эта функция устанавливает центр фокуса камеры. Функция принимает любые х и у координаты, но есть такие точки, которые мы не хотим показывать – например, нам не нужно, чтобы экран выходил за пределы карты (где находится просто пустое пространство!).

Взгляните, вот на эту диаграмму:

Диаграмма тайловой карты и поля обзора в Cocos2D

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

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

Посмотрите на эту диаграмму:

Диаграмма, показывающая как передвигать слой, чтобы соответствовать экрану в Cocos2D

Вообразите большой мир, по координатам которого мы проводим взглядом от 0 до до крайних точек. Центр нашего взгляда расположен в точке centerOfView, и мы хотим переместить его в точку actualPosition. Чтобы сделать эту точку центром нашего поля зрения, всё, что нам нужно сделать – перетянуть карту вниз!

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

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

Скриншот сцены с ниндзя

Передвижение ниндзя

Мы хорошо начали, но наш ниндзя просто сидит на месте. А ниндзя себя так не ведут.

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

// В методе init
self.isTouchEnabled = YES;
 
-(void) registerWithTouchDispatcher
{
	[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self 
            priority:0 swallowsTouches:YES];
}
 
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
	return YES;
}
 
-(void)setPlayerPosition:(CGPoint)position {
	_player.position = position;
}
 
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
 
    CGPoint touchLocation = [touch locationInView: [touch view]];		
    touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation];
    touchLocation = [self convertToNodeSpace:touchLocation];
 
    CGPoint playerPos = _player.position;
    CGPoint diff = ccpSub(touchLocation, playerPos);
    if (abs(diff.x) &gt; abs(diff.y)) {
        if (diff.x &gt; 0) {
            playerPos.x += _tileMap.tileSize.width;
        } else {
            playerPos.x -= _tileMap.tileSize.width; 
        }    
    } else {
        if (diff.y &gt; 0) {
            playerPos.y += _tileMap.tileSize.height;
        } else {
            playerPos.y -= _tileMap.tileSize.height;
        }
    }
 
    if (playerPos.x = 0 &amp;&amp;
        playerPos.x &gt;= 0 ) 
    {
            [self setPlayerPosition:playerPos];
    }
 
    [self setViewpointCenter:_player.position];
 
}

Первым делом, в методе init мы разрешаем обработку прикосновений. Затем переопределяем метод registerWithTouchDispatcher, чтобы зарегестрировать себя для обработки прикосновений. Это позволит вызывать методы ccTouchBegan/ccTouchEnded (вариант для единичного прикосновения), вместо методов ccTouchesBegan/ccTouchesEnded (для многих).

Вы можете спросить зачем вообще нам это нужно, ведь мы прекрасно пользовались методами ccTouchesBegan/ccTouchesEnded в туториале Как сделать простую игру для iPhone c помощью Cocos2D. Это правда, и в данном случае особой разницы нет. Как бы ты ни было, я хотел ознакомить всех с этим методом, если кто-то его еще не видел, потому, что он имеет два существенных преимущества (вот дословно из исходного кода Cocos2D):

  • “Нет необходимости иметь дело с объектами класса NSSet, работу по разделению касаний выполняет диспетчер. Вы получаете один UITouch за один вызов метода.”
  • “Вы можете “зарезервировать” UITouch вернув YES в ccTouchBegan. Обновления зарезвированных касаний посылаются только делегатам, зарезервировавшим их. Так что если происходит смещение/ окончание/ отмена касания вы можете быть уверены, что это ваше касание. Это освобождает вас от множества проверок при работе с множеством касаний.”

В любом случае, внутри ccTouchEnded мы получаем точку прикосновения и преобразуем ее в GL координаты как обычно. Что здесь нового, так это вызов [self convertToNodeSpace:touchLocation].

Это из-за того, что точка касания которую мы получаем имеет координаты относительно экрана (например 100, 100). Но мы могли уже проскроллить карту, так что относительно ее координаты будут соответствовать, например, (800, 800). Так что вызов этого метода сдвигает координаты, относительно сдвига слоя.

Далее, мы вычисляем расстояние между точкой касания и позицией игрока. Мы должны выбрать направление движения, так что первым делом мы решаем куда нам двигаться: вверх/вниз или влево/вправо на основании того,  какое значение из полученных больше – х или у.

Мы устанавливаем позицию игрока соответствующим образом, и затем устанавливаем центр обзора в точку позиции игрока, как было описано в последнем разделе.

Update: Обратите внимание, что необходимо сделать проверку того, что мы не передвигаем игрока за пределы карты!

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

Теперь наш ниндзя движется по карте!

Что дальше?

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

Вот образец проекта с кодом, разработанным на данный момент.

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

Таги: , , ,

Столкновения и сбор предметов: как сделать игру с тайловой графикой с помощью Cocos2D. Часть 2

Ммм, это было вкусно!

Ммм, это было вкусно!

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

В первой части туториала, мы рассмотрели как создается карта с помощью Tiled, как эта карта добавляется в игру, как скроллить карту, чтобы следовать за игроком и как использовать слои объектов.

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

Давайте вернемся к тому моменту, где мы остановились и сделаем нашу карту больше похожей на игру!

Тайловые карты и столкновения

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

Так что нам нужно придумать способ чтобы пометить некоторые тайлы, как “непроходимые”, чтобы игрок не мог перемещаться на них. Существует много возможных решений (включая использование объектных слоев), но я покажу вам новую технику, которая, на мой взгляд, достаточна эффективна и является хорошим хорошим упражнением – использование мета слоев и свойств слоя.

Давайте приступим! Включите Tiled снова, кликните “Слой  Добавить слой тайлов”, нозовите этот слой “Meta” и кликните ОК. Это слой, в который мы положим несколько фальшивых тайлов, чтобы отметить “специальные тайлы”.

Теперь нам надо добавить специальные тайлы. Кликните “КартаНовый набор тайлов”, выберите файл meta_tiles.jpg в вашей папке ресурсов и кликните Open. Задайте 1 для значений Отступ и Промежуток и кликните ОК.

Вы должны увидеть новую вкладку в панели “Наборы тайлов” для for meta_tiles. Откройте ее и вы увидите два тайла: красный и зеленый.

Tiled и мета тайлы

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

Убедитесь что выбран слой Meta, выберите stamp tool, выберите красный тайл и раскрасьте любой объект, который вы желаете сделать непроходимым для ниндзя. Когда вы закончите это будет похоже вот на что:

Tiled и тайлы для столкновений

Теперь тайлу необходимо задать свойство, пометить его так, чтобы мы у себя в коде понимали что этот тайл “непроходимый”. Кликните правой кнопкой на красном тайле в секции “Наборы тайлов” и кликните на “Свойства тайла”. Добавьте новое свойство для “Collidable”, установленное в “True”, вот так:

Свойства тайлов в Tiled

Сохраните карту и вернитесь в XCode. Внесите следующие изменения в HelloWorldScene.h:

// Внутри объявления класса HelloWorld
CCTMXLayer *_meta;
 
// после объявления класса
@property (nonatomic, retain) CCTMXLayer *meta;

Затем сделайте следующие изменения в HelloWorldScene.m:

// В самом начале implementation
@synthesize meta = _meta;
 
// В dealloc
self.meta = nil;
 
// В init, сразу после загрузки бэкграунда
self.meta = [_tileMap layerNamed:@"Meta"];
_meta.visible = NO;
 
// Добавьте новый метод
- (CGPoint)tileCoordForPosition:(CGPoint)position {
    int x = position.x / _tileMap.tileSize.width;
    int y = ((_tileMap.mapSize.height * _tileMap.tileSize.height) - position.y) / _tileMap.tileSize.height;
    return ccp(x, y);
}

Давайте остановимся здесь на секунду. Мы объявляем указатель на мета слой как обычно и получаем значение для него из карты. Обратите внимание, что мы делаем этот слой невидимым, поскольку мы не хотим видеть объекты этого слоя, они нам нужны только для того, чтобы пометить, что некоторый участок является непроходимым.

Затем мы добавляем новый вспомогательный метод для преобразует х и у координаты в “координаты тайла”. Каждый из тайлов имеет координаты, начиная с (0, 0) в верхнем левом углу и (49, 49) для нижнего правого (в нашем случае).

Координаты тайлов в Tiled

Кстати, этот скриншот из Java версии Tiled. Мне кажется, что функция отображения координат для тайлов еще не портирована в Qt версию.

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

Получить х координату легко – мы просто делим ее на ширину тайла. Чтобы получить у координату необходимо развернуться, потому что в Cocos2D (0, 0) это нижний левый угол, а не верхний левый.

Затем замените содержимое setPlayerPosition следующим:

CGPoint tileCoord = [self tileCoordForPosition:position];
int tileGid = [_meta tileGIDAt:tileCoord];
if (tileGid) {
    NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
    if (properties) {
        NSString *collision = [properties valueForKey:@"Collidable"];
        if (collision &amp;&amp; [collision compare:@"True"] == NSOrderedSame) {
            return;
        }
    }
}
_player.position = position;

Здесь мы преобразуем координаты игрока в координаты тайла. Затем мы используем функцию tileGIDAt в мета слое, чтобы получить GID в определенной координате тайла.

А что такое GID? Я думаю, GID означает “globally unique identifier” (глобальный уникальный идентификатор). В данном случае, лучше считать его просто идентификатором тайла, который мы используем.

Затем мы используем GID, чтобы посмотреть свойства тайла. Мы получаем словарь со свойствами и смотрим равен ли “Collidable” true. Если да, то выходим из функции, тем самым не меняя позицию игрока.

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

Скриншот, демонстрирующий невозможность продвижения сквозь стену

Динамическое модифицирование тайловой карты

Пока что наш ниндзя прекрасно проводит время в исследованиях, но этот мир немного скучноват. Здесь просто нечего делать!

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

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

Откройте Tiled, кликните “СлойДобавить слой тайлов”, назовите слой “Foreground” и кликните ОК. Убедитесь, что выбран слой Foreground и добавьте пару предметов для сбора на карту. Мне нравится использовать тайл, который похож на арбуз.

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

Теперь нам нужно отметить эти тайлы как собираемые, подобно тому как мы помечали тайлы как непроходимые. Выберите мета слой, переключитесь на meta_tiles и раскрасьте все собираемые тайлы в зеленый. Вам надо будет также кликнуть “СлойПоднять слой”, чтобы зеленый цвет был вам виден.

Раскрашивание собираемых предметов с помощью мета тайлов

Далее, нам надо добавиь свойство к тайлу, чтобы пометить его как собираемый. Кликните правой кнопкой на зеленом тайле в секции “Наборы тайлов”, кликните “Свойства тайла” и добавьте новое свойство, под названием “Collectable”, значение “True”.

Сохраните карту и возвращайтесь в Xcode. Сделайте следующие изменения в HelloWorldScene.h:

// Внутри объвления класса HelloWorld
CCTMXLayer *_foreground;
 
// После объявления класса
@property (nonatomic, retain) CCTMXLayer *foreground;

Затем внесите изменения в HelloWorldScene.m:

// В самом начале implementationn
@synthesize foreground = _foreground;
 
// В dealloc
self.foreground = nil;
 
// В init, сразу после загрузки бэкграунда
self.foreground = [_tileMap layerNamed:@"Foreground"];
 
//  в setPlayerPosition, сразу после if (collision &amp;&amp; [collision compare:@"True"] == NSOrderedSame)
NSString *collectable = [properties valueForKey:@"Collectable"];
if (collectable &amp;&amp; [collectable compare:@"True"] == NSOrderedSame) {
    [_meta removeTileAt:tileCoord];
    [_foreground removeTileAt:tileCoord];
}

Здесь все как обычно, инициализируем указатель на верхний слой. Что здесь нового, так это проверка тайла, на который передвигается игрок, имеет ли этот тайл свойство “Collectable”. Если да, мы используем метод removeTileAt, чтобы удалить тайл с мета слоя и с верхнего слоя.

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

Ниндзя собирается съесть арбуз

Создание счетчика

Наш ниндзя счастлив и сыт, но мы хотели бы знать, как много дынь он съел. Мы не хотим, чтобы он растолстел из-за нас.

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

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

Разумеется, двум нашим слоям понадобится способ для коммуникации – слой Hud должен быть в курсе о том, что ниндзя съел дыню. Есть много способов достичь этого, но мы будем использовать самый простой – слой HelloWorld будет содержать указатель на слой HelloWorldHud и вызывать в нем определенный метод.

Так что добавьте следующее в HelloWorldScene.h:

// Перед объявлением класса HelloWorld 
@interface HelloWorldHud : CCLayer
{   
    CCLabel *label; //класс CCLabel использовался в версии cocos2d v0.99.2, сейчас его уже нет, вместо него можно использовать CCLabelTTF
}
 
- (void)numCollectedChanged:(int)numCollected;
@end
 
// Внутри объявления класса HelloWorld 
int _numCollected;
HelloWorldHud *_hud;
 
// После объявления класса
@property (nonatomic, assign) int numCollected;
@property (nonatomic, retain) HelloWorldHud *hud;

И сделайте следующие изменения в HelloWorldScene.m:

// В самом верху файла
@implementation HelloWorldHud
 
-(id) init
{
    if ((self = [super init])) {
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        label = [CCLabel labelWithString:@"0" dimensions:CGSizeMake(50, 20)
            alignment:UITextAlignmentRight fontName:@"Verdana-Bold" 
            fontSize:18.0];
        label.color = ccc3(0,0,0);
        int margin = 10;
        label.position = ccp(winSize.width - (label.contentSize.width/2) 
            - margin, label.contentSize.height/2 + margin);
        [self addChild:label];
    }
    return self;
}
 
- (void)numCollectedChanged:(int)numCollected {
    [label setString:[NSString stringWithFormat:@"%d", numCollected]];
}
 
@end
 
// В самом начале секции implementation класса HelloWorld
@synthesize numCollected = _numCollected;
@synthesize hud = _hud;
 
// В dealloc
self.hud = nil;
 
// Добавьте к методу +(id) scene (прим. пер. - + (ССScene *) scene для более новой версии cocos2d), сразу перед return
HelloWorldHud *hud = [HelloWorldHud node];    
[scene addChild: hud];
 
layer.hud = hud;
 
// Добавьте внутри setPlayerPosition, в случае если тайл является собираемым
self.numCollected++;
[_hud numCollectedChanged:_numCollected];

Здесь ничего особенного. Наш второй слой унаследован от CCLayer и просто добавляет метку в нижний правый угол. Мы изменяем сцену добавлением второго слоя к ней и перед слою HelloWorld ссылку на Hud. Затем мы модифицируем слой HelloWorld вызовом метода в Hud, когда счет изменяется, чтобы он мог обновить состояние метки соответствующим образом.

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

Счетчик арбузов

Звуки и музыка

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

Просто внесите вот такие изменения в HelloWorldScene.m:

// В самом верху файла
#import "SimpleAudioEngine.h"
 
// В начале меода init класса HelloWorld 
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pickup.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"hit.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"move.caf"];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"TileMap.caf"];
 
// Для непроходимых тайлов
[[SimpleAudioEngine sharedEngine] playEffect:@"hit.caf"];
 
// Для собираемых тайлов
[[SimpleAudioEngine sharedEngine] playEffect:@"pickup.caf"];
 
// Перед тем, как задать игроку новую позицию
[[SimpleAudioEngine sharedEngine] playEffect:@"move.caf"];

Теперь наш ниндзя может тащиться под музыку пока ест!

Что дальше?

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

Вот копия Cocos2D игры с тайловой графикой которую мы разработали.

Если вам понравилась эта серия, то наши хорошие друзья и Geek and Dad разработали продолжение: Enemies and Combat: How To Make a Tile-Based Game with Cocos2D Part 3! Ознакомьтесь с ним чтобы узнать как вы можете расширить игру, добавив в нее врагов, звездочки и сцену победы / проигрыша!

Если у вас есть какие-либо идеи или предложения как можно эффективно использовать Tiled или карты на основе тайлов в Cocos2D, если вы использовали их или будете использовать их в своих проектах, пожалуйста пишите на форум!

Таги: , , , ,

28 December 2012

Core Graphics 101: Паттерны

Create a cool grip pattern effect with Core Graphics!

Создаём крутой паттерн с грип эффектом средствами Core Graphics!

Добро пожаловать на занятия под названием Core Graphics 101!В этой череде уроков мы узнаем на практических примерах как начать работать с помощью Core Graphics !
В уроках один, два, и три, мы узнали как кастомизировать табличное представление сначала и до самого финиша – пользуясь только средствами Core Graphics!
В уроке четвёртом, мы раскрыли как самому создать блестящую глянцевую UIButton опять же лишь за счет Core Graphics!
В этом уроке мы собираемся продемонстрировать вам как создать фон в стиле “grip”,  который стал довольно популярным и в настоящий день и используется во многих приложениях. Если вам предстояло его увидеть – признайтесь, что это выглядит достаточно круто!
На этом пути вы укрепите техники по рисовании дуг и теней, и узнаете как использовать встроенный шаблон для рисования Core Graphics.
И так клавиатуру в руки и поехали!

 

Приступаем к работе

Загрузите XCode,выберите “FileNew Project…”, выберите шаблон View-based Application, и нажмите “Choose…”. Назовите ваш проект “CoolPattern”, и нажмите Save.
Далее выберите группу Classes, перейдите в “FileNew File…”, выберите iOSCocoa Touch ClassObjective-C class, убедитесь, что выбран “Subclass of UIView”, и нажмите Next. Назовите файл CoolPatternView.m, убедитесь также, что выбрано  ”Also create CoolPatternView.h” , и нажмите Finish.
Следующий действием откройте CoolPatternView.m, и добавьте следующий код наверх файлика:

static inline double radians (double degrees) { return degrees * M_PI/180; }

Это всего лишь вспомогающая функция для перевода градусов в радианы, которая понадобится нам позже.
Далее раскомментируйте drawRect,и вставьте в этот метод следующий код:

CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
                                       blue:0.0 alpha:1.0].CGColor;
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);

Как обычно, это наш тестовый код для заполнения целого отображения красным цветов для тестовых целей.
Далее пришло время настроить CoolPatternViewController.xib.  Идея заключается в том, что мы хотим получить возможность прокрутки нашего фона чуть вверх и вниз и увидеть как он спружинит и придёт в исходное положение.
И так начнём, откройте ResourcesCoolPatternViewController.xib. Обратите внимание, что там указано отображение по умолчанию, но мы хотим установить UIScrollView взамен. Тогда удалите View, который установлен в XIB сейчас, и перенесите UIScrollView на его место. В Size Inspector(3-яя вкладка),  измените размер нового отображения на 320×480. Также зажмите control и нажмите при нажатой клавише на File’s Owner и держа кнопку в нажатом положении ведите к Scroll View, а затем установите Scroll View как View.
Еще одна деталь – в Attributes Inspector для UIScrollView, выберите “Always Scroll Vertically”. Иначе он не будет “скроллиться” по умолчанию, так как мы изменили размер нашей view.
Далее перетащите UIView в Scroll View сделав его потомком. Мы хотим, чтобы view отображалось даже когда пользователь скроллит выше высоты экрана, для этого в Size Inspector, меняем  y  -400, и высоту ставим 1280 ,чтобы у нас получилась невероятно высокая view, чтобы всегда оставаться в её пределах .
Затем переходим во вкладку Identity Inspector и меняем Class нашей view на CoolPatternView.
Еще один момент,  перетащите label  на новую дочернюю view, чтобы она стала частью Cool Pattern View, и добавьте какой-нибудь текст в центр экрана. В моём случае я также установил количествой линий равное 0, чтобы мой текст отображался в несколько рядов, выравнивание по середине, увеличил размер шрифта, настроил auto-sizing аттрибут, чтобы надпись оставалась посередине экрана после поворота устройства и установил белый цвет.
Скомпилируйте и запустите проект, и если полёт нормальный,  то вы увидите красную view, которую можно “скроллить” вверх и вниз и наблюдать как отображение отпружинивает обратно (при этом будучи красного цвета за пределами высоты экрана):
Our grip view mocked up.

Наш выход

Если вы посмотрите на увеличенный грип эффект, вы должно быть увидите, что это всего лишь шаблон:
Grip Effect Zoomed
Пунктирной линией я выделил шаблон, который повторяется. В результате здесь всего 2 круга, один слева сверху и другой диагональный справа, у каждого круга под ним небольшая тень.
Основываясь на материале, который мы уже изучили из этой серии уроков,  вы должно быть уже знаете решение для создания этого эффекта: создать прямоугольник для заполнения, выяснить как много раз вам следует повторить шаблон вдоль оси X и вдоль оси Y, и затем вызвать код, который нарисует круги/ тени в цикле.
Однако, когда имеешь дело с паттернами, это происходит в следующей последовательности: Core Graphics встроенный в API вызывает  рисование паттерна, что в результате облегчает нашу задачу и мы выигрываем по пунктам а) быстрее и проще спрограммировать б) выигрыш в производительности.И так давайте попробуем этот метод!

Drawing Patterns with Core Graphics

Замените содержимое метода drawRect следующим:

CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef bgColor = [UIColor colorWithHue:0 saturation:0 brightness:0.15 alpha:1.0].CGColor;
CGContextSetFillColorWithColor(context, bgColor);
CGContextFillRect(context, rect);
static const CGPatternCallbacks callbacks = { 0, &amp;MyDrawColoredPattern, NULL };
CGContextSaveGState(context);
CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace(context, patternSpace);
CGColorSpaceRelease(patternSpace);
CGPatternRef pattern = CGPatternCreate(NULL,
                                       rect,
                                       CGAffineTransformIdentity,
                                       24,
                                       24,
                                       kCGPatternTilingConstantSpacing,
                                       true,
                                       &amp;callbacks);
CGFloat alpha = 1.0;
CGContextSetFillPattern(context, pattern, &amp;alpha);
CGPatternRelease(pattern);
CGContextFillRect(context, self.bounds);
CGContextRestoreGState(context);

Первое что, этот метод делает, это заполняет весь прямоугольник фоновым цветом.
Далее, метод заполняет CGPatternCallbacks структуру, которая объявлена как эта:

struct CGPatternCallbacks {
   unsigned int version;
   CGPatternDrawPatternCallback drawPattern;
   CGPatternReleaseInfoCallback releaseInfo;
};
typedef struct CGPatternCallbacks CGPatternCallbacks;

И так, входными параметрами сперва будет 0,  затем указатель к нашей функции, с помощью которой мы будем рисовать паттерн, и NULL для функции, которая будет вызвана когда паттерн рисования окончательно выполнится.
Далее,  функция создаёт новую цветовую схему для паттерна, и устанавливает цветовую схему заливки к уже созданной цветовой схеме.
Следующая линия наиболее важная часть – создания объекта паттерна. Здесь мы объявлявем граници паттерна для рисования ( наши будут 24×24 точки), устанавливаем пробел между тайлами(в нашем случае 24 точки каждый), создаём связь к callback структуре, которую настроили ранее и еще пару других параметров.
Далее устанавливается заливка паттерна к паттерну объекта(а не цвету заливки), и просто заполняем прямоугольник.
Теперь давайте напишем “MyDrawColoredPattern” callback который на самом деле рисует паттерн. Добавьте следующее поверх drawRect метода:

void MyDrawColoredPattern (void *info, CGContextRef context) {
 
    CGColorRef dotColor = [UIColor colorWithHue:0 saturation:0 brightness:0.07 alpha:1.0].CGColor;
    CGColorRef shadowColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.1].CGColor;
 
    CGContextSetFillColorWithColor(context, dotColor);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, shadowColor);
    CGContextAddArc(context, 3, 3, 4, 0, radians(360), 0);
    CGContextFillPath(context);
    CGContextAddArc(context, 16, 16, 4, 0, radians(360), 0);
    CGContextFillPath(context);
 
}

Эта функцию устанавливает 2 цвета – полностью черный для точек, и белый с 0.1 альфа каналом для тени. Затем мы говорим, что хотим установить первый как цвет заполнения, и второй как цвет тени(установите 1 точку поверх заполнямой территории).
Затем мы просто рисуем два круга, используя функцию AddArc (круг это просто дуга в 360 градусов!), и затем заполняем весь контур.Один круг диагонально справа от другого.
Верите вы или нет,  на этом всё! Скомпилируйте и запустите ваш код,  и тогда вы увидите круто смотрящийся грип эффект!
Finished Grip Effect with Core Graphics Patterns

Core Graphics Паттерны и производительность

Как я упомянул ранее, Core Graphics паттерны невероятно быстрые. Чтобы показать насколько быстрые,  я сделал маленький тест, где я использовал 3 пути рисования:

  1. С паттернами, как мы сделали ранее
  2. Используя UIImage взамен паттерну
  3. Рисуя паттерн в цикле со множеством вызовов Core Graphics

Я затем добавил немного кода,  чтобы записать как долго drawRect отнимает времени для каждого метода (В версии с UIImage время для загрузки изображения включено). Здесь результаты для моего устройства:
Core Graphics Patterns Performance Comparison
Ничего себе! Неплохое дело, если ваш паттерн будет более сложный (такой как например рисование большего количества форм, нежели двух, как мы рисуем в данном случае) рисование паттерна всё равно происходит абсолютно нормально и при этом вы видите какой выигрыш в производительности, а всё потому, что код рисования вызывается всего лишь один раз и затем повторяется.
Обновлено: После некоторой дискуссии вокруг этой темы, если вы используете [UIColor colorWithPatternImage:...] вы можете получить аналогичные результаты,  как во время использования непосредственно Core Graphics паттернов .
И так если у вас имеется эффект, которые следует повторять в вашем проекте, аналогично нашему случаю, вы моожете смело использовать Core Graphics паттерны (или [UIColor colorWithPatternImage])!

Куда дальше?

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

Это что касается Core Graphics 101 series (по крайней мере на данный момент!), сейчас у вас есть понимание фундаментальных основ Core Graphics и теперь при возникновении необходимости,  или препятствия на вашем пути – вы смело сможете воспользоваться этими знаниями в ваших собственных проектах!
Если вы заинтересованы в Core Graphics,вы может быть также заинтересованы в изучении анимации. Если так то отличным местом, чтобы начать будет Урок:Как использовать UIView анимацию!

Таги: , , , ,

Core Graphics 101: Дуги и Контуры

Drawing Arcs, Oh My!

Рисуем дуги,о боже!

Это третяя часть нашей серии уроков, показывающих на практических уроках азы работы с Core Graphics API!
В первой части серии, мы узнали как рисуются линии, прямоугольники, градиенты средствами Core Graphics когда создавали фон ячейкам таблицы.
Во второй части серии, мы познакомились с рисованием теней и созданием эффекта блеска при кастомизации заголовков наших табличек.
В этой части мы окончательно покончим с нашими таблицами добавив собственный футер. Мы также научимся рисовать дуги, по-больше поработаем с клиппингом и контурами!
Если у вас до сих пор нету проекта, вы можете взять сэмпл кода  на котором мы остановились в прошлом уроке.

Приступаем к работе…

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


…ждём…

…ждём…

…ждём…

Tomato-San is angry!

Tomato-San is angry!

Что?! Вы до сих пор читаете тут?! Вы можете сделать это – попробуйте сами ! :]

Решение

В случае если у вас возникли какие-то проблемы, здесь приведено решение.
Убедитесь, что выбрана группа “Classes” под “Groups & Files”, переходим в “FileNew File…”, выберите iOSCocoa Touch Class, Objective-C class,убедитесь, что выбрано “Subclass of UIView”, и нажмите далее. Назовите файл “CustomFooter.m”, убедитесь,  что выбрано “Also create CustomFooter.h”, и нажмите “Finish”.
Переключитесь в CustomFooter.m, раскомментируйте метод drawRect,и замените контент следующим:

CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
    blue:0.0 alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);

Затем переключитесь в RootViewController.m и сделайте следующие изменения:

// В секции import
#import "CustomFooter.h"
// Добавьте следующие методы
-(CGFloat) tableView:(UITableView *)tableView 
    heightForFooterInSection:(NSInteger)section {
    return 15;
}
- (UIView *) tableView:(UITableView *)tableView 
    viewForFooterInSection:(NSInteger)section {
    return [[[CustomFooter alloc] init] autorelease];
}

Скомпилируйте и запустите проект, и если всё пойдёт хорошо, то вы увидите следующее:
Table View Footer Placeholder

Вернёмся к делу

И так теперь заполненная область на месте, всё как обычно, давайте приукрасим футер. Но прежде, чем начать, давайте освежим нашу память и подумаем о том, что нам предстоит сделать.
Zoomed image of Table View Footer
Отметим следующие детали о картинке выше:

  • Бумага имеет изящную дугу снизу.
  • К бумаге применён градиент от светло-серого к тёмно-серому.
  • До сих пор имеется светлая подсвеченая линия, идущая по краю бумаги.
  • Бумага откидывает тень, которая подчеркивает кривую дугу.

Очевидно, новое во всём этом – дуга. Давайте немного это обсудим.

Создание дуги – математика

Главное понять, дуга это всего лишь кривая линия, которая представляет из себя частичку круга. В нашем случае мы хотим, чтобы низ бумаги выглядел как небольшой кусочек  большого круга сверху:
Diagram of the circle that our arc is part of
Как вы видите на иллюстрации, дуга,  которую мы хотим нарисовать,  просто верхушка очень большого круга, с очень большим радиусом, от указанного начального угла (startAngle) к указанному конечному углу (endAngle).
И так, как нам объяснить Core Graphics что это за дуга? Мы собираемся использовать CGContextAddArc API, и на вход этой функции мы подадим три параметра:

  1. Центральная точка круга
  2. Радиус круга
  3. Начальная и конечная точки линии, которую мы собираемся нарисовать

Но черт побери, мы не знаем ровным счетом ничего об этом, и что нам теперь делать ?!
Это подходящий момент, когда математика придёт нам на помощь. Мы можем вычислить исходя из уже имеющихся у нас знаний!
Первое, что мы знаем это ширину таблички для которой мы хотим нарисовать дугу:
Diagram of the rectangle in which our arc will be drawn
Второе- интересная математическая теорема, называющаяся  Теорема о пересекающихся хордах , которая говорит о том, что если вы рисуете любые две линии которые объединяют 2 точки в круге (они называются хорды),произведения которых равны.
Diagram of the Intersecting Chord Theorem
Если вы хотите понять почему это так, то перейдите по ссылке выше – там находится забавное маленькое демо на яваскрипте, с которыми вы можете поиграться.
Вооруженные этими знаниями, взгляните, что получится, если мы нарисуем 2 хорды следующим образом:
Diagram of the lines we'll draw to figure out the radius...
И так мы нарисовали одну линию соединяющую нижнюю точку нашей дуги, и мы нарисовали одну линию из верхней точке дуги вниз к низу круга.
Если мы так сделаем, мы знаем a, b,и c, что позволяет нам получить d.
Тогда d будет: (a * b) / c. Подставим и получим:

// Просто подставляем...
CGFloat d = ((arcRectWidth/2) * (arcRectWidth/2)) / (arcRectHeight);
// Или еще проще...
CGFloat d = pow(arcRectWidth, 2)/(4*arcRectHeight);

И теперь мы знаем c и d, мы знаем, что радиус ровно половина, то есть : (c + d) / 2! Подставим :

// Просто подставляем...
CGFloat radius = (arcRectHeight + (pow(arcRectWidth, 2)/(4*arcRectHeight)))/2;
// Или еще проще...
CGFloat radius = (arcRectHeight/2) + (pow(arcRectWidth, 2)/(8*arcRectHeight));

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

CGPoint arcCenter = CGPointMake(arcRectTopMiddleX, arcRectTopMiddleY - radius);

И теперь раз мы знаем центральную точку, радиус, и прямоугольник под дугу, стало совсем просто вычислить начальный и конечный углы с некоторой аккуратностью:
Diagram of how to figure out the start and end angle for an arc from the radius...
И так мы начнём с того, что выясним угол показанный на диаграмме . Помните косинусы, тангенсы и котанкенгсы? Косинус угла есть отношение прилегающего катета к гипотенузе.

Другими словами , cosine(angle) = (arcRectWidth/2) / radius.Чтобы получить угол, нам просто нужно взять арккосинус:

CGFloat angle = acos(arcRectWidth/(2*radius));

И теперь зная этот угол, мы получим начальный и конечные углы:
Diagram of how to figure out the start and end angles...
Хорошо! Теперь мы понимаем как всё это работает, давайте соберём всё это вместе в функцию.
Между делом, я считаю что будет справедливым отметить, что есть куда более просто способ нарисовать дугу например используя CGContextAddArcToPoint функцию. Мы собираемся обсудить это в следующем уроке этой серии, но на данный момент я считаю важно понять математическую основу всего этого беспредела.

Рисуем дуги и создаём контур

Откройте Common.h и добавьте следующее вниз файла:

static inline double radians (double degrees) { return degrees * M_PI/180; }
CGMutablePathRef createArcPathFromBottomOfRect(CGRect rect, CGFloat arcHeight);

Первое, что мы добавили, это хелпер для конвертации градусов в радианы.
Затем… Подождите минутку… Что за CGMutablePathRef ?
Запомните,  что всего существует 2 шага для рисования в Core Graphics. Первый: вы обозначаете траекторию, затем вы заливаете эту траекторию.
До нынешнего момента, мы просто добавляли траекторию, которую мы хотели очертить/нарисовать к контексту с такими функциями как  CGContextMoveToPoint, CGContextAddLineToPoint, CGContextAddRect, и т.д. и затем заполняем это потом с помощью CGContextStrokePath или CGContextFillPath.
Иногда мы так и будем поступать, следовать вышеперечисленным действиям, сначала добавляя путь к контексту и затем заливать это с вызовом функцию, такой как CGContextFillRect ,например.
Но сейчас, заместо добавления пути непосредственно к контексту, мы собираемся сохранить путь в специальной для этого переменной.Теперь переиспользовать  контур несколько раз гораздо проще, заместо вызова одинаковой функции снова и снова.
Сделать многоразовый путь очень просто – вы просто вызываете CGPathXXX, взамен CGContextXXX.
Давайте взглянем на то, как это работает.Добавьте следующую новую функцию вниз Common.m:

CGMutablePathRef createArcPathFromBottomOfRect(CGRect rect, CGFloat arcHeight) {
 
    CGRect arcRect = CGRectMake(rect.origin.x, 
        rect.origin.y + rect.size.height - arcHeight, 
        rect.size.width, arcHeight);
 
    CGFloat arcRadius = (arcRect.size.height/2) + 
        (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
    CGPoint arcCenter = CGPointMake(arcRect.origin.x + arcRect.size.width/2, 
        arcRect.origin.y + arcRadius);
 
    CGFloat angle = acos(arcRect.size.width / (2*arcRadius));
    CGFloat startAngle = radians(180) + angle;
    CGFloat endAngle = radians(360) - angle;
 
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, arcCenter.x, arcCenter.y, arcRadius, 
        startAngle, endAngle, 0);
    CGPathAddLineToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPathAddLineToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect));
    return path;    
 
}

И так, эта функция принимает прямоугольник всей области, и плавающую точку параметра, отвечающего за то, насколько большой будет дуга(которая должна быть внизу прямоугольника). Мы вычисляем прямоугольник с дугой по этим двум параметрам.
Затем вы узнаём радиус, центр, начальный и конечный углы с помощью математики, приведённой выше.
Далее, мы создаём наш путь. Путь будет совпадать с дугой, плюс линию по краям прямоугольника над дугой.
Сначала мы создаём многоразовый путь с помощью CGPathCreateMutable. Далее, мы используем CGPathXXX  взамен CGContextXXX.
Мы добавили нашу дугу с помощью CGPathAddArc, в которую приходят все те переменные,  которые мы уже вычислили, затем закрываем путь, нарисовав линии к каждой точке.
Давайте попробуем! Сделайте следующие изменения в CustomFooter.m:

// В верху файла
#import "Common.h"
// Внутри initWithFrame
self.opaque = YES;
self.backgroundColor = [UIColor clearColor];
// Замените содержимое метода drawRect следующим:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef whiteColor = [UIColor colorWithRed:1.0 green:1.0 
    blue:1.0 alpha:1.0].CGColor; 
CGColorRef lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 
    blue:230.0/255.0 alpha:1.0].CGColor;
CGColorRef darkGrayColor = [UIColor colorWithRed:187.0/255.0 green:187.0/255.0 
    blue:187.0/255.0 alpha:1.0].CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
    blue:0.2 alpha:0.5].CGColor;
CGFloat paperMargin = 9.0;
CGRect paperRect = CGRectMake(self.bounds.origin.x+paperMargin, 
                        self.bounds.origin.y,
                        self.bounds.size.width-paperMargin*2, 
                        self.bounds.size.height);
CGRect arcRect = paperRect;
arcRect.size.height = 8;
CGContextSaveGState(context);
CGMutablePathRef arcPath = createArcPathFromBottomOfRect(arcRect, 4.0);
CGContextAddPath(context, arcPath);
CGContextClip(context);            
drawLinearGradient(context, paperRect, lightGrayColor, darkGrayColor);
CGContextRestoreGState(context);
CFRelease(arcPath);

Ок, сначала мы устанавливаем отображению непрозрачность и с чистым фоновым цветом по умолчанию.
Отметим, что на самом деле мы можем установить это в initWithFrame  и это будет вызываться даже несмотря на то,  что мы вызываем  ”init” метод в RootViewController.
Далее мы делаем уже привычные для нас Core Graphics настройки (контекст, цвет) и создаём ограниченную коробку (bounding box, одно из свойств объектов)для всей бумажной области целиком (запомните, нам нужен небольшой отступ, что все линии совпали) и область,  которую мы хотим превратить в дугу (верхняя часть прямоугольника, под которой будет тень и пустое пространство).
Затем, мы получаем путь бумажной области (с дугой внизу),  вызывая наш метод createArcPathFromBottomOfRect,  который мы только что написали. Мы можем добавить путь к нашему контексту, и сделать клип к этому пути.

Всеё дальнейшее рисование будет ограничено этим контуром. И так мы хотим использовать метод the drawLinearGradient , который мы написали ранее и voilà!
Одна последняя деталь, мы освобождаем путь, когда мы заканчиваем с ним возиться с помощью CFRelease.
Скомпилируйте и запустите ваш проэкт, и если всё пойдёт гладко, то вы увидите следующее:
Table Footer, First Draft
Выглядит довольно прилично, но мы чуток отполируем табличку.

Клиппинг, контуры, и четно-нечетный принцип

Далее добавьте следующее вниз метода drawRect (но перед вызовом CFRelease):

CGContextSaveGState(context);
CGPoint pointA = CGPointMake(arcRect.origin.x, 
    arcRect.origin.y + arcRect.size.height - 1);
CGPoint pointB = CGPointMake(arcRect.origin.x, arcRect.origin.y);
CGPoint pointC = CGPointMake(arcRect.origin.x + arcRect.size.width - 1, 
    arcRect.origin.y);
CGPoint pointD = CGPointMake(arcRect.origin.x + arcRect.size.width - 1, 
    arcRect.origin.y + arcRect.size.height - 1);
draw1PxStroke(context, pointA, pointB, whiteColor);
draw1PxStroke(context, pointC, pointD, whiteColor);    
CGContextRestoreGState(context);

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

CGContextAddRect(context, paperRect);
CGContextAddPath(context, arcPath);
CGContextEOClip(context);
CGContextAddPath(context, arcPath);
CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor);
CGContextFillPath(context);

Ок, далее есть новый и очень важный концепт,  на котором основана данная техника, и так давайте разъясним этот момент.

Вспомните из предыдущих уроков, что для рисования тени вы включали функцию рисования тени, затем заполняли контур. Core Graphics заполнит контур, а также нарисует подходящую тень под ним.
Но здесь, мы уже заполнили наш контур градиентом – так что нам не нужно перерисовывать эту область цветом снова.
Хорошо, это звучит как работа для клиппинга! Мы можем установить клиппинг так, что Core Graphics будет только рисовать часть внешней бумажной области. Затем мы можем сказать ему, чтобы залить бумажную область и нарисовать тени и бумажная область заливки будет проигнорирована(из-за клиппинга),но тень будет отображаться сквозь.
Но у нас нету контура для этого – единственный контур, который у нас имеется, только для бумажной области, не для той, что за её пределами…
К счастью, мы можем просто получить контур для внешней области, основываясь на внутренней,через удобную гибкость Core Graphics. Вам нужно добавить более, чем один путь к контексту, и затем вызвать “CGContextEOClip”.
Когда вы вызваете более, чем один контур к контексту, Core Graphics нужно определить, какие точки нужно залить, а какие нет. Например, у вас есть форма в виде пончика(мммм, пончики!) в котором внешне залито, а внутри нет.
Вы можете применить различные алгоритмы,  чтобы позволить Core Data узнать как их связать. Один из этих алгоритмов “EO”, или четно-нечетный принцип. Это означает,  что для каждой точки, Core Graphics будет рисовать линию от этой точки во вне от рисуемой области . Если линии пересекаются,  нечетное число точек будет залить, иначе они не будут залиты.
Далее приведена диаграмма, показывающая это из Quartz2D гайда по программированию:
Even Odd Rule Diagram
И так, вызывая “EO”, мы говорим Core Graphics, что даже если мы добавили 2 контура к нашему контексту, он должен рассмотреть это как 1 контур следую алгоритму. Тогда внешняя часть ( вся бумажная область,то есть paperRect), будет заполнена, но внутренния (контур дуги, arcPath) не будет заполнена.
И затем мы говорим Core Graphics сделать клип к этому контору, то есть нарисовать во внешней области.
Так как у нас есть заклипенная настроенная область,  мы добавили контур для дуги, установили тень, и заполнили дугу. Конечно, больше нам не осталось ничего заполнять ( так как всё нужное заклиппено), но тень будет нарисована во внешней области!
Скомпилируйте и запустите проект, и если всё пойдёт хорошо, вы увидите тень под футером:
Table Footer With Shadows

Последние мазки

Наша табличка выглядит довольно хорошо, но давайте уделим немного времени для завершающих шагов над приложением.
Во-первых, давайте пофиксим, возможно, худшую часть: последняя ячейка таблицы не должна рисовать разделитель между ячейками. К тому же,  нету разницы между выбранной ячейкой и нет, и цвет текста сменим на безобразно белый.
И так давайте внесём изменения в CustomCellBackground.h:

// Внутри @interface
BOOL _lastCell;
BOOL _selected;
// После @interface
@property  BOOL lastCell;
@property  BOOL selected;

Затем сделаем следующием изменения в CustomCellBackground.m:

// После @implementation
@synthesize lastCell = _lastCell;
@synthesize selected = _selected;
// Замените первый вызов drawLinearGradient следующим:
if (_selected) {
    drawLinearGradient(context, paperRect, lightGrayColor, separatorColor);
} else {
    drawLinearGradient(context, paperRect, whiteColor, lightGrayColor);
}
// Замените оставшийся код этимWrap the rest of the code with the following
if (!_lastCell) {
    // Код рисующий 1 пиксельную черту и разделитель
}
// Затем добавьте следующее
else {
 
    CGContextSetStrokeColorWithColor(context, whiteColor);
    CGContextSetLineWidth(context, 1.0);
 
    CGPoint pointA = CGPointMake(paperRect.origin.x, 
        paperRect.origin.y + paperRect.size.height - 1);
    CGPoint pointB = CGPointMake(paperRect.origin.x, paperRect.origin.y);
    CGPoint pointC = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, 
        paperRect.origin.y);
    CGPoint pointD = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, 
        paperRect.origin.y + paperRect.size.height - 1);
 
    draw1PxStroke(context, pointA, pointB, whiteColor);
    draw1PxStroke(context, pointB, pointC, whiteColor);
    draw1PxStroke(context, pointC, pointD, whiteColor);
 
}

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

// Внутри tableView:cellForRowAtIndexPath, внутри условия cell == nil:
((CustomCellBackground *)cell.selectedBackgroundView).selected = YES;
// После создания cell, внутри условия indexPath.section == 0:
((CustomCellBackground *)cell.backgroundView).lastCell = 
        indexPath.row == _thingsToLearn.count - 1;
((CustomCellBackground *)cell.selectedBackgroundView).lastCell = 
        indexPath.row == _thingsToLearn.count - 1;
// После создания cell, else условие:
((CustomCellBackground *)cell.backgroundView).lastCell = 
        indexPath.row == _thingsLearned.count - 1;
((CustomCellBackground *)cell.selectedBackgroundView).lastCell = 
        indexPath.row == _thingsLearned.count - 1;
// В конце, перед return cell:
cell.textLabel.highlightedTextColor = [UIColor blackColor];

Скомпилируйте и запустите, и вы увидите , что всё стало гораздо лучше:
Table View Finishing Touches
Далее, давайте установим фон и панель навигации, чтобы смотрелось получше. Откройте MainWindow.xib,расширьте Navigation Controller, и кликните на Navigation Bar. В  Attributes Inspector,измените оттенок цвета на nice dark как указано:
Setting the tint color in a navigation bar
Затем,  двойной клик на Window и перетащите UIImageView на window и сделайте так, чтобы это отображение покрывало всё свободное пространство. Установите на изображение, картинку, которая вам нравится , или вы можете скачать это изображение сделанное jaylopez из sxc.hu.
Последнее, что нам осталось сделать, это отключить установленный по умолчанию фон для table view, чтобы наше изображение показывало сквозь него. Откройте RootViewController.xib, выберите Table View, и измените Background down в View section ,чтобы очистить фоновый цвет:
Removing the striped grouped table view background color
Сохраните ваш XIBs и скомпилируйте, а затем запустите проект и если у вас не возникнет ошибок, то вы увидите следующее:
Finished custom drawn table view
И вот чего мы хотели! Полностью собственноручно нарисованная табличка с помощью Core Graphics!

Куда после ?

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

Таги: , , , , ,

Core Graphics 101: Тени и глянец

Custom draw the header of this table!

 Ручная отрисовка заголовка таблицы!

Это вторая часть нашей серии уроков, которые показывают на практических примерах как работать с  Core Graphics API !
В первой части, мы узнали как нарисовать линии, прямоугольники, градиенты через создание фона для ячеек таблицы.
В этом уроке мы пойдём дальше и узнаем о кастомизации заголовков таблицы. При освоении материала мы укрепим наши знания в Core Graphics и узнаем как нарисовать тень и  создать эффект глянца !
Если у вас его еще нету, возьмите пример проекта на котором мы закончили в прошлом уроке.

 

Возьмёмся за работу

Следующим шагом в кастомизации нашей таблицы будет украшение заголовка таблицы. Для начала давайте повторим последовательность действий, совершенную в прошлом уроке – для начала просто заполним нужную нам территорию красным цветом, только чтобы убедится, что всё это работает.
И так убедитесь что выбрана группа “Classes”над “Groups & Files”,идём в “FileNew File…”,выбираем iOSCocoa Touch Class, Objective-C class, убеждаемся, что выбрана “Subclass of UIView” , и тыкает кнопку Next. Назовите файл “CustomHeader.m”, убедитесь, что такое создаеётся заголовочный файл: “Also create CustomHeader.h”,жмём “Finish”.
Переключитесь к CustomHeader.m, раскомментируйте метод drawRect, и замените контент следующим кодом:

CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
    blue:0.0 alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);

Вы должно быть уже знакомы с этими строками, в прошлый раз мы делали тоже самое !
И так давайте установим это как наше отображение заголовка таблицы. Переключитесь на RootViewController.m и сделайте следующие модификации :

// в секции импорта
#import "CustomHeader.h"
// добавьте следующие методы
- (UIView *) tableView:(UITableView *)tableView 
    viewForHeaderInSection:(NSInteger)section {
    CustomHeader *header = [[[CustomHeader alloc] init] autorelease];        
    return header;
}
-(CGFloat) tableView:(UITableView *)tableView 
    heightForHeaderInSection:(NSInteger)section {
    return 50;
}

Чтобы замените view загалочка в table view, всё, что мы должны сделать – реализовать метод viewForHeaderInSection. Нам также нужно реализовать метод heightForHeaderInSection ,тогда табличка будет знать насколько высокий нужно сделать заголовок .
Скомпилируйте и запустите проект (cmd+r), и если всё пошло хорошо, вы должны увидеть следующее:
Placeholders for Custom Header
Отлично – теперь давайте сделаем эту область более привлекательной !

Наша цель

Давайте освежим нашу память по поводу нашей цели. Здесь мы видим увеличенный заголовок, который мы хотим в итоге получить:
Zoomed in view of Header
Отметим следующие о картинке выше:

  • Синяя область это градиент от светло синего к тёмно синему.
  • Наверху синей области используется глянцевый эффект.
  • Также там имеется тёмно синяя граница, идущая по краю синей области.
  • Также обратите внимание на незначительную тень под заголовком.
  • Верх “бумажной” области нарисован, для плавного перехода к заголовку.
  • На Table view controller нужно будет установить цвет и label для каждого отображения.

Большинство из этого хорошо освежит вашу память после предыдущего урока и урока о Custom UIViews, но мы узнаем несколько новых финтов при создании заголовка для наших таблиц, и так, поехали!

Для начала настройка

Прежде всего нужно сделать некоторые основные настройки. Мы собираеимся разрешить табличкам выставлять цвет заголовка и какой-нибудь текст для отображения, и укажем прямоугольники, которые мы собираемся рисовать, точнее разные частички одного заголовка (такие как сам заголовок, небольшой кусочек бумаги под ним для создания иллюзии).
Приступим. Сделайте следующие изменения в CustomHeader.h:

// Внутри @interface
UILabel *_titleLabel;
UIColor *_lightColor;
UIColor *_darkColor;
CGRect _coloredBoxRect;
CGRect _paperRect;
// После @interface
@property (retain) UILabel *titleLabel;
@property (retain) UIColor *lightColor;
@property (retain) UIColor *darkColor;

Затем сделайте следующие изменения в CustomHeader.m:

// В секции import
#import "Common.h"
// После @implementation
@synthesize titleLabel = _titleLabel;
@synthesize lightColor = _lightColor;
@synthesize darkColor = _darkColor;
// Добавьте новые методы
- (id)init {
    if ((self = [super init])) {
        self.backgroundColor = [UIColor clearColor];
        self.opaque = NO;
        self.titleLabel = [[[UILabel alloc] init] autorelease];
        _titleLabel.textAlignment = UITextAlignmentCenter;
        _titleLabel.opaque = NO;
        _titleLabel.backgroundColor = [UIColor clearColor];
        _titleLabel.font = [UIFont boldSystemFontOfSize:20.0];
        _titleLabel.textColor = [UIColor whiteColor];
        _titleLabel.shadowColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
        _titleLabel.shadowOffset = CGSizeMake(0, -1);
        [self addSubview:_titleLabel];
        self.lightColor = [UIColor colorWithRed:105.0f/255.0f green:179.0f/255.0f 
            blue:216.0f/255.0f alpha:1.0];
        self.darkColor = [UIColor colorWithRed:21.0/255.0 green:92.0/255.0 
            blue:136.0/255.0 alpha:1.0];        
    }
    return self;
}
-(void) layoutSubviews {
 
    CGFloat coloredBoxMargin = 6.0;
    CGFloat coloredBoxHeight = 40.0;
    _coloredBoxRect = CGRectMake(coloredBoxMargin, 
                                 coloredBoxMargin, 
                                 self.bounds.size.width-coloredBoxMargin*2, 
                                 coloredBoxHeight);
 
    CGFloat paperMargin = 9.0;
    _paperRect = CGRectMake(paperMargin, 
                            CGRectGetMaxY(_coloredBoxRect), 
                            self.bounds.size.width-paperMargin*2, 
                            self.bounds.size.height-CGRectGetMaxY(_coloredBoxRect));
 
    _titleLabel.frame = _coloredBoxRect;
 
}
// Замените drawRect следующим кодом
- (void)drawRect:(CGRect)rect {
 
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
        blue:0.0 alpha:1.0].CGColor;
    CGColorRef greenColor = [UIColor colorWithRed:0.0 green:1.0 
        blue:0.0 alpha:1.0].CGColor;
 
    CGContextSetFillColorWithColor(context, redColor);
    CGContextFillRect(context, _coloredBoxRect);
 
    CGContextSetFillColorWithColor(context, greenColor);
    CGContextFillRect(context, _paperRect);
}
// Внутри dealloc
[_titleLabel release];
_titleLabel = nil;
[_lightColor release];
_lightColor = nil;
[_darkColor release];
_darkColor = nil;

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

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

Мы также должны быть уверены, в том, что наше отображение прозрачное, потому что мы хотим видеть фон через наш label.
layoutSubviews вызывается, когда наша view меняет свой размер. Здесь мы добавляем код для вычисления прямоугольника, для которого мы будем рисовать цветной бокс, и где будем рисовать бумагу. Собственно всё в этом методе просто вычисления.
Окончательно, мы просто добавляем цвет к каждому из “боксов” в drawRect с разными цветами, дабы убедится, что мы произвели правильные вычисления.
Один последний шаг – сделайте следующий модификации в RootViewController.m:

// Внутри tableView:viewForHeaderInSection, перед return
header.titleLabel.text = [self tableView:tableView titleForHeaderInSection:section];

Скомпилируйте и запустите приложение, и вы должны увидеть следующее:
Header colored with subrects to draw
Приближаемся к заветной цели! Как вы видите, раскрашивание прямоугольников в данном случае довольно полезная примочка!

Рисуем падающие тени

Примемся за рисование,  мы будем рисовать снизу вверх. И так для начала мы нарисуем бумагу, затем тень и только потом цветной бокс.
Нарисовать бумагу просто – мы всего лишь заполним прямоугольник белым цветом.
Но как мы нарисуем тени ? Хорошо, в Core Graphics, чтобы нарисовать тень, вы просто вызываете функцию для включения рисования теней и затем указываете контур. Тень будет нарисована под указанным путём с параметрами, которые вы указали – независимо от формы контура!

И так давайте взглянем как это работает . Замените контент метода drawRect с указанным ниже:

CGContextRef context = UIGraphicsGetCurrentContext();    
CGColorRef whiteColor = [UIColor colorWithRed:1.0 green:1.0 
    blue:1.0 alpha:1.0].CGColor;
CGColorRef lightColor = _lightColor.CGColor;
CGColorRef darkColor = _darkColor.CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
    blue:0.2 alpha:0.5].CGColor;   
 
CGContextSetFillColorWithColor(context, whiteColor);
CGContextFillRect(context, _paperRect);
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor);
CGContextSetFillColorWithColor(context, lightColor);
CGContextFillRect(context, _coloredBoxRect);
CGContextRestoreGState(context);

Пропустив цвета и рисование бумаги (на данный момент вы уже должны понимать как это происходит) , мы вызываем функцию CGContextSetShadowWithColor чтобы установить тени для рисования.
Первый параметр это отступ тени.Здесь мы указываем нарисовать 2 точки под данным контуром.
Следующий параметр это туманность тени. Параметр 0 будет означать острые края, чем больше значение, тем сглаженней будут края. Мы устанавливаем значение 3 для мягкой огранки.
В итоге мы указали цвет тени. Отметим, что мы используем серый цвет в альфа каналом в 0.5 (полу прозрачность), из которой следует более реалистичная тень.
После установки теней, мы заполняем цветной прямоугольный бокс светлым цветом. Факт заполнения цветом этого пути послужит причиной создания тени.
Только одна деталь – так как мы теперь действительно используем заданный цвет,  давайте модифицируем RootViewController.m  для изменения цвета в секции 1:

// Внутри tableView:viewForHeaderInSection, перед return
if (section == 1) {
    header.lightColor = [UIColor colorWithRed:147.0/255.0 green:105.0/255.0 
        blue:216.0/255.0 alpha:1.0];
    header.darkColor = [UIColor colorWithRed:72.0/255.0 green:22.0/255.0 
        blue:137.0/255.0 alpha:1.0];
}

Скомпилируйте и запустите проект, и вы должны увидеть следующее:
Headers with Shadow
Вау – выглядит очень замечательно! Но давайте добавим немного глянца с помощью градиента, блеска и верхней границы.

Добавление эффекта блеска

Зацените несколько красивых рабок сделанных Matt Gallagher и Michael Heyeck .
На мой глаз, чтобы придать блестящий эффект, вы можете можете использовать градиентную альфа маску, которая намного проще для понимания и кода, нежели харкорный путь создания этого эффекта собственноручно, и так давайте попробуем этот метод.
Это что-то, что мы будем использовать несколько позже, и так добавьте код в наш Common файл. Откройте Common.h и напишите:

void drawGlossAndGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef endColor);

Следующее добавьте в Common.m:

void drawGlossAndGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef endColor) {
 
    drawLinearGradient(context, rect, startColor, endColor);
 
    CGColorRef glossColor1 = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.35].CGColor;
    CGColorRef glossColor2 = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.1].CGColor;
 
    CGRect topHalf = CGRectMake(rect.origin.x, rect.origin.y, 
        rect.size.width, rect.size.height/2);
 
    drawLinearGradient(context, topHalf, glossColor1, glossColor2);
 
}

И так эта рутина позволяет нарисовать градиент поверх прямоугольника от начального цвета до цвета конечного,и затем добавить блеск на верхнюю половину.
Чтобы нарисовать градиент, мы вспоминаем рутину, которой уже пользовались не раз.
Затем чтобы нарисовать блеск, мы просто рисуем другой градиент поверх этого,  от довольно прозрачного (белый с 0.35 альфа) к очень прозрачному (белый с 0.1 альфа).
Просто, не правда ли? Даватйе разобъём это и посмотрим как оно работает. Возвращаемся назад в CustomHeader.m и добавляем следующий код вниз метода drawRect:

drawGlossAndGradient(context, _coloredBoxRect, lightColor, darkColor);  
// Рисуем черту
CGContextSetStrokeColorWithColor(context, darkColor);
CGContextSetLineWidth(context, 1.0);    
CGContextStrokeRect(context, rectFor1PxStroke(_coloredBoxRect));

Здесь мы вызываем уже известный нам код и затем рисуем линию шириной в 1 пиксель вокруг прямоугольника с тёмным цветом, прямо как в предыдущем уроке.
Скомпилируйте и запустите приложение, давайте глянем, что вышло:
Headers with Gloss
Я думаю это выглядит очень круто, как вы считаете ?

Куда дальше ?

Здесь лежит пример проекта со всем написанным нами кодом в этом уроке.
В этот момент вы должны стремится создать свои собственные вещи с помощью Core Graphics – подумайте о том как можно это сделать!
И хорошая новость – это не конец новому с Core Graphics! В следующем уроке мы украсим эту красивую табличку нашим собственным футером(footer), в которую войдёт улучшенное рисование арок с Core Graphics и сделаем еще пару окончательных штрихов!

Таги: , , , , ,

Core Graphics 101: Линии, прямоугольники и градиенты.

Welcome to Core Graphics 101!

Добро пожаловать в Core Graphics 101!

 

 

Core Graphics действительно крутой API для iOS. Как разработчик, вы можете использовать его для кастомизаци вашего программного интерфейса с помощью парочки действительно изящных трюков – зачастую даже не нуждаясь для этого в художнике!
Но для многих ios разработчиков  Core graphics по началу может быть чем-то пугающим, так как это довольно большой API, и имеет много подводных камней на пути по достижению успеха.
И так в этой серии уроков мы собираемся погрузиться в этот загадочный мир Core Graphics и разобрать его шаг за шагом, представив вам все его тонкости в серии практических упражнений – начиная с красивой table view с помощью Core Graphics!
TableView, который мы будем делать, в результате будет выглядеть как тот,  что на скриншоте.  Вдохновение для конкретно этого дизайна пришло из Bills,красиво оформленного приложения, созданного никем иным как PoweryBase.Это довольно крутое приложение, так что вам следует его оценить!
В этой первой статье мы будем работать над созданием красивого изображением каждой ячейки в tableView с помощью Core  Graphics,то есть над table view cell.Мы раскроем с чего начать, используя Core Graphics, как заполнить  и очертить прямоугольник, как нарисовать градиенты, и что делать с проблемами, возникшими от линии в 1 пиксель шириной.
В следующих статьях из этой серии мы будем работать над украшением оставшейся части приложения – то есть header,footer для table view, и закончим добавив немного шарма нашей табличке.
И так приступим и давайте получим удовольствия с помощью core graphics !

Приступая к работе

 

Приступая к работе настроим проект с каркасом табличного представления, которые мы хотим кастомизировать.
Откройте XCode, выберите шаблон Navigation-based Application,и назовите ваш проект “CoolTable”. Скомпилируйте и запустите проект только для того, чтобы убедится, что пустая таблица появилась при старте:
Blank Table View

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

// Внутри @interface
NSMutableArray *_thingsToLearn;
NSMutableArray *_thingsLearned;
// После
@property (copy) NSMutableArray *thingsToLearn;
@property (copy) NSMutableArray *thingsLearned;

Всё, что мы делаем здесь – добавляем 2 массива, в которые мы собираемся добавить строки(strings) для представления данных, которые будут отображены в этих двух секциях нашей таблицы.
Теперь переключитесь на RootViewController.m и сделайте следующие изменения:

// После @implementation
@synthesize thingsToLearn = _thingsToLearn;
@synthesize thingsLearned = _thingsLearned;
// Раскомментируйте viewDidLoad и добавьте следующее:
self.title = @"Core Graphics 101";
self.thingsToLearn = [NSMutableArray arrayWithObjects:@"Drawing Rects",
    @"Drawing Gradients", @"Drawing Arcs", nil];
self.thingsLearned = [NSMutableArray arrayWithObjects:@"Table Views",
    @"UIKit", @"Objective-C", nil];
// Раскомментируйте shouldAutorotateToInterfaceOrientation и измените возвращающее значение на следующее:
return YES;
// Измените return значение в  numberOfSectionsInTableView на:
return 2;
// Измените return значение в  tableView:numberOfRowsInSection на:
if (section == 0) {
    return _thingsToLearn.count;
} else {
    return _thingsLearned.count;
}
// Внутри tableView:cellForRowAtIndexPath, после комментария "Configure the cell":
NSString *entry;
if (indexPath.section == 0) {
    entry = [_thingsToLearn objectAtIndex:indexPath.row];
} else {
    entry = [_thingsLearned objectAtIndex:indexPath.row];
}
cell.textLabel.text = entry;
// Внутри viewDidUnload
self.thingsToLearn = nil;
self.thingsLearned = nil;
// Внутри dealloc
[_thingsToLearn release];
_thingsToLearn = nil;
[_thingsLearned release];
_thingsLearned = nil;
// Добавьте следующий новый метод
-(NSString *) tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
   if (section == 0) {&gt;
        return @"Things We'll Learn";
    } else {
        return @"Things Already Covered";
    }
}

 

Ок отлично – теперь у нас есть некоторые данные! Скомпилируйте и запустите проект, и вы должны увидеть что-то вроде этого:
Table View with Plain Style
Однако, если вы прокрутите таблицу чуток вверх или вниз, то несомненно заметите, что заголовок плавает при прокрутке секций:
Table View with Plain Style - Floating Headers
Это стандартное поведение table view у которой установлен стиль “plain”(простой). Однако, от стиля с которым нам предстоит работать мы не хотим такого поведения, нам не нужно, чтобы заголовок размещался таким образом- нам нужно, чтобы они оставались со строками как отдельный блок.И к счастья, такое поведение табличного отображения устанавливается со стилем “grouped”(сгруппированы)!

И так переключим стиль. Откроем RootViewController.xib, кликнем на   Table View в xib, и установим стиль “Grouped”:
Table View Style Setting
Великолепно!сохраните RootViewController.xib и вернёмся к проекту, и теперь у нас есть насыщенная(но довольно обыденная)табличка:
Table View with Grouped Style
И так давайте сделаем нашу табличку более привлекательной используя Core Graphics! Но перед тем как начать, давайте обсудим то, что у нас должно получится.

Анализируем стиль таблицы

Для получения итогового результата, мы собираемся разрисовать tableView в трёх разных секциях:header(шапка таблицы), cells(ячейки), footer(подвал):
Table View Analyzed
В этой статье, мы будем рисовать ячейки, и так давайте взглянем по-точнее как они выглядят:
Table View Cells Zoomed

Обратим внимание на несколько вещей здесь:

  • Ячейки покрыты градиентом от белого к светло серому.
  • Каждая ячейка имеет белый край по всей границе ячейки для обеспечения точности её определения(за исключением последней ячейки, у который лишь белые границы по бокам)
  • Каждая ячейка имеет одну серую линию для её отделения от следующей ячейки (за исключением последней ячейки).

Существует небольшой отступ от фактического края,чтобы подчеркнуть падающую тень от заголовка.
Кстати – это всё создаёт иллюзию падающего света, вы с этим дизайном столкнётесь еще не раз во всяких разных приложениях и будущих статьях серии. Возьмём заголовок, верх яркий, подсвеченный(белый) и низ должен иметь тень( серый).
И так в теории для того, чтобы нарисовать ячейки, нам только нужно знать как рисовать градиент и пару линий.Должно быть довольно просто, не так ли? Приступим!

Привет, Core Graphics!

Всякий раз когда вы хотите нарисовать что-либо кастомное за счет средств Core Graphics на ios, вам следует располагать непосредственно рисование внутри UIView.Там находится специальный метод, называющийся drawRect, и так вы пишете код, отвечающий за рисование именно в этом месте.
Для начала давайте просто создадим view(вид) “Hello, World!”, который весь будет зарисован красным цветом, затем установим как бэкграунд нашей tableview cell, чтобы убедится, что всё это работает.
И так идём в “FileNew File…”, выбираем iOS/CocoaTouch Class, Objective-C class,убедитесь что выбрана “Subclass of UIView”, и нажмите кнопку “Next”. Назовите файл “CustomCellBackground.m”, убедитесь что стоит галочка “Also create CustomCellBackground.h”, в последних версиях XCode он автоматом создаёт загаловочный файл, и затем клик на кнопку “Finish”.
Нам не нужно вносить какие-либо изменения в заголовочный файл, так что перейдём непосредственно к  CustomCellBackground.m и сделаем следующие изменения:

// Раскомментируйте drawRect и замените его содержимое следующим кодом:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = 
    [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);

Ок, здесь целая куча нового материала, так что давайте постепенно разъясним что к чему.
На первой линии мы вызываем метод под названием “UIGraphicsGetCurrentContext()” для того, чтобы получить Core Graphics Context, который мы будем использовать в оставшихся методах. Мне нравится думать, о context(связь,контекст) как о холсте на котором мы будем рисовать. В этом случае  полотно это вид(view), но вы можете получить другие виды контекста также, такие как offscreen buffer, который в последствии вы можете обратить в изображение.
Одной интересной особенностью контекста является то, что он является stateful.stateful — модель, при которой объект содержит информацию о своем состоянии. Это значит, что когда вы вызываете методы для изменения чего-то вроде цвета заливки,т.е. fill color, например, цвет заливки останется окрашен в тот же цвет, пока позднее вы не измените его на какой-то другой.
На самом деле, это именно то, что мы делаем на 3-ей линии кода – мы используем CGContextSetFillColorWithColor для установки заливки цвета в красный, в любое время мы зальём форму в будущем.
Вы должно быть заметили, что когда мы вызываем этот метод, мы не можем обеспечивать строго UIColor – вместо этого мы устанавливаем CGColorRef .К счастью,очень просто затем конвертировать простой-в-использовании UIColor в CGColor, просто получая доступ к свойству CGColor.
В результате на последней линии мы вызываем метод дабы заполнить указанный прямоугольник  (используя любой цвет заливки установленный к контексту прежде).Для прямоугольника мы передаём границы view.
Теперь когда у нас есть привлекательная красная view, давайте установим этот цвет как бэкграунд нашей table View cell! Просто сделайте указанный изменения в RootViewController.m:

// Наверху файла
#import "CustomCellBackground.h"
// Внутри RootViewController.m, в методе tableView:cellForRowAtIndexPath , 
//   внутри блока cell == nil,после вызова initWithStyle:
cell.backgroundView = [[[CustomCellBackground alloc] init] autorelease];
cell.selectedBackgroundView = [[[CustomCellBackground alloc] init] autorelease];
// В конце функции, прямо перед return cell:
cell.textLabel.backgroundColor = [UIColor clearColor];

Единственное, что мы сделали здесь, так установили фон нашего отображения и выбранный фон, то есть который будет отображатся при нажатии на выбранную ячейку. Мы также изменили цвет фона у текста на прозрачный, и так наш фон будет просвечивать сквозь фон ячеек(label) под текст.
Скомпилируйте и запустите приложение и вы увидите следующее:
Hello, Core Graphics!
Шикарно, мы можем рисовать используя Core Graphics! И верите вы или нет, нами уже изучена куча важных техник – как получить контекст для рисования, как изменить цвет заливки и как залить прямоугольник цветом. Вы можете сделать пару довольно милых интерфейсов уже, пользуясь лишь этими навыками !
Но мы собираемся шагнуть дальше и изучить еще один полезный метод для создания замечательного пользовательского интерфейса:градиенты!

Рисуем градиенты

Мы собираемся нарисовать несколько градиентов в нашем проекте, так что давайте добавим наш код для рисования градиентов в helper функцию.  Таким образом мы не будем повторяться на протяжении всего проекта!
И так переходим в  ”FileNew File…”, выбираем iOSCocoa Touch Class, Objective-C class,убедитесь, что выбрано “Subclass of NSObject”, и нажимаем Next. Назоваите файлик  ”Common.m”, убедитесь, что выбрано “Also create Common.h”, и нажмите “Finish”.
Теперь удалите всё в Common.h и разместите там следующий код:

#import &lt;Foundation/Foundation.h&gt;
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef  endColor);

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

#import "Common.h"
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, 
    CGColorRef  endColor) {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
 
    NSArray *colors = [NSArray arrayWithObjects:(id)startColor, (id)endColor, nil];
 
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, 
        (CFArrayRef) colors, locations);
 
    // More coming... 
}

В этой функции много чего, так что разобъём её на две части. Мы начинаем с части, которую только что написали, которая создаёт градиент, который в последствии нарисуем.
Прежде всего нам нужно получить цветовое пространство с помощью которого мы будем рисовать градиент. В целом есть много всего, что вы можете делать с цветовым пространством, но в 99% времени вы просто хотите получить стандартную связь с цветовой схемой RGB, так что мы просто используем функцию CGColorSpaceCreateDeviceRGB , чтобы получить нужную нам связь.
Далее, мы настраиваем массив, который отслеживает расположение каждого цвета через диапазон градиента, иными словами у нас есть только два цвета и мы хотим, чтобы первый был цветом начинающим градиент , а второй цветом окончания градиента,то есть мы устанавливаем значение начала градиент, в данном случае 0 , и 1 будет означать конец градиента.
Отметим, что вы можете иметь три и более цвета в градиенте, если вы этого хотите, и вы можете устанавливать где каждый цвет будет начинать градиент в данном случае. Это может быть довольно полезно для определённых эффектов.
После этого, мы создаём массив с цветами, которые проходят в нашу функцию. Мы используем простой старый NSArray здесь ради удобства.
Затем мы создаём наш градиент с помощью CGGradientCreateWithColors, входящими параметрами для которого являются цветовая хема, массив цветов и их расположение, которое мы предварительно сделали. Отметим, что нам нужно конвертировать NSArray в CFArrayRef .
У нас теперь есть характеристика градиента, но пока что мы ничего не нарисовал – это просто указатель к информации, которую мы будем использовать впоследствии, когда начнём рисовать.И так давайте продолжим!Добавим следующий код в drawLinearGradient после комментария “More coming” :

CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
 
CGContextSaveGState(context);
CGContextAddRect(context, rect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
 
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);

Первое, что мы здесь делаем, так вычисляем точки начала и конца для которых мы хотим рисовать градиент. Мы просто выставляем их как линии от “верхушки середины” к “низу середине” прямоугольника. Отметим, что мы используем несколько помогающих функций из  CGGeometry.h (такие как CGRectGetMidX) для их вычисления  (и дабы почистить код!)
Остальной код помогает нам нарисовать градиент в установленный прямоугольник – ключевая функция CGContextDrawLinearGradient.Странная штука касательно этой функции заключается в том, что ,хотя она заполняет градиентом указанную область, нет никакого способа заполнить градиентом часть указанной области.
Хорошо…воспользуемся функцией CGContextClip, мы отрежем часть области .Клиппинг является удивительной особенностью Core Graphics, благодаря которой вы сможете ограничить область в произвольной форме. Всё, что вам нужно сделать – добавить форму к контексту, но затем вместо заполнения прямоугольника как мы обычно делали, вы вызовете CGContextClip. И всё дальнейшее рисование будет ограничено этим регионом!
И так это то, что мы делаем здесь. Мы добавляем наш прямоугольник к контексту,отсекаем за счет CGContextClip и затем вызываем  CGContextDrawLinearGradient, который принимает все вышеперечисленные параметры, которые мы только что настроили.
И так, что на счет этого материала  CGContextSaveCGState/CGContextRestoreCGState ? Хорошо, помните, что Core Graphics это машина состояний, и однажды установив что-то, это остаётся в том же состоянии до тех пор, пока вы не вернёте это значение.
Хорошо, мы только что обрезали регион, и так пока мы с этим что-либо не сделаем, мы не сможем рисовать за его пределами!
Следовательно, в этом случае  CGContextSaveCGState/CGContextRestoreCGState приходят на помощь . С этим мы можем сохранить нынешние настройки нашего контекста в стек, и затем вытащить их обратно и работать с того места, на котором был контекст на момент сохранения.
Осталась одна последняя вещь – нам нужно вызвать CGGradientRelease для того, чтобы освободить память, созданную  CGGradientCreateWithColors ранее (иCGColorSpaceRelease тоже, спасибо @Jim!).
Отлично ! Давайте упомянем о нашей новой функции и отредактируем файлик в котором рисуем фон ячейки.Откройте CustomCellBackground.m и сделайте следующие изменения:

// Добавьте в начало файла
#import "Common.h"
// Замените содержимое drawRect следующим:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef whiteColor = [UIColor colorWithRed:1.0 green:1.0 
    blue:1.0 alpha:1.0].CGColor; 
CGColorRef lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 
    blue:230.0/255.0 alpha:1.0].CGColor;
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor, lightGrayColor);

Скомпилируйте и запустите ваш проект, и вы увидите следующее:
Cells with Gradients
Вау, это потрясающе что простой градиент вытворяет!

Обведём таблицу

Это всё выглядит довольно круто, но мы собираемся добавить немного “штрихов”, чтобы сделать табличку чуть более привлекательной . Мы нарисуем белый прямоугольник , который будет идти по кромке наших ячеек, и серую разделительную линию между ячейками.
Мы уже знаем как заполнить прямоугольник – так теперь займёмся рисованием линий вокруг прямоугольника, это также просто!
Давайте попробуем . Сделаем следующие изменения в  CustomCellBackground.m:

// Добавим красный к остальным цветам
CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0 
    blue:0.0 alpha:1.0].CGColor;
// Добавим снизу 
CGRect strokeRect = CGRectInset(paperRect, 5.0, 5.0);
CGContextSetStrokeColorWithColor(context, redColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, strokeRect);

Мы собираемся нарисовать этот прямоугольник с красной чертой и сделать это по-середине ячейки, для того, чтобы для начала просто это сделать. И так мы создаем цвет и немного сужаем наш прямоугольник с помощью вспомогательной функцииCGRectInset.
Если вы такого никогда не видели, всё, что CGRectInset метод делает – это отнимает заданное количество от X и Y сторон прямоугольника и возвращает вам результат.
Затем мы устанавливаем цвет штриха красный, устанавливаем также линию шириной в 1 точку и вызываем CGContextStrokeRect to stroke the rectangle.
Скомпилируем и запустим наш проект и вы увидите следующее:
Fuzzy 1 Pixel Lines in Core Graphics
Это выглядите нормально сперва….Но затем слегка нечетко и довольно странно, не правда ли ?И так если вы увеличите, то вы увидите кое-что странное::
Fuzzy 1 Pixel Lines in Core Graphics - Zoomed
Мы сказали нарисовать с 1 точкой в ширину (что должно быть эквивалентно 1 пикселю в iPhone 3GS),но это появляется из-за того, что рисуется несколько пикселей поверх друг друга…что не так ?

Точечные линии и пиксельные границы

Хорошо, это происходит потому что когда Core Graphics обводит контур, он рисует черту прямо по центру точного края пути.
В нашем случае, края контура это прямоугольник, который мы желаем заполнить. И так когда рисуется 1 пиксельная линия вдоль края, половина линии(1/2 пикселя) будет внутри прямоугольника, и другая половина линии(1/2 пикселя) будет за пределами прямоугольника.
Но конечно, мы не можем рисовать 1/2 пикселя, взамен этого Core Graphics использует анти-сглаживание, чтобы нарисовать оба пикселя, но за счет того, что у одного более светлый оттенок, получается будто нарисован всего 1 пиксель.
Но мы не хотим это использовать, нам нужен всего лишь 1 пиксель, каналья! Есть несколько путей для решения нашей задачи:

  • Вы можете использовать клип, чтобы обрезать ненужные пиксели.
  • Вы можете отключить антисглаживание и также изменить связи прямоугольника дабы убедиться, что линия там где нужно.
  • Вы можете изменить путь к линии таким образом, что эффект 1/2 пикселя будет принят во внимание.

В этом уроке мы собираемся пойти третьим путём и изменить прямоугольник, чтобы он брал во внимание поведение строки, то есть её размер. Давайте создадим для этого вспомогающий метод.
И так откроем  Common.h и добавим следующие объявления в низ файла:

CGRect rectFor1PxStroke(CGRect rect);

Затем добавим следующие изменения в Common.m:

CGRect rectFor1PxStroke(CGRect rect) {
    return CGRectMake(rect.origin.x + 0.5, rect.origin.y + 0.5, 
        rect.size.width - 1, rect.size.height - 1);
}

Здесь мы изменяем прямоугольник таким образом, что край это пол пути + размер самого прямоугольника, тогда поведение строки становится корректным.
Далее давайте вызовём  CustomCellBackground.m:

// Замените strokeRect объявление со следующим:
CGRect strokeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));

Теперь если вы скомпилируете и запустите, граница прямоугольника примет опрятный вид:
1 Pixel Lines in Core Graphics - Sharp
Ок отлично. теперь давайте сдалем это в нужном цвете и расположении. Сделайте следующие изменение в Ok nice.  CustomCellBackground.m:

// Замените strokeRect объявление и установите цвет линии в соответствии с указанным:
CGRect strokeRect = paperRect;
strokeRect.size.height -= 1;
strokeRect = rectFor1PxStroke(strokeRect);
CGContextSetStrokeColorWithColor(context, whiteColor);

Здесь мы сокращаем высоту прямоугольника на 1 , чтобы оставить место для разграничителя, конвертируем и устанавливаем кромке белый цвет.
Скомпилируйте и запустите ваш проект, и тогда вы сможете наблюдать за вашей красивой белой границой вокруг ячеек:
Custom Cells with White Border
Далее, добавим светло серый разделитель между ячейками !

Рисуем линии

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

void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, 
    CGColorRef color);

И следующий в Common.m:

void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, 
    CGColorRef color) {
 
    CGContextSaveGState(context);
    CGContextSetLineCap(context, kCGLineCapSquare);
    CGContextSetStrokeColorWithColor(context, color);
    CGContextSetLineWidth(context, 1.0);
    CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
    CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
    CGContextStrokePath(context);
    CGContextRestoreGState(context);        
 
}

Ок, давайте пройдёмся по нему. В начале и конце мы сохраняем/восстанавливаем наши контексты, то есть мы не оставляемся изменения, которые мы сделали вокруг.
Затем мы устанавливаем вид нашей линии. По умолчанию для линии это “butt” концы, что означает:линия заканчивается ИМЕННО в точке окончания линии.
Но это не есть хорошо для нас, так как мы начинаем и заканчиваем линии в 1/2 точки, дабы избежать проблемы с чертой. И так взамен, мы устанавливаем “квадратные” концы, которые продолжат линию на 1/2, что в нашем случае просто идеальное решение всех проблем, превосходно!
Мы затем устанавливаем цвет и ширину линии как обычно.
Затем приступаем к самому этапу рисования линии. Чтобы нарисовать линии в Core Graphics, вы первым шагом двигаетесь к точке A(пока ничего не нарисовано), и затем добавляете линию к точке B(которая добавляет линию из точки А в точку B внутри контекста) Вы можете затем вызвать CGContextStrokePath чтобы очертить рамку линии.
Великолепно! Так что давайте используем это, чтобы нарисовать разделительную черту, сделав несколько указанных изменений в CustomCellBackground.m’s drawRect:

// Добавьте к другим цветам
CGColorRef separatorColor = [UIColor colorWithRed:208.0/255.0 green:208.0/255.0 
    blue:208.0/255.0 alpha:1.0].CGColor;
// Добавьте вниз метода
CGPoint startPoint = CGPointMake(paperRect.origin.x, 
    paperRect.origin.y + paperRect.size.height - 1);
CGPoint endPoint = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, 
    paperRect.origin.y + paperRect.size.height - 1);
draw1PxStroke(context, startPoint, endPoint, separatorColor);

Скомпилируйте и запустите ваш проект, и теперь там будет красивый разделитель между линиями!
Custom Cells with Separator

Куда дальше?

Здесь лежит экземпляр проекта со всем кодом, который мы написали в за этот урок.
В данный момент вы должны быть знакомы с некоторыми довольно крутыми и сильными техниками Core Graphics – заполнение прямоугольников и создание границ прямоугольника, рисование линий и градиентов, а также отделение границ с помощью клипов!Не говоря уже о том, что наша таблица начинает выглядеть довольно круто!
Но дальше больше! Мы еще не раскрыли как добавить падающие тени и нарисовать дугу, создать  эффект блеска и еще много полезного материала, которым мы займёмся в следующем уроке когда будем рисовать привлекательный header для наших табличек!
В то же время, если у Вас возникли вопросы, предложения или комментарии, пожалуйста, жарьте! :]

Таги: , , , , ,

Core Graphics 101: Глянцевые кнопочки

Create your own cool and flexible buttons from scratch!

Создайте ваши собственные крутые и легко модифицируемые кнопки с нуля!

Добро пожаловать на другую часть уроков Core Graphics 101!В этой серии уроков мы на практических примерах узнаем как начать работать с Core Graphics !
В уроках один, два, и три, мы узнали как создать свою табличку средствами Core Graphics!

В этом уроке мы собираемся применить различные практические решения для кастомизации UIButton.
В процессе  мы узнаем как рисовать закругленные прямоугольники,  как просто играться с оттенком вашего Core Graphics рисования, и будем использовать пару уже изученных нами концептов.
Как Alex Curylo из Under The Bridge много раз упомянал, уже есть много хороших решений для кастомизации ваших кнопок.  Мои любимые для быстрого и лёгкого создания кнопки это Button Maker не кем иным, как  Dermot Daly.
Но я думаю, что во всей этой дискуссии мы упустили детального урока по собственноручной кастомизации кнопки, от начала и до самого конца с Core Graphics. Это довольно просто, и таким образом вы можете получить образ, который нужен для вашему приложения.
И так давайте возьмёмся и сделать кнопки!

 

Приступаем к работе

Создайте новый проект в XCode, используя View-based Application шаблон, и назовите проект “CoolButton”.
Убедитесь, что выбрана группа “Classes” под  ”Groups & Files”,идём в “FileNew File…”, выбираем iOSCocoa Touch Class, Objective-C class, убедитесь, что выбрано  ”Subclass of UIView”, и нажмите далее. Назовите файл “CoolButton.m”,Убедитесь, что также создасться заголовочный файл “Also create CoolButton.h”, и нажмите  ”Finish”.
Затем замените контент CoolButton.h следующим:

#import &lt;UIKit/UIKit.h&gt;
@interface CoolButton : UIButton {
    CGFloat _hue;
    CGFloat _saturation;
    CGFloat _brightness;
}
@property  CGFloat hue;
@property  CGFloat saturation;
@property  CGFloat brightness;
@end

Обратите внимание, что на самом деле у нас сабкласс не  UIView, а UIButton. Мы также определяем некоторые свойства, которые позволят нам менят оттенок, насыщенность и яркость  нашего отображения.
Далее внесите следующие изменения в CoolButton.m:

// Под @implementation
@synthesize hue = _hue;
@synthesize saturation = _saturation;
@synthesize brightness = _brightness;
// Удалите метод initWithFrame и добавьте следующее:
-(id) initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        self.opaque = NO;
        self.backgroundColor = [UIColor clearColor];
        _hue = 1.0;
        _saturation = 1.0;
        _brightness = 1.0;
    }
    return self;
}
// Раскомментируйте метод drawRect и замените содержимое следующим кодом:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef color = [UIColor colorWithHue:_hue saturation:_saturation 
    brightness:_brightness alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, color);
CGContextFillRect(context, self.bounds);
// Добавьте внизу файл
- (void)setHue:(CGFloat)hue {
    _hue = hue;
    [self setNeedsDisplay];
}
- (void)setSaturation:(CGFloat)saturation {
    _saturation = saturation;
    [self setNeedsDisplay];
}
- (void)setBrightness:(CGFloat)brightness {
    _brightness = brightness;
    [self setNeedsDisplay];
}

Здесь мы всего лишь инициализируем переменые и заполняем всю область наим настроенным цветом и убеждаемся, что всё готово для начала.
Отметим, что мы используем разные конструкторы для создания цвета – теперь взаместо colorWithRed:Green:Blue, мы используем colorWithHue:saturation:brightness. Это для понимания в будущем.
Последнеем, что нам осталось сделать, это перезаписать сеттер для свойств оттенка, насыщенности и яркости, и так мы вызываем setNeedsDisplay при их вызове. Это заставит наше отображение каждый раз перерисовывать цвет, при изменении параметров кнопки.
Теперь переключитесь на CoolButtonViewController.h и внесите следующие изменения:

// Перед @interface
@class CoolButton;
// Внутри @interface
CoolButton *_button;
// Посл
 @interface
@property (retain) IBOutlet CoolButton *button;
- (IBAction)hueValueChanged:(id)sender;
- (IBAction)saturationValueChanged:(id)sender;
- (IBAction)brightnessValueChanged:(id)sender;

Здесь мы объявляем связи к кнопке (которую мы будем делать в Interface Builder) и несколько коллбэков для изменения значения положения слайдера (который мы также добавим через Interface Builder).

Так что давайте пойдём вперёд и сделаем это прямо сейчас. Откройте CoolButtonViewController.xib, и перетащите UIButton, 3 UILabels,и 3 UISliders на отображение, чтобы это выглядело примерно следующим образом:
Button View Layout
Далее, измените класс UIButton ,чтобы был CoolButton, перейдя в Identity Inspector и изменив Class в выпадающем списке на CoolButton:
Identity Inspector
Также, убедитесь, что ввыбран Attributes Inspector,и измените тип рисуемой кнопки на Custom, чтобы убрать закругленные углы кнопки, используемые по-умолчанию.
Затем, перетащите из “File’s Owner” в CoolButton и соедините это к  button outlet. Также, перетащите от каждого слайдера к  ”File’s Owner”, чтобы соединить с соответствующим свойством значение переменной в меняющемся коллбеке.
Одна последняя деталь перед тем, как поглядим на полученное. Внесите следующие изменения в CustomViewController.m:

// В import секцию
#import "CoolButton.h"
// Под @implementation
@synthesize button = _button;
// В viewDidUnload
self.button = nil;
// В dealloc
[_button release];
_button = nil;
// Добавьте вниз файла
- (IBAction)hueValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.hue = slider.value;
 
}
- (IBAction)saturationValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.saturation = slider.value;
 
}
- (IBAction)brightnessValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.brightness = slider.value;
 
}

Отметим, что по умолчанию UISliders установлены значения от 0.0 до 1.0 – которые идеальны для наших переменных  hue, saturation, и brightness, у которых также диапазон значений колеблется от 0.0 до 1.0, следовательно мы устанавливаем их непосредственно друг к другу без каких либо изменений.
Скомпилируйте и запустите проект, и если всё нормально работает, вы сможете подвигать слайдеры, чтобы заливать “CoolButton” различными цветами:
Button Placeholder

Рисуем закруглённые прямоугольники

Это правда, вы можете делать квадратные кнопки, но нынче очень модно именно закруглённые прямоугольные кнопки.
В последнем уроке мы узнали как рисовать дуги используя CGContextAddArc API.И так мы определённо можем использовать эти знания для рисования дуг в каждом углу, и рисовать линии, чтобы их соединять.
Но есть более легкий путь, где нам не нужно использовать так много вычислений, и этот способ наиболее подходящий для рисования закруглённых прямоугольников. Это CGContextAddArcToPoint API.
CGContextAddArcToPoint API позволяет вам указать дугу для рисования просто указав 2 касательные и радиус. Следующая диаграмма из  программный гайд по Quartz2D всё вам хорошо покажет:
CGContextAddArcToPoint Diagram
И так в случае с прямоугольником, мы очевидно знаем касательные линии для каждой дуги,  которые хотим нарисовать – это всего лишь углы прямоугольника! И мы можем указать радиус основываясь на закруглении,  которое будет использоваться,  большом или меньшем.
Другая прелесть этой функции заключается в том, что если текущая точка на пути не установлена, где вы говорите дуге начать рисовать, функция будет рисовать линию от текущей точки в начале пути. И так вы можете смело использовать эту функцию для рисования закруглённых прямоугольников всего лишь в пару вызовов.
Так как мы начали создавать несколько одинаковых закруглённых прямоугольников в этом уроке и на будущее, давайте добавим хелпер Common.h/m для создания пути длч закруглённого прямоугольника по прямоугольнику.
Если у вас до сих пор нет его, вы можете скачать Common.h/m на месте, котором мы остановились в прошлый раз, и добавить это в ваш проект.
Затем добавьте следующее в Common.h:

CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius);

И добавьте следующее вниз Common.m:

CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius) {
 
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect), 
        CGRectGetMaxX(rect), CGRectGetMaxY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMaxY(rect), 
        CGRectGetMinX(rect), CGRectGetMaxY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect), 
        CGRectGetMinX(rect), CGRectGetMinY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect), 
        CGRectGetMaxX(rect), CGRectGetMinY(rect), radius);
    CGPathCloseSubpath(path);
 
    return path;        
}

Вышенаписанний код рисует закруглённый прямоугольник в следующем порядке:
How to draw a rounded rect with Core Graphics

  1. Двигаться к центру верхней линии.
  2. Добавить дугу в верхний правый угол. Перед рисованием дуги, CGPathAddArcToPoint нарисует линию от нынешней точки (середина прямоугольника) к началу нашей дуги.
  3. Аналогичным образом, добавить дугу в нижний праывй угол и соединительную линию.
  4. Аналогичный образом, добавить дугу в левый нижний угол и соединительную линию.
  5. Аналогичным образом, добавить дугу в верхний левый угол и соединительную линию.
  6. Затем соединить окончательную точку дуги с точкой начала с помощью CGPathCloseSubpath.

Также отметим, сверху мы используем несколько полезных помогающих методов из CGGeometry.h ,чтобы  вытащить различные местонахождения в прямоугольнике, которые нам нужны.
Ок давайте посмотрим как это работает! Откройте CoolButton.m и сделайте следующие модификации:

// Вверху файла
#import "Common.h"
// Замените контект метода drawRect со следующим:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:_brightness alpha:1.0].CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
        blue:0.2 alpha:0.5].CGColor;
CGFloat outerMargin = 5.0f;
CGRect outerRect = CGRectInset(self.bounds, outerMargin, outerMargin);
CGMutablePathRef outerPath = createRoundedRectForRect(outerRect, 6.0);
if (self.state != UIControlStateHighlighted) {
	CGContextSaveGState(context);
	CGContextSetFillColorWithColor(context, outerTop);
	CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor);
	CGContextAddPath(context, outerPath);
	CGContextFillPath(context);
	CGContextRestoreGState(context);
}

Ок, мы  объявляем наши 2 цвета, затем мы используем CGRectInset, чтобы получить слегка меньший прямоугольник (5 пикселей на каждой стороне) в котором мы будем рисовать закруглённый прямоугольник. Мы делаем это меньше, так как нам нужно место для рисования тени во вне.
Далее, мы вызываем функци, которую мы только что нарисовали для создания пути для наших закруглённых прямоугольников. Затем мы установили цвет заполнения и тень, добавили путь к контексту, и вызвали FillPath для заполнения этого нашим нынешним цветом.
Скомпилируйте и запустите приложение, и если всё хорошо работает, вы увидите следующее:
Rounded Rect, Draft 2

Стилизуем нашу кнопку

Отлично, теперь у нас уже есть что-то похожее на кнопку, но она не выглядит достойно :[
И так давайте с этим что-нибудь сделаем! Давайте добавим некоторые дополнительные улучшения нашей кнопке.
Откройте CoolButton.m и сделайте следующие модификации:

// Замените секцию с цветами следующим кодом
CGColorRef blackColor = [UIColor colorWithRed:0.0 green:0.0 
        blue:0.0 alpha:1.0].CGColor;
CGColorRef highlightStart = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.4].CGColor;
CGColorRef highlightStop = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.1].CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
        blue:0.2 alpha:0.5].CGColor;
CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:1.0*_brightness alpha:1.0].CGColor;
CGColorRef outerBottom = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.80*_brightness alpha:1.0].CGColor;
CGColorRef innerStroke = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.80*_brightness alpha:1.0].CGColor;
CGColorRef innerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.90*_brightness alpha:1.0].CGColor;
CGColorRef innerBottom = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.70*_brightness alpha:1.0].CGColor;
// Добавьте вниз 
CGContextSaveGState(context);
CGContextAddPath(context, outerPath);
CGContextClip(context);
drawGlossAndGradient(context, outerRect, outerTop, outerBottom);
CGContextRestoreGState(context);

Первое, что мы делаем – устанавливаем группу цветов, которыми будем пользоваться в последствии. Всего есть несколько общих цветов, и затем несколько цветов, основанных на предыдущих. Наш базовый цвет в точности тот,  который мы настроим, затем у нас есть несколько цветов, которые чуть-чуть по темнее нынешнего цвета.
Если вы объявляете ваши цвета таким образом, изменить цвет вашего отображения становиться очень просто, и это может стать очень полезным при использовании кода в других приложениях!
Далее делаем клип к нашему закруглённому прямоугольнику, и заполняем эту область градиентом(взаместо сплошного цвета).

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

Button Styling - Update 1
Теперь давайте добавим еще один контур с несколько другим градиентом, чтобы создать эффект типа тиснения. Сделайте следующие модификации в CoolButton.m:

// Добавьте после создания outerPath
CGFloat innerMargin = 3.0f;
CGRect innerRect = CGRectInset(outerRect, innerMargin, innerMargin);
CGMutablePathRef innerPath = createRoundedRectForRect(innerRect, 6.0);
// Внизу
CGContextSaveGState(context);
CGContextAddPath(context, innerPath);
CGContextClip(context);
drawGlossAndGradient(context, innerRect, innerTop, innerBottom);
CGContextRestoreGState(context);

Здесь мы сокращаем прямоугольник опять с CGRectInset,затем получаем закруглённый прямоугольник и покрываем градиентом. Скомпилируйте и запустите , чтобы оценить улучшения:
Button Styling - Update 2
Теперь давайте добавим очень тонкую подсветку вверху, если кнопка в нажатом состоянии. Сделайте пару изменений в CoolButton.m:

// После создания innerPath
CGFloat highlightMargin = 2.0f;
CGRect highlightRect = CGRectInset(outerRect, highlightMargin, highlightMargin);
CGMutablePathRef highlightPath = createRoundedRectForRect(highlightRect, 6.0);
// Внизу
if (self.state != UIControlStateHighlighted) {
    CGContextSaveGState(context);
    CGContextSetLineWidth(context, 4.0);
    CGContextAddPath(context, outerPath);
    CGContextAddPath(context, highlightPath);
    CGContextEOClip(context);
    drawLinearGradient(context, outerRect, highlightStart, highlightStop);
    CGContextRestoreGState(context);
}

На самом деле мы просто создали другой закруглённый прямоугольник который немного меньше, чем outerRect,и заполнили область между двумя прямоугольниками с альфа подсвеченным градиентом, используя четно-нечетный принцип из прошлого урока.
Скомпилируйте и запустите, чтобы посмотреть на полученный результат:
Button Styling - Update 3
Давайте обернём еще красивее. Добавьте следующий код внизу drawRect метожа:

CGContextSaveGState(context);
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, blackColor);
CGContextAddPath(context, outerPath);
CGContextStrokePath(context);
CGContextRestoreGState(context);
CGContextSaveGState(context);
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, innerStroke);
CGContextAddPath(context, innerPath);
CGContextClip(context);
CGContextAddPath(context, innerPath);
CGContextStrokePath(context);
CGContextRestoreGState(context);    
CFRelease(outerPath);
CFRelease(innerPath);
CFRelease(highlightPath);

Здесь мы рисуем по контуру 2 пиксельную линию, и по внутреннему контору 1 пиксельную линию.
Вы должно быть удивлены,  тем что линия из двух точек. Так, мы используем различные техники для решения 1 пиксельной проблемы – здесь это клиппинг маски. На самом деле мы просто устанавливаем черте 2 точки и затем делаем клиппинг на внешнюю область.
В конце, у нас получается такой контур.
Скомпилируйте и запустите и насладитесь!
Button Styling - Update 4

Подсветка кнопки

Наша кнопка выглядит просто замечательно, но у неё нету действий как у кнопки. То есть она не загорается  при нажатии.
К счастью, Jeff LaMarche показывает как справиться с этой бедой в одном из его постов на эту тему.
Основная мысль заключается в оверрайде действий, реагирующих на нажатие, чтобы сказать нашей кнопке перерисовать себя, то есть обновить яркость при нажатии.
Давайте сделаем следующие изменения в CoolButton.m:

// Добавьте следующие методы вниз
- (void)hesitateUpdate
{
    [self setNeedsDisplay];
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    [self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    [self setNeedsDisplay];
 
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [self setNeedsDisplay];
    [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    [self setNeedsDisplay];
    [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1];
}

Скомпилируйте и запустите проект, и вы увидите, что тепеь при нажатии кнопка подсвечивается. Но давайте сделаем это чуточку лучше с еще одной модификацией:

// Вначало функции
CGFloat actualBrightness = _brightness;
if (self.state == UIControlStateHighlighted) {
    actualBrightness -= 0.10;
}   
// Затем замените все дальнейшие действия функции, которые используют 
//      "_brightness" чтобы установить цвет с "actualBrightness".

Скомпилируйте и запустите и теперь кнопка выглядит довольно хорошо и реагирует на нажатие!
Tapped Button

Куда дальше ?

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

Таги: , , , ,

26 December 2012

Использование системы контроля версий Git в Xcode для iOS 6. Часть 1

How to use Git source control with Xcode

Туториал написал Малек Трабелси, страстный iOS разработчик из Тунисии, сфокусированный в-основном на мобильных и веб-технологиях.

Независимый ли вы разработчик, либо работаете в команде, если не используете контроль версий, то пора начать.

Контроль версий это круто, потому что можно откатиться на более старую версию кода, посмотреть на произошедшие изменения за это время, и работать командой. И лучшая из систем контроля версий (СКВ) уже встроена в Xcode – git!

git система распределенного контроля версий, разработанная Линусом Торвальдсом, директором разработки ядра линукс. Няшность git — нет центрального репозитория, у каждого есть свой взгляд на код, и добавлять код можно из других источников.

В туториале, вы получите опыт работы с git и узнаете, как испоьзовать его в Xcode, в командной строке, и интегрировать проекты Xcode с Github’ом, популярным репозиторием git. Полностью обновлено для iOS 6 и новых фич Xcode 4.5 для гита.

Без лишних слов, Git! Продолжайте читать!

Таги: , , , ,

23 November 2012

Passbook – ответы на часто задаваемые вопросы

Passbook - ответы на часто задаваемые вопросы!

Passbook – ответы на часто задаваемые вопросы!

Это – пост из блога члена нашей команды авторов Marin Todorov, более 12 лет работающего как разработчик программного обеспечения, независимого разработчика под iOS и создателя журнала Touch Code Magazine.

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

Чтобы помочь вам разобраться с Passbook, я написал две массивных главы о нем в iOS 6 by Tutorials. Я также опубликовал (вот здесь)сокращенную бесплатную версию первой из этих глав..

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

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

Я надеюсь, что это поможет вам сотворить с Passbook что-нибудь действительно потрясающее! Продолжайте читать!

Таги: , , , , , ,