Все примеры кода из этой книги были протестированы на iPhone 4, iPhone 3GS и эмуляторе iPhone/iPad, но не исключено, что у вас все же возникнут какие-то сложности. Например, у вас будет иная версия SDK, нежели та, в которой компилировался и тестировался код из примера. Информация, изложенная в этой книге, проверялась на каждом этапе подготовки издания. Тем не менее мы могли допустить какие-то ошибки или чего-то недосмотреть, поэтому с благодарностью примем от вас информацию о любых подобных недочетах, которые могут вам встретиться, а также все ваши предложения о том, как можно было бы улучшить будущие издания книги. С автором и редакторами можно связаться по следующему адресу:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
(800) 998-9938 (в США или Канаде)
(707) 829-0515 (международный или местный телефон)
(707) 829-0104 (факс)
Благодарности
Энди Орам, мой любезный редактор, вновь потрудился на славу и внимательно проработал все изменения, появившиеся в новом издании книги. Фактически эта книга переработана полностью, это касается и содержащихся в ней скриншотов и примеров кода. Я хотел бы поблагодарить также Кшиштофа Гробельного и Кшиштофа Гутовского — моих хороших друзей и коллег, выполнивших техническое рецензирование книги. Без их участия она ни за что не оказалась бы в ваших руках.
Особой благодарности заслуживает Рэйчел Румелиотис, поддерживавшая меня и Энди. В первую очередь спасибо ей за ту административную работу, которая на первый взгляд как будто не видна. Кроме того, с наилучшей стороны себя показала Меган Конноли из издательства O’Reilly. Она терпеливо сносила мои причитания о бумажной работе, сотрудничество с ней доставило одно удовольствие. Благодарю Джессику Хозман за то, что помогла нам справиться с проблемами, которые возникали с Git. Я и поверить не мог, что те простые решения, которые она мне подсказывала, действительно сработают. Но они работали, а я порой чувствовал себя идиотом.
Последние, но немаловажные благодарности хочется высказать Алине Риззони, Бруно Пэкхему и Томасу Пэкхему за их преданную дружбу. Я счастлив, что знаю их, и высоко ценю их помощь и поддержку.
От издательства
Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты vinitski@minsk.piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах.
Глава 1. Реализация контроллеров и видов
1.0. Введение
В iOS 7 появилось множество новых пользовательских возможностей, а также масса новых API, с которыми мы, программисты, можем вволю экспериментировать. Вероятно, вы уже знаете, что в iOS 7 разительно изменился пользовательский интерфейс. Во всех предыдущих версиях он оставался практически неизменным по сравнению с первой версией iOS, и поэтому многие приложения разрабатывались так, как будто пользовательский интерфейс никогда не изменится. В настоящее время графические дизайнеры столкнулись с целым букетом проблем, так как теперь требуется создавать интерфейсы и продумывать пользовательские взаимодействия с программой так, чтобы программа хорошо смотрелась и в iOS 7, и в более ранних версиях.
Чтобы программировать приложения для iOS 7, вы должны знать основы языка Objective-C, с которым мы будем работать на протяжении всей этой книги. Как понятно из названия, язык Objective-C основан на С, но имеет определенные расширения, которые облегчают оперирование объектами. Объекты и классы имеют фундаментальное значение в объектно-ориентированном программировании (ООП). К числу объектно-ориентированных языков относятся Objective-C, Java, C++ и многие другие. В Objective-C, как и в любом объектно-ориентированном языке, вы имеете доступ не только к объектам, но и к примитивам. Например, число –20 (минус двадцать) можно выразить в виде примитива следующим образом:
NSInteger myNumber = -20;
В этой простой строке кода определяется переменная myNumber, относящаяся к типу данных NSInteger. Ее значение устанавливается в 20. Так определяются переменные в языке Objective-C. Переменная — это простое присваивание имени местоположению в памяти. В таком случае если мы задаем 20 в качестве значения переменной myNumber, то сообщаем машине, что собираемся выполнить фрагмент кода, который поместит указанное значение в область памяти, соответствующую переменной myNumber.
В сущности, все приложения iOS используют архитектуру «модель — вид — контроллер» (MVC). C архитектурной точки зрения модель, вид и контроллер — это три основные составляющие приложения iOS.
Модель — это мозг приложения. Она выполняет все вычисления и создает для себя виртуальный мир, в котором может существовать сама, без видов и контроллеров. Иными словами, вы можете считать модель виртуальной копией вашего приложения, без интерфейса.
Вид — это окно, через которое пользователь взаимодействует с вашим приложением. В большинстве случаев вид отображает содержимое модели, но, кроме того, он же воспринимает и действия пользователя. Любые контакты между пользователем и вашим приложением отправляются в вид. После этого они могут быть перехвачены контроллером вида и переданы в модель.
Контроллеры в программах iOS — это, как правило, контроллеры видов, которые я только что упомянул. Контроллер вида является, в сущности, переходным звеном между моделью и видом. Он интерпретирует события, происходящие с одной стороны, и по мере необходимости использует эту информацию для внесения изменений на другой стороне. Например, если пользователь изменяет какое-либо поле в виде, то контроллер гарантирует, что и модель изменится соответствующим образом. А если модель получит новые данные, то контроллер прикажет виду отобразить их.
В этой главе вы узнаете, как выстраивать структуру приложения iOS и использовать виды и контроллеры видов для создания интуитивно понятных приложений.
В этой главе мы будем создавать большинство компонентов пользовательского интерфейса на базе шаблона Single View Application из Xcode. Чтобы воспроизвести приведенные инструкции, следуйте рекомендациям, приведенным в подразделе «Создание и запуск вашего первого приложения для iOS» данного раздела. Убедитесь в том, что ваше приложение является универсальным, а не ориентировано только на iPhone или на iPad. Универсальное приложение может работать как на iPhone, так и на iPad.
Создание и запуск вашего первого приложения для iOS
Прежде чем подробнее познакомиться с возможностями Objective-C, вкратце рассмотрим, как создать простое приложение для iOS в среде Xcode. Xcode — это интегрированная среда разработки (IDE) для работы с Apple, позволяющая создавать, строить и запускать ваше приложение в эмуляторе iOS и даже на реальных устройствах с iOS. По ходу книги мы подробнее обсудим Xcode и ее возможности, а пока научимся создавать и запускать самое простое приложение. Я полагаю, что вы уже скачали Xcode из Mac App Store и установили ее на своем компьютере. В таком случае выполните следующие шаги.
1. Откройте Xcode, если еще не сделали этого.
2. Выберите в меню пункт File (Файл), далее — New Project (Новый проект).
3. Слева в диалоговом окне создания нового проекта выберите подкатегорию Application (Приложение) в основной категории iOS. Затем справа щелкните на варианте Single View Application (Приложение с единственным видом) и нажмите кнопку Next (Далее).
4. На следующем экране вы увидите поле Product Name (Название продукта). Здесь укажите название, которое будет понятно вам, например My First iOS App. В разделе Organization name (Название организации) введите название вашей компании или, если работаете самостоятельно, любое другое осмысленное название. Название организации — довольно важная информация, которую, как правило, придется здесь указывать, но пока она нас не особенно волнует. В поле Company Identifier (Идентификатор компании) запишите com.mycompany. Если вы действительно владеете собственной компанией или пишете приложение для фирмы, являющейся вашим работодателем, то замените mycompany настоящим названием. Если просто экспериментируете, придумайте какое-нибудь название. В разделе Devices (Устройства) выберите вариант Universal (Универсальное).
5. Как только зададите все эти значения, просто нажмите кнопку Next (Далее).
6. Система предложит сохранить новый проект на диске. Выберите желаемое местоположение проекта и нажмите кнопку Create (Создать).
7. Перед запуском проекта убедитесь, что к компьютеру не подключено ни одного устройства iPhone или iPad/iPod. Это необходимо, поскольку, если к вашему Mac подключено такое устройство, то Xcode попытается запускать приложения именно на устройстве, а не на эмуляторе. В таком случае могут возникнуть некоторые проблемы с профилями инициализации (о них мы поговорим позже). Итак, отключите от компьютера все устройства с системой iOS, а затем нажмите большую кнопку Run (Запуск) в левом верхнем углу Xcode. Если не можете найти кнопку Run, перейдите в меню Product (Продукт) и выберите в меню элемент Run (Запуск).
Ура! Вот и готово простое приложение, работающее в эмуляторе iOS. Может быть, оно и не кажется особенно впечатляющим: в эмуляторе мы видим просто белый экран. Но это лишь первый шаг к освоению огромного iOS SDK. Давайте же отправимся в это непростое путешествие!
Определение переменных и понятие о них
Во всех современных языках программирования, в том числе в Objective-C, существуют переменные. Переменные — это просто псевдонимы, обозначающие участки (местоположения) в памяти. Каждая переменная может иметь следующие свойства:
тип данных, представляющий собой либо примитив (например, целое число), либо объект;
• имя;
• значение.
Задавать значение для переменной приходится не всегда, но вы обязаны указывать ее имя и тип. Вот несколько типов данных, которые необходимо знать для написания типичного приложения iOS.
Если тип данных является изменяемым, то вы можете изменить такие данные уже после инициализации. Например, вы можете откорректировать одно из значений в изменяемом массиве, добавлять в него новые значения или удалять их оттуда. Напротив, при работе с неизменяемым типом вы должны предоставлять все значения для него уже на этапе инициализации. Позже нельзя будет пополнить набор этих значений, удалить какие-либо значения или изменить их. Неизменяемые типы полезны в силу своей сравнительно более высокой эффективности. Кроме того, они помогают избежать ошибок, если все значения должны оставаться неизменными на протяжении всего жизненного цикла данных.
• NSInteger и NSUInteger. Переменные этого типа могут содержать целочисленные значения, например 10, 20 и т. д. Тип NSInteger может содержать как положительные, так и отрицательные значения, но тип NSUInteger является беззнаковым, на что указывает буква U в его названии. Не забывайте, что слово «беззнаковый» в терминологии языков программирования означает, что число ни при каких условиях не может быть отрицательным. Отрицательные значения могут содержаться только в числовом типе со знаком.
• CGFloat. Содержит числа с плавающей точкой, имеющие десятичные знаки, например 1.31 или 2.40.
• NSString. Позволяет сохранять символьные строки. Такие примеры мы рассмотрим далее.
• NSNumber. Позволяет сохранять числа как объекты.
• id. Переменные типа id могут указывать на объект любого типа. Такие объекты называются нетипизированными. Если вы хотите передать объект из одного места в другое, но по какой-то причине не хотите при этом указывать их тип, то вам подойдет именно такой тип данных.
• NSDictionary и NSMutableDictionary. Это соответственно неизменяемый и изменяемый варианты хеш-таблиц. В хеш-таблице вы можете хранить ключ и ассоциировать этот ключ со значением. Например, ключ phone_num может иметь значение 0 55524 87700. Для считывания значений достаточно ссылаться на ассоциированные с ними ключи.
• NSArray и NSMutableArray. Неизменяемые и изменяемые массивы объектов. Массив — это упорядоченная коллекция элементов. Например, у вас может быть 10 строковых объектов, которые вы хотите сохранить в памяти. Для этого хорошо подойдет массив.
• NSSet, NSMutableSet, NSOrderedSet, NSMutableOrderedSet. Это типы множеств. Множества напоминают массивы тем, что могут содержать в себе наборы объектов, но в отличие от массива множество может включать в себя только уникальные объекты. Массив может содержать несколько экземпляров одного и того же объекта, а в множестве каждый объект может присутствовать только в одном экземпляре. Рекомендую вам четко усвоить разницу между массивами и множествами и использовать их правильно.
• NSData и NSMutableData. Неизменяемые и изменяемые контейнеры для любых данных. Такие типы данных очень вам пригодятся, если вы, например, хотите выполнить считывание содержимого файла в память.
Одни из рассмотренных нами типов данных являются примитивами, другие — классами. Вам придется просто запомнить, какие из них относятся к каждой из категорий. Например, тип данных NSInteger является примитивом, а NSString — классом. Поэтому из NSString можно создавать объекты. В языке Objective-C, как и в C и C++, существуют указатели. Указатель — это тип данных, в котором сохраняется адрес в памяти. По этому адресу уже хранятся фактические данные. Вы уже, наверное, знаете, что указатели на классы обозначаются символом астериска (*):
NSString *myString = @"Objective-C is great!";
Следовательно, если вы хотите присвоить строку переменной типа NSString на языке Objective-C, то вам понадобится просто сохранить данные в указатель типа NSString *. Но если вы собираетесь сохранить в переменной значение, представляющее собой число с плавающей точкой, то не сможете использовать указатель, так как тип данных, к которому относится эта переменная, не является классом:
/* Присваиваем переменной myFloat значение PI */
CGFloat myFloat = M_PI;
Если вам нужен указатель на эту переменную, соответствующую числу с плавающей точкой, то вы можете поступить так:
/* Присваиваем переменной myFloat значение PI */
CGFloat myFloat = M_PI;
/* Создаем переменную указателя, которая направлена на переменную myFloat */
CGFloat *pointerFloat = &myFloat;
Мы получаем данные от исходного числа с плавающей точкой путем простого разыменования (myFloat). Если получение значения происходит с применением указателя, то требуется использовать астериск (*pointerFloat). В некоторых ситуациях указатели могут быть полезны — например, при вызове функции, которая задает в качестве аргумента значение с плавающей точкой, а вы хотите получить новое значение после возврата функции.
Но вернемся к теме классов. Пожалуй, следует разобраться с ними немного подробнее, пока мы окончательно не запутались. Итак, приступим.
Как создавать классы и правильно пользоваться ими
Класс — это структура данных, у которой могут быть методы, переменные экземпляра и свойства, а также многие другие черты. Но пока мы не будем углубляться в подробности и поговорим об основах работы с классами. Каждый класс должен следовать таким правилам.
Класс должен наследовать от суперкласса. Из этого правила есть немногочисленные исключения. В частности, классы NSObject и NSProxy являются корневыми. У корневых классов не бывает суперкласса.
• Класс должен иметь имя, соответствующее Соглашению об именованиях методов в Cocoa.
• У класса должен быть файл интерфейса, в котором определяется интерфейс этого класса.
• У класса должна быть реализация, в которой вы прописываете все возможности, которые вы «обещали» предоставить согласно интерфейсу класса.
NSObject — это корневой класс, от которого наследуют практически все другие классы. В этом примере мы собираемся добавить класс под названием Person в проект, который был создан в подразделе «Создание и запуск вашего первого приложения для iOS» данного раздела. Далее мы добавим к этому классу два свойства, firstName и lastName, которые относятся к типу NSString. Выполните следующие шаги, чтобы создать класс Person и добавить его в ваш проект.
1. Откройте проект в Xcode и в меню File (Файл) выберите New-File (Новый— Файл).
2. Убедитесь, что слева, в разделе iOS, вы выбрали категорию Cocoa Touch. После этого выберите элемент Objective-C Class (Класс для Objective-C) и нажмите Next (Далее).
3. В разделе Class (Класс) введите имя Person.
4. В разделе Subclass of (Подкласс от) введите NSObject.
Когда справитесь с этим, нажмите кнопку Next (Далее). На данном этапе Xcode предложит вам сохранить этот файл. Просто сохраните новый класс в том каталоге, где находятся ваш проект и все его файлы. Это место выбирается по умолчанию. Затем нажмите кнопку Create (Создать) — и дело сделано.
После этого в ваш проект будут добавлены два новых файла: Person.h и Person.m. Первый файл — это интерфейс вашего класса Person, а второй — файл реализации этого класса. В Objective-C.h-файлы являются заголовочными. В таких файлах вы определяете интерфейс каждого файла. В.m-файле пишется сама реализация класса.
Теперь рассмотрим заголовочный файл нашего класса Person и определим для этого класса два свойства, имеющие тип NSString:
@interface Person: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
Как и переменные, свойства определяются в особом формате в следующем порядке.
1. Определение свойства должно начинаться с ключевого слова @property.
2. Затем следует указать квалификаторы свойства. Неатомарные (nonatomic) свойства не являются потокобезопасными. О безопасности потоков мы поговорим в главе 14. Вы можете указать и другие квалификаторы свойств: assign, copy, weak, strong или unsafe_unretained. Чуть позже мы подробнее поговорим и о них.
3. Затем укажите тип данных для свойства, например NSInteger или NSString.
4. Наконец, не забудьте задать имя для свойства. Имена свойств должны соответствовать рекомендациям Apple.
Как было указано ранее, свойства могут иметь различные квалификаторы. Вот важнейшие квалификаторы, в которых вы должны разбираться.
strong — свойства этого типа будут сохраняться во время исполнения. Они могут быть только экземплярами классов. Иными словами, вы не можете сохранить значение в свойстве типа strong, если значение является примитивом. Можно сохранять объекты, но не примитивы.
• copy — аналогичен strong, но при выполнении присваивания к свойствам этого типа среда времени исполнения будет делать копию объекта в правой части операции присваивания. Объект, находящийся в правой части этой операции, должен соответствовать протоколу NSCopying или NSMutableCopying.
• assign — значения объектов или примитивов, задаваемые в качестве значения свойства типа assign, не будут копироваться или сохраняться этим свойством. Для свойств примитивов этот квалификатор будет создавать адрес в памяти, в котором вы сможете поместить информацию примитива. В случае с объектами свойства такого типа будут просто указывать на объект в правой части равенства.
• unsafe_unretained — аналогичен квалификатору assign.
• weak — практически аналогичен квалификатору assign, но с одним большим отличием. При работе с объектами, когда объект, присвоенный свойству такого типа, высвобождается из памяти, среда времени исполнения будет автоматически устанавливать значение этого свойства в nil.
Итак, у нас есть класс Person с двумя свойствами, firstName и lastName. Вернемся к файлу реализации делегата нашего приложения (AppDelegate.m) и создадим объект типа Person:
#import «AppDelegate.h»
#import «Person.h»
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
Person *person = [[Person alloc] init];
person.firstName = @"Steve";
person.lastName = @"Jobs";
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В этом примере мы выделяем и инициализируем наш экземпляр класса Person. Возможно, вы еще не понимаете, что это значит, но в подразделе «Добавление функционала к классам с помощью методов», приведенном далее, мы подробно об этом поговорим.
Добавление нового функционала к классам с помощью методов
Методы — это строительные блоки, из которых состоят классы. Например, класс Person может иметь логические возможности — обозначим их как «ходить», «дышать», «есть» и «пить». Обычно такие функции инкапсулируются в методах.
Метод может принимать параметры. Параметры — это переменные, передаваемые вызывающей стороной при вызове метода и видимые только этому методу. Например, в упрощенном мире у нашего класса Person был бы метод walk. Но вы могли бы добавить к этому методу параметр или аргумент и назвать его walkingSpeed. Этому параметру вы бы присвоили тип CGFloat. Теперь, если другой программист вызовет этот метод в вашем классе, он может указать, с какой скоростью будет идти Person. Вы как автор класса напишете соответствующий код, который будет обрабатывать различные скорости ходьбы Person. Не переживайте, если у вас возникает ощущение «как-то много работы получается». Рассмотрим следующий пример. В нем я добавил метод в файл реализации того класса Person, который мы создали в подразделе «Как создавать классы и правильно пользоваться ими» данного раздела.
#import «Person.h»
@implementation Person
— (void) walkAtKilometersPerHour:(CGFloat)paramSpeedKilometersPerHour{
/* здесь пишем код для этого метода */
}
— (void) runAt10KilometersPerHour{
/* Вызываем метод walk в нашем собственном классе и передаем значение 10 */
[self walkAtKilometersPerHour:10.0f];
}
@end
Типичный метод в языке Objective-C имеет следующие качества.
1. Префикс указывает компилятору, является ли данный код методом экземпляра (—) или методом класса (+). К методу экземпляра можно обратиться лишь после того, как программист выделит и инициализирует экземпляр вашего класса. Получить доступ к методу класса можно, вызвав его непосредственно из этого класса. Не волнуйтесь, если на первый взгляд это кажется сложным. В этой книге мы рассмотрим многочисленные примеры методов, пока просто следите за ходом рассказа.
2. Тип данных для метода, если метод возвращает какое-либо значение. В примере мы указали тип данных void. Так мы сообщаем компилятору, что не собираемся возвращать от метода какое-либо значение.
3. Первая часть имени метода, за которой идет первый параметр. Метод может и не иметь параметров. Методы, не принимающие параметров, довольно широко распространены.
4. Список последующих параметров, идущих за первым.
Рассмотрим пример метода с двумя параметрами:
— (void) singSong:(NSData *)paramSongData loudly:(BOOL)paramLoudly{
/* Параметры, к которым мы можем обратиться здесь, в этом методе, таковы:
paramSongData (для доступа к информации о песне)
paramLoudly сообщает нам, должны мы петь песню громко или нет
*/
}
Важно учитывать, что каждый параметр каждого метода обладает внешним и внутренним именем. Внешнее имя входит в состав метода, а внутреннее имя — это фактическое название (или псевдоним) параметра, которое может использоваться в пределах реализации метода. В предыдущем примере внешнее имя первого параметра — singSong, а внутреннее — paramSongData. Внешнее имя второго параметра — loudly, а внутреннее — paramLoudly. Имя метода и внешние имена его параметров вместе образуют сущность, которая называется селектором метода. В данном случае селектор упомянутого метода будет иметь вид singSong: loudly:. Как будет объяснено далее в этой книге, селектор является идентификатором каждого метода в среде времени исполнения. Никакие два метода в рамках одного и того же класса не могут иметь одинаковые селекторы.
В нашем примере мы определили в файле реализации класса Person (Person.m) три метода:
walkAtKilometersPerHour:;
• runAt10KilometersPerHour;
• singSong: loudly:.
Если бы мы хотели использовать любой из этих методов из какой-нибудь сущности, находящейся вне класса, например из делегата приложения, то должны были бы предоставить эти методы в нашем файле интерфейса (Person.h):
#import
@interface Person: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
— (void) walkAtKilometersPerHour:(CGFloat)paramSpeedKilometersPerHour;
— (void) runAt10KilometersPerHour;
/* Не предоставляем метод singSong: loudly: для доступа извне.
Этот метод является внутренним для нашего класса. Зачем же нам открывать к нему доступ? */
@end
Имея такой файл интерфейса, программист может вызывать методы walkAtKilometersPerHour: и runAt10KilometersPerHour извне класса Person. А метод singSong: loudly: так вызывать нельзя, поскольку он не предоставлен в файле интерфейса. Итак, продолжим: попробуем вызвать все три этих метода из делегата нашего приложения и посмотрим, что получится:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
Person *person = [[Person alloc] init];
[person walkAtKilometersPerHour:3.0f];
[person runAt10KilometersPerHour];
/* Если раскомментировать следующую строку кода, то компилятор выдаст
вам ошибку и сообщит, что такого метода в классе Person не существует */
//[person singSong: nil loudly: YES];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Итак, теперь мы умеем определять и вызывать методы экземпляров. А что насчет методов классов? Сначала разберемся, что такое методы классов и чем они отличаются от методов экземпляров.
Метод экземпляра — это метод, относящийся к экземпляру класса. Например, в нашем случае вы можете создать экземпляр класса Person дважды и получить в гипотетической игре, которую разрабатываете, двух разных персонажей. Один персонаж будет ходить со скоростью 3 км/ч, другой — 2 км/ч.
Пусть вы и написали код для метода экземпляра walk всего один раз, но когда во время исполнения создаются два экземпляра класса Person, поступающие от них вызовы методов экземпляра маршрутизируются к соответствующему экземпляру класса (тому, который выполнил вызов).
Напротив, методы класса работают только с самим классом. Например, в вашей игре есть экземпляры класса Light, отвечающего за подсвечивание сцен в вашей игре. У этого класса может быть метод dimAllLights. Вызвав этот метод, программист погасит в игре все источники света независимо от того, где они находятся. Рассмотрим пример метода класса, применяемого с нашим классом Person:
#import «Person.h»
@implementation Person
+ (CGFloat) maximumHeightInCentimeters{
return 250.0f;
}
+ (CGFloat) minimumHeightInCentimeters{
return 40.0f;
}
@end
Метод maximumHeightInCentimeters — это метод класса, возвращающий гипотетический максимальный рост любого персонажа в сантиметрах. Метод класса minimumHeightInCentimeters возвращает минимальный рост любого персонажа. Вот как мы предоставим оба этих метода в файле интерфейса нашего класса:
#import
@interface Person: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) CGFloat currentHeight;
+ (CGFloat) maximumHeightInCentimeters;
+ (CGFloat) minimumHeightInCentimeters;
@end
Мы добавили к нашему классу Person еще одно свойство, принимающее значения с плавающей точкой. Оно называется currentHeight. С его помощью экземпляры этого класса могут хранить информацию о своей высоте в памяти (для справки) — точно так же, как имя и фамилию.
А в делегате нашего приложения мы продолжим работать с методами вот так:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
Person *steveJobs = [[Person alloc] init];
steveJobs.firstName = @"Steve";
steveJobs.lastName = @"Jobs";
steveJobs.currentHeight = 175.0f; /* Сантиметры */
if (steveJobs.currentHeight >= [Person minimumHeightInCentimeters] &&
steveJobs.currentHeight <= [Person maximumHeightInCentimeters]){
/* Высота этого персонажа находится в пределах допустимого */
} else {
/* Высота этого персонажа находится вне пределов допустимого */
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Соблюдение требований, предъявляемых другими классами, с помощью протоколов
В языке Objective-C существует концепция под названием «протокол». Протоколы встречаются и во многих других языках, но называются везде по-разному; например, в Java аналогичная сущность называется «интерфейс». Как понятно из названия, протокол — это набор правил, которым класс должен соответствовать, чтобы его можно было использовать тем или иным образом. Если класс выполняет правила определенного протокола, то принято говорить, что он соответствует этому протоколу. Протоколы отличаются от самих классов тем, что не имеют реализации. Это просто правила. Например, у любой машины есть колеса, дверцы и цвет кузова, а также многие другие свойства. Определим эти свойства в протоколе Car. Просто выполните следующие шаги, чтобы создать заголовочный файл, который может содержать наш протокол Car.
1. Откройте ваш проект в Xcode и в меню File (Файл) выберите New-File (Новый — Файл).
2. Убедитесь, что слева, в разделе iOS, вы выбрали категорию Cocoa Touch. После этого выберите элемент Objective-C Protocol (Протокол для Objective-C) и нажмите Next (Далее).
3. В разделе Class (Класс) введите имя Car, затем нажмите кнопку Next (Далее).
4. Далее система предложит вам сохранить ваш протокол на диске. Просто выберите для этого место (как правило, в каталоге с вашим проектом) и нажмите кнопку Create (Создать).
После этого Xcode создаст для вас файл Car.h с таким содержимым:
#import
@protocol Car
@end
Продолжим и определим свойства для протокола Car, как мы обсуждали ранее в этом разделе:
#import
@protocol Car
@property (nonatomic, copy) NSArray *wheels;
@property (nonatomic, strong) UIColor *bodyColor;
@property (nonatomic, copy) NSArray *doors;
@end
Теперь, когда наш протокол определен, создадим класс, обозначающий автомобиль, — например, Jaguar, — а потом обеспечим соответствие этого класса протоколу. Просто выполните все шаги, перечисленные в подразделе «Как создавать классы и правильно пользоваться ими» данного раздела, после чего обеспечьте его соответствие протоколу Car следующим образом:
#import
#import «Car.h»
@interface Jaguar: NSObject
@
end
Если вы попробуете собрать ваш проект на данном этапе, то компилятор выдаст вам несколько предупреждений, например такое:
Auto property synthesis will not synthesize property declared in a protocol
Это означает, что ваш класс Jaguar пытается соответствовать протоколу Car, но на самом деле не реализует всех требуемых свойств и/или методов, описанных в этом протоколе. Теперь вы уже знаете, что в протоколе могут содержаться необходимые и факультативные (опциональные) элементы, которые вы помечаете ключевыми словами @optional или @required. По умолчанию действует квалификатор @required, и поскольку мы явно не указываем квалификатор для этого протокола, компилятор неявно выбирает @required за нас. Следовательно, класс Jaguar теперь обязан реализовывать все аспекты, требуемые протоколом Car, вот так:
#import
#import «Car.h»
@interface Jaguar: NSObject
@property (nonatomic, copy) NSArray *wheels;
@property (nonatomic, strong) UIColor *bodyColor;
@property (nonatomic, copy) NSArray *doors;
@end
Отлично. Теперь мы понимаем основы работы с протоколами, то, как они работают и как их определить. Далее в этой книге мы подробнее поговорим о протоколах, а на данный момент вы получили довольно полное представление о них.
Хранение элементов в коллекциях и получение элементов из коллекций
Коллекции — это такие объекты, в экземплярах которых могут храниться другие объекты. Одна из самых распространенных разновидностей коллекций — это массив, который инстанцирует NSArray или NSMutableArray. В массиве можно хранить любой объект, причем массив может содержать несколько экземпляров одного и того же объекта. В следующем примере мы создаем массив из трех строк:
NSArray *stringsArray = @[
@"String 1",
@"String 2",
@"String 3"
];
__unused NSString *firstString = stringsArray[0];
__unused NSString *secondString = stringsArray[1];
__unused NSString *thirdString = stringsArray[2];
Макрос __unused приказывает компилятору «не жаловаться», когда переменная — в нашем случае переменная firstString — объявлена, но ни разу не использовалась. По умолчанию в такой ситуации компилятор выдает в консоль предупреждение, сообщающее, что переменная не используется. В нашем кратком примере мы объявили переменные, но не задействовали их. Поэтому, если добавить вышеупомянутый макрос в начале объявления переменной, это вполне устроит и нас, и компилятор.
Изменяемый массив — это такой массив, в который можно вносить изменения уже после того, как он был создан. Как мы видели ранее, неизменяемый массив не может быть дополнен новой информацией уже после создания. Вот пример неизменяемого массива:
NSString *string1 = @"String 1";
NSString *string2 = @"String 2";
NSString *string3 = @"String 3";
NSArray *immutableArray = @[string1, string2, string3];
NSMutableArray *mutableArray = [[NSMutableArray alloc]
initWithArray: immutableArray];
[mutableArray exchangeObjectAtIndex:0 withObjectAtIndex:1];
[mutableArray removeObjectAtIndex:1];
[mutableArray setObject: string1 atIndexedSubscript:0];
NSLog(@"Immutable array = %@", immutableArray);
NSLog(@"Mutable Array = %@", mutableArray);
Вывод этой программы таков:
Immutable array = (
«String 1»,
«String 2»,
«String 3»
)
Mutable Array = (
«String 1»,
«String 3»
)
Еще одна распространенная коллекция, которая часто встречается в программах для iOS, — это словарь. Словари похожи на массивы, но каждому объекту в словаре присваивается ключ, и по этому ключу вы можете позже получить интересующий вас объект. Рассмотрим пример:
NSDictionary *personInformation =
@{
@"firstName": @"Mark",
@"lastName": @"Tremonti",
@"age": @30,
@"sex": @"Male"
};
NSString *firstName = personInformation[@"firstName"];
NSString *lastName = personInformation[@"lastName"];
NSNumber *age = personInformation[@"age"];
NSString *sex = personInformation[@"sex"];
NSLog(@"Full name = %@ %@", firstName, lastName);
NSLog(@"Age = %@, Sex = %@", age, sex);
А вот и вывод этой программы:
Full name = Mark Tremonti
Age = 30, Sex = Male
Можно также использовать изменяемые словари, которые довольно сильно похожи на изменяемые массивы. Содержимое изменяемого словаря можно изменить после того, как словарь инстанцирован. Пример:
NSDictionary *personInformation =
@{
@"firstName": @"Mark",
@"lastName": @"Tremonti",
@"age": @30,
@"sex": @"Male"
};
NSMutableDictionary *mutablePersonInformation =
[[NSMutableDictionary alloc] initWithDictionary: personInformation];
mutablePersonInformation[@"age"] = @32;
NSLog(@"Information = %@", mutablePersonInformation);
Вывод этой программы таков:
Information = {
age = 32;
firstName = Mark;
lastName = Tremonti;
sex = Male;
}
Еще можно работать с множествами. Множества похожи на массивы, но любой объект, входящий в состав множества, должен встречаться в нем только один раз. Иными словами, в одном множестве не может быть двух экземпляров одного и того же объекта. Пример множества:
NSSet *shoppingList = [[NSSet alloc] initWithObjects:
@"Milk",
@"Bananas",
@"Bread",
@"Milk", nil];
NSLog(@"Shopping list = %@", shoppingList);
Запустив эту программу, вы получите следующий вывод:
Shopping list = {(
Milk,
Bananas,
Bread
)}
Обратите внимание: элемент Milk упомянут в программе дважды, а в множество добавлен всего один раз. Эта черта множеств — настоящее волшебство. Изменяемые множества можно использовать и вот так:
NSSet *shoppingList = [[NSSet alloc] initWithObjects:
@"Milk",
@"Bananas",
@"Bread",
@"Milk", nil];
NSMutableSet *mutableList = [NSMutableSet setWithSet: shoppingList];
[mutableList addObject:@"Yogurt"];
[mutableList removeObject:@"Bread"];
NSLog(@"Original list = %@", shoppingList);
NSLog(@"Mutable list = %@", mutableList);
А вывод будет таким:
Original list = {(
Milk,
Bananas,
Bread
)}
Mutable list = {(
Milk,
Bananas,
Yogurt
)}
Обсуждая множества и коллекции, следует упомянуть еще два важных класса, о которых вам необходимо знать:
NSOrderedSet — неизменяемое множество, учитывающее, в каком порядке в него добавлялись объекты;
• NSMutableOrderedSet — изменяемый вариант вышеупомянутого изменяемого множества.
По умолчанию множества не учитывают, в каком порядке объекты в них добавлялись. Рассмотрим пример:
NSSet *setOfNumbers = [NSSet setWithArray:@[@3, @4, @1, @5, @10]];
NSLog(@"Set of numbers = %@", setOfNumbers);
Запустив эту программу, получим на экране следующий вывод:
Set of numbers = {(
5,
10,
3,
4,
1
)}
Но на самом деле мы наполняли множество элементами в другом порядке. Если вы хотите сохранить правильный порядок, просто воспользуйтесь классом NSOrderedSet:
NSOrderedSet *setOfNumbers = [NSOrderedSet orderedSetWithArray
:@[@3, @4, @1, @5, @10]];
NSLog(@"Ordered set of numbers = %@", setOfNumbers);
Разумеется, вы можете воспользоваться и изменяемой версией упорядоченного множества:
NSMutableOrderedSet *setOfNumbers =
[NSMutableOrderedSet orderedSetWithArray:@[@3, @4, @1, @5, @10]];
[setOfNumbers removeObject:@5];
[setOfNumbers addObject:@0];
[setOfNumbers exchangeObjectAtIndex:1 withObjectAtIndex:2];
NSLog(@"Set of numbers = %@", setOfNumbers);
А вот и результаты:
Set of numbers = {(
3,
1,
4,
10,
0
)}
Прежде чем завершить разговор о множествах, упомяну еще об одном удобном классе, который может вам пригодиться. Класс NSCountedSet может несколько раз содержать уникальный экземпляр объекта. Правда, в нем эта задача решается иначе, нежели в массивах. В массиве может несколько раз присутствовать один и тот же объект. А в рассматриваемом здесь «подсчитываемом множестве» каждый объект появляется в множестве как будто заново, но множество ведет подсчет того, сколько раз объект был добавлен в множество, и снижает значение этого счетчика на единицу, как только вы удалите из этого множества экземпляр данного объекта. Вот пример:
NSCountedSet *setOfNumbers = [NSCountedSet setWithObjects:
@10, @20, @10, @10, @30, nil];
[setOfNumbers addObject:@20];
[setOfNumbers removeObject:@10];
NSLog(@"Count for object @10 = %lu",
(unsigned long)[setOfNumbers countForObject:@10]);
NSLog(@"Count for object @20 = %lu",
(unsigned long)[setOfNumbers countForObject:@20]);
Вывод программы:
Count for object @10 = 2
Count for object @20 = 2
Класс NSCountedSet является изменяемым, хотя из его названия это и не следует.
Обеспечение поддержки подписывания объектов в ваших классах
Традиционно при необходимости доступа к объектам, содержащимся в коллекциях — например, массивах и словарях, — программисту требовалось получить доступ к методу в словаре или массиве, чтобы получить или установить желаемый объект. Например, создавая изменяемый словарь, мы добавляем в него два ключа и значения, получая эти значения обратно:
NSString *const kFirstNameKey = @"firstName";
NSString *const kLastNameKey = @"lastName";
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
[dictionary setValue:@"Tim" forKey: kFirstNameKey];
[dictionary setValue:@"Cook" forKey: kLastNameKey];
__unused NSString *firstName = [dictionary valueForKey: kFirstNameKey];
__unused NSString *lastName = [dictionary valueForKey: kLastNameKey];
Но с развитием компилятора LLVM этот код можно сократить, придав ему следующий вид:
NSString *const kFirstNameKey = @"firstName";
NSString *const kLastNameKey = @"lastName";
NSDictionary *dictionary = @{
kFirstNameKey: @"Tim",
kLastNameKey: @"Cook",
};
__unused NSString *firstName = dictionary[kFirstNameKey];
__unused NSString *lastName = dictionary[kLastNameKey];
Как видите, мы инициализируем словарь, давая ключи в фигурных скобках. Точно так же можно поступать и с массивами. Вот как мы обычно создаем и используем массивы:
NSArray *array = [[NSArray alloc] initWithObjects:@"Tim", @"Cook", nil];
__unused NSString *firstItem = [array objectAtIndex:0];
__unused NSString *secondObject = [array objectAtIndex:1];
А теперь, имея возможность подписывать объекты, мы можем сократить этот код следующим образом:
NSArray *array = @[@"Tim", @"Cook"];
__unused NSString *firstItem = array[0];
__unused NSString *secondObject = array[0];
Компилятор LLVM не останавливается и на этом. Вы можете также добавлять подписывание и к собственным классам. Существует два типа подписывания:
подписывание по ключу — действуя таким образом, вы можете задавать внутри объекта значение для того или иного ключа точно так же, как вы делали бы это в словаре. Указывая ключ, вы также можете получать доступ к значениям внутри объекта и считывать их;
подписывание по индексу — как и при работе с массивами, вы можете устанавливать/получать значения внутри объекта, предоставив для этого объекта индекс. Это целесообразно делать в массивоподобных классах, где элементы естественным образом располагаются в порядке, удобном для индексирования.
Сначала рассмотрим пример подписывания по ключу. Для этого создадим класс под названием Person, имеющий свойства firstName и lastName. Далее мы позволим программисту менять значения этих свойств (имя и фамилию), просто предоставив ключи для этих свойств.
Вам может понадобиться добавить к классу подобный механизм подписывания по ключу, например, по такой причине: имена ваших свойств могут изменяться и вы хотите предоставить программисту возможность устанавливать значения таких свойств, не учитывая, будут ли имена этих свойств впоследствии изменяться. В противном случае программисту лучше будет использовать свойства напрямую. Другая причина реализации подписывания по ключу — стремление скрыть точную реализацию/объявление ваших свойств от программиста и закрыть программисту прямой доступ к этим свойствам.
Чтобы обеспечить поддержку подписывания по ключу в ваших собственных классах, вы должны реализовать в вашем классе два следующих метода и записать сигнатуры методов в файле заголовков этого класса. В противном случае компилятор не узнает, что в вашем классе поддерживается подписывание по ключу.
#import
/* Мы будем использовать их как ключи для наших свойств firstName
и lastName, так что если имена наших свойств firstName и lastName
в будущем изменятся в реализации, нам не придется ничего переделывать
и наш класс останется работоспособным, поскольку мы сможем просто
изменить значения этих констант в нашем файле реализации */
extern NSString *const kFirstNameKey;
extern NSString *const kLastNameKey;
@interface Person: NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
— (id) objectForKeyedSubscript:(id)paramKey;
— (void) setObject:(id)paramObject forKeyedSubscript:(id)paramKey;
@end
Метод objectForKeyedSubscript: будет вызываться в вашем классе всякий раз, когда программист предоставит ключ и захочет прочитать в вашем классе значение, соответствующее данному ключу. Очевидно, тот параметр, который будет вам передан, будет представлять собой ключ, по которому программист хочет считать интересующее его значение. Дополнительно к этому методу мы будем вызывать в нашем классе метод setObject: forKeyedSubscript: всякий раз, когда программист захочет задать значение для конкретного ключа. Итак, в данной реализации мы хотим проверить, ассоциированы ли заданные ключи с именами и фамилиями. Если это так, то собираемся установить/получить в нашем классе значения имени и фамилии:
#import «Person.h»
NSString *const kFirstNameKey = @"firstName";
NSString *const kLastNameKey = @"lastName";
@implementation Person
— (id) objectForKeyedSubscript:(id)paramKey{
NSObject *keyAsObject = (NSObject *)paramKey;
if ([keyAsObject isKindOfClass: [NSString class]]){
NSString *keyAsString = (NSString *)keyAsObject;
if ([keyAsString isEqualToString: kFirstNameKey] ||
[keyAsString isEqualToString: kLastNameKey]){
return [self valueForKey: keyAsString];
}
}
return nil;
}
— (void) setObject:(id)paramObject forKeyedSubscript:(id)paramKey{
NSObject *keyAsObject = (NSObject *)paramKey;
if ([keyAsObject isKindOfClass: [NSString class]]){
NSString *keyAsString = (NSString *)keyAsObject;
if ([keyAsString isEqualToString: kFirstNameKey] ||
[keyAsString isEqualToString: kLastNameKey]){
[self setValue: paramObject forKey: keyAsString];
}
}
}
@end
Итак, в этом коде мы получаем ключ в методе objectForKeyedSubscript:, а в ответ должны вернуть объект, который ассоциирован в нашем экземпляре с этим ключом. Ключ, который получаем, — это объект, соответствующий протоколу NSCopying. Это означает, что при желании мы можем сделать копию такого объекта. Рассчитываем на то, что ключ будет представлять собой строку, чтобы мы могли сравнить его с готовыми ключами, которые были заранее объявлены в начале класса. В случае совпадения зададим значение данного свойства в этом классе. После этого воспользуемся методом valueForKey:, относящимся к объекту NSObject, чтобы вернуть значение, ассоциированное с заданным ключом. Но, разумеется, прежде, чем так поступить, мы должны гарантировать, что данный ключ — один из тех, которые мы ожидаем. В методе setObject: forKeyedSubscript: мы делаем совершенно противоположное — устанавливаем значения для заданного ключа, а не возвращаем их.
Теперь в любой части вашего приложения вы можете инстанцировать объект типа Person и использовать заранее определенные ключи kFirstNameKey и kLastNameKey, чтобы изменить значения свойств firstName и lastName, вот так:
Person *person = [Person new];
person[kFirstNameKey] = @"Tim";
person[kLastNameKey] = @"Cook";
__unused NSString *firstName = person[kFirstNameKey];
__unused NSString *lastName = person[kLastNameKey];
Этот код позволяет достичь точно того же результата, что и при более лобовом подходе, когда мы устанавливаем свойства класса:
Person *person = [Person new];
person.firstName = @"Tim";
person.lastName = @"Cook";
__unused NSString *firstName = person.firstName;
__unused NSString *lastName = person.lastName;
Вы также можете поддерживать и подписывание по индексу — точно как при работе с массивами. Как было указано ранее, это полезно делать, чтобы обеспечивать программисту доступ к объектам, выстраиваемым в классе в некоем естественном порядке. Но, кроме массивов, существует не так уж много структур данных, где целесообразно упорядочивать и нумеровать элементы, чего не скажешь о подписывании по ключу, которое применяется в самых разных структурах данных. Поэтому пример, которым иллюстрируется подписывание по индексу, немного надуман. В предыдущем примере у нас существовал класс Person с именем и фамилией. Теперь мы хотим предоставить программистам возможность считывать имя, указывая индекс 0, а фамилию — указывая индекс 1. Все, что требуется сделать для этого, — объявить методы objectAtIndexedSubscript: и setObject: atIndexedSubscript: в заголовочном файле класса, а затем написать реализацию. Вот как мы объявляем два этих метода в заголовочном файле класса Person:
— (id) objectAtIndexedSubscript:(NSUInteger)paramIndex;
— (void) setObject:(id)paramObject atIndexedSubscript:(NSUInteger)paramIndex;
Реализация также довольно проста. Мы берем индекс и оперируем им так, как это требуется в нашем классе. Ранее мы решили, что у имени должен быть индекс 0, а у фамилии — индекс 1. Итак, получаем индекс 0 для задания значения, присваиваем значение имени первому входящему объекту и т. д.:
— (id) objectAtIndexedSubscript:(NSUInteger)paramIndex{
switch (paramIndex){
case 0:{
return self.firstName;
break;
}
case 1:{
return self.lastName;
break;
}
default:{
[NSException raise:@"Invalid index" format: nil];
}
}
return nil;
}
— (void) setObject:(id)paramObject atIndexedSubscript:(NSUInteger)paramIndex{
switch (paramIndex){
case 0:{
self.firstName = paramObject;
break;
}
case 1:{
self.lastName = paramObject;
break;
}
default:{
[NSException raise:@"Invalid index" format: nil];
}
}
}
Теперь можно протестировать весь написанный ранее код вот так:
Person *person = [Person new];
person[kFirstNameKey] = @"Tim";
person[kLastNameKey] = @"Cook";
NSString *firstNameByKey = person[kFirstNameKey];
NSString *lastNameByKey = person[kLastNameKey];
NSString *firstNameByIndex = person[0];
NSString *lastNameByIndex = person[1];
if ([firstNameByKey isEqualToString: firstNameByIndex] &&
[lastNameByKey isEqualToString: lastNameByIndex]){
NSLog(@"Success");
} else {
NSLog(@"Something is not right");
}
Если вы правильно выполнили все шаги, описанные в этом разделе, то на консоли должно появиться значение Success.
1.1. Отображение предупреждений с помощью UIAlertView