Что может быть хуже того момента, когда App Store отвергает ваше приложение из-за багов? Когда приложение с кучей багов размещается в магазине. Оно получает один негативный отзыв, второй… Репутация компании и разработчика катится вниз и восстановить её уже очень сложно.
iOS – вторая по популярности мобильная ОС в мире, причём 65% пользователей использует самую свежую версию. И каждый из них ждёт от любого приложения качества и высокой стабильности. В ситуации, когда к команде разработчиков каждый день присоединяется 1000 новичков, добиться этого не просто. Ниже приведены 10 наиболее популярных ошибок по версии Toptal, которые совершают неопытные iOS-разработчики. Запомните и постарайтесь избегать их.
Отсутствие понимания устройства асинхронных процессов
Одна из наиболее распространённых ошибок – неправильная обработка асинхронного кода. Давайте рассмотрим простой пример: пользователь открывает страницу с таблицей, данные подгружаются с сервера и размещаются в ней. Описать процесс можно так:
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
На первый взгляд всё в порядке, но давайте проанализируем: мы сначала получаем данные, потом обновляем UI. Загвоздка в том, что получение данных – асинхронный процесс, и новые данные не будут получены до перегрузки интерфейса. Поэтому данный код необходимо переписать, поставив строку «2» сразу после «1»:
@property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }
Впрочем, и такая запись может не привести к нужному результату, если…
Запуск кода, связанного с UI, не в главном потоке
Итак, вы переписали код, но наши таблицы всё ещё не заполнены обновлёнными данными. Неужели есть ещё ошибка в столь простом коде? Для поиска ответа остановим код внутри блока и посмотрим, в какой очереди он вызывается. Возможно, он не обновился из-за того, что пользовательский интерфейс обслуживается вне главной очереди.
Многие популярные библиотеки (Alamofire, AFNetworking и Haneke) требуют вызова completionBlock в основной очереди. Но иногда разработчики просто забывают об этом. А ведь сделать это так просто:
dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });
Непонимание многопоточности и параллелизма
Параллельную обработку можно сравнить с очень острым ножом: вы можете легко пораниться, если не будете осторожны, но при правильном использовании она придаст коду невероятную эффективность.
Безусловно, в подавляющем большинстве задач вы можете обойтись без параллелизма, но его использование даст следующие преимущества:
- Почти каждое мобильное приложение использует веб-сервисы (к примеру, для вычислений или работы с БД). Если вы поместите их в главную очередь, то приложение или «подвиснет» на время выполнения, или iOS его закроет, если это затянется надолго. Именно поэтому перемещение таких операций в параллельный поток – прекрасный выход из ситуации.
- Все современные iOS-устройства имеют несколько ядер, так почему бы не воспользоваться этим для повышения быстродействия?
Но, как уже было сказано, пользоваться параллелизмом надо уметь. Давайте рассмотрим пару популярных ошибок, связанных с ним (часть кода опущена для удобства):
Случай 1
final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }
Мультипоточный код:
let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }
Итак, мы создали ThreadSafeVar для обработки counter, что должно сделать работу с потоками безопасной. Или нет? Два потока могут достигать линии инкремента одновременно, поэтому выражение counter.value == someValue никогда не станет истиной. Для разрешения этой ситуации создадим ThreadSafeCounter, который возвращает значение после увеличения:
class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }
Случай 2
struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }
Здесь dispatch_barrier_sync используется для синхронизации доступа к массиву. К сожалению, код не учитывает, что такой алгоритм будет создавать копию каждый раз при добавлении элемента, что означает добавление очереди к синхронизации.
Да, такой код будет работать. Но потребуется провести несколько тестов, чтобы удостовериться в том, что потери производительности незначительны. В противном случае стоит искать другое решение.
Незнание тонкостей работы с переменными объектами
Swift очень полезен для предотвращения ошибок с типами, но iOS-разработчики используют также Objective-C. Именно здесь существует опасность с переменными объектами, которые могут приводить к скрытым проблемам. Известно, что неизменяемые объекты должны вызываться из функций, но, к сожалению, немногие знают, почему. Давайте рассмотрим следующий код:
// Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end
Код корректен, NSArray является подклассом NSMutableArray. Так что может пойти не так?
Чаще всего проблема возникает, когда другой разработчик решает сделать следующее:
NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }
Это действие крайне негативно скажется на работе класса.
А вот другой случай, в результате которого программа поведёт себя непредсказуемо:
Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];
Вы ожидаете, что [newChildBoxes count] > [childBoxes count], но что если не так? В этом случае класс плохо описан, так как он меняет значение, которое уже возвращено.
Исправить это можно, если вы допишете в начальный код:
- (NSArray *)boxes { return [self.m_boxes copy]; }
Непонимание принципов работы NSDictionary
Если вы когда-нибудь работали с NSDictionary и произвольным классом, то знаете, что не можете использовать класс, если он не соответствует NSCopying в качестве ключа словаря. Многие iOS-разработчики задаются вопросом, зачем Apple добавила это ограничение.
Вам поможет понимание работы NSDictionary. Технически это всего лишь хэш-таблица. Упрощённо рассмотрим, как она работает при добавлении объекта в качестве ключа:
- Шаг 1: рассчитывается hash(Key).
- Шаг 2: основываясь на хэше, ищется место для размещения объекта. Обычно это делается путем вычисления модуля хэш-значения со значением словаря. Затем полученный индекс используется для хранения пары «ключ / значение».
- Шаг 3: если в этом месте отсутствует объект, то создаётся связанный список для записи и хранения нашей пары «ключ/значение». В противном случае пара добавляется в конец списка.
А вот как извлекается:
- Шаг 1: высчитывается hash(Key).
- Шаги2: ищется ключ по хэшу. Если данные отсутствует, возвращается nil.
- Шаг 3: если там связанный список, выполняются итерации объекта, пока [stored_key isEqual:Key].
На основании этого мы можем сделать два вывода:
- Если хэш ключа изменяется, запись должна быть перенесена в другой связанный список.
- Ключи должны быть уникальными.
Давайте рассмотрим это на простом классе:
@interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end
Теперь представьте, что NSDictionary не копирует ключи:
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;
Потом мы обнаруживаем опечатку и исправляем её:
p.name = @"Jon Snow";
Что происходит со словарём? Поскольку имя изменено, изменился и хеш. Теперь наш объект находится в неправильном месте, так как всё ещё имеет старое значение хэша, а словарь не знает об изменении. Таким образом, не ясно, какой хэш мы должны использовать для поиска данных в словаре.
Или ещё хуже. Представьте себе, что мы уже имели «Jon Snow» в нашем словаре с рейтингом 5. Словарь будет иметь два разных значения для одного и того же ключа.
Как видите, невнимательность может создать много проблем при работе с изменяемыми ключами NSDictionary. Лучшим выходом будет копирование объектов перед сохранением с соответствующей пометкой.
Использование StoryBoard вместо XIB
Большинство новых разработчиков iOS следуют рекомендациям Apple и используют сториборды по умолчанию для UI. У такого подхода есть не только [спорные] преимущества, но и явные недостатки. Начнём с плохого:
- Использование Storyboard несколькими членами команды – крайне сложная задача. Технически реально использовать несколько сторибордов, но для этого придётся прописывать переходы.
- Имена переходов и контроллеров в сторибордах – строки, которые вам придётся прописывать в коде (а из-за их количества это его уничтожит). Или создавать огромный список констант. Можно ещё использовать SBConstants, но это тоже не сильно упрощает задачу.
- Использование сторибордов практически исключает модульное программирование из-за малого числа повторов. Для минимального продукта (MVC) или прототипа это не критично, но для настоящего приложения это очень важный недостаток.
Преимущества:
- Навигация интуитивно понятна. Теоретически. Фактически реальное приложение имеет десятки контроллеров, подключённых в разных направлениях. То есть навигация будет выглядеть как большой клубок ниток, который точно не даст вам понимания о взаимодействии данных.
- Статические таблицы. Это неоспоримое преимущество, если не считать того, что 90% всех статических таблиц рано или поздно становится динамическими. А в этом случае лучше работать с XIB.
Путаницы со сравнением указателей и объектов
При сравнении двух объектов мы можем использовать два подхода: сопоставление указателей и самих объектов.
Равенство указателей означает, что оба ссылаются на один и тот же объект. В Objective-C мы используем для этого ==. Равенство объектов означает, что оба логически идентичны. К примеру, как один и тот же пользователь из разных таблиц. В Objective-C для этого используется isEqual или, что даже лучше, isEqualToString, isEqualToDate и т.д.
Взгляните на следующий код:
NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }
Что появится в консоли, когда мы запустим код? Мы увидим «a is equal to b», так как оба указателя ссылаются на один и тот же объект в памяти.
Но теперь давайте изменим строку # 2 на:
NSString *b = [[@"a" mutableCopy] copy];
И теперь мы увидим «a is NOT equal to b» потому что указатели ссылаются на разные объекты, хоть визуально они и идентичны.
Проблема решает использованием isEqual или типизированной функцией. Внесём изменение в строку «3» и запишем код правильно:
if ([a isEqual:b]) {
Использование строго заданных значений
Существуют две основные проблемы со строго заданными значениями:
- Часто неясно, что они представляют.
- Если они используются в нескольких местах в коде, они должны быть повторно введены (или скопированы и вставлены).
Взгляните на пример:
if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];
Что такое 172800 и почему именно это значение? На самом деле это число секунд в 2 сутках (то есть 24*60*60*2).
Вместо подобной записи вы можете определить значение с помощью инструкции #define. Например:
#define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"
#define – это макрос препроцессора, который заменяет значение именем в коде. То есть, если вы пропишите #define в файле заголовка и импортируете его где-нибудь, все вхождения будут заменены на строго заданные значения.
Но это решение не работает, когда возникает путаница с типами. Для его иллюстрации взгляните на код:
#define X = 3 ... CGFloat y = X / 2;
Наверняка вы ждёте, что значение y будет 1.5, но это не так. На самом деле оно примет значение 1. Причина в том, что #define не имеет информации о типе. Так что в нашем случае на основании двух значений типа Int (3 и 2) получается результат также типа Int вместо Float.
Этого можно избежать, используя константы:
static const CGFloat X = 3; ... CGFloat y = X / 2; // y теперь 1.5
Использование default в конструкции switch
Использование выражения default в конструкции switch может привести к ошибкам и неправильной работе. Взгляните на код, написанный на Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }
Аналогичный код на Swift:
enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }
Данный код описывает алгоритм, позволяющий вносить изменения только администраторам. Но что произойдёт, если мы захотим открыть доступ ещё и менеджерам? Ничего не получится, если мы не обновим блок кода switch. Однако если вместо default использовать значения enum, изменения будут учтены при компиляции и вы сможете это исправить перед тестированием или выпуском приложения. Вот так это должно выглядеть на Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }
Так – на Swift:
enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }
Использование NSLog для журнала логов
Многие iOS-разработчики используют NSLog в своих приложениях, чтобы вести служебные записи, однако это может стать большой ошибкой. Взгляните на документацию Apple, а именно на описание функции NSLog. Всё очень просто:
void NSLog(NSString *format, ...);
Проблема в том,что, если вы подключите своё устройство к XCode Organizer, увидите все свои отладочные сообщения. Именно по этой причине не стоит использовать NSLog для журнала: он содержит много нежелательной информации, кроме того, это выглядит непрофессионально.
Поэтому лучше заменить NSLogs на настраиваемый CocoaLumberjack или какой-нибудь фрейморк для протоколирования.
iOS — очень мощная и быстро развивающаяся платформа. Apple прилагает огромные усилия, чтобы внедрить новое оборудование и функции для самой iOS, а также постоянно расширять язык Swift.
Знание Objective-C и Swift сделает вас отличным разработчиком iOS и предоставит возможности для работы над сложными проектами с использованием передовых технологий.
Пройти обучение
Комментарии