Детектируем недоступные API на данном minimal deployment target

     

Как правило iOS приложения (да и в общем macOS тоже) пишутся с использованием самых новых инструментов разработки (читай последних SDK), но при этом поддерживаются предыдущие версии iOS (macOS). Бывают ситуации, когда по недосмотру используются API из новых версий, которые недоступны на данном minimal deployment target. Если такую ошибку пропустить в прод (зарелизить приложение), то вызов такого метода на версии iOS которая его не поддерживает неминуемо приведет к крэшу. Дабы облегчить себе жизнь можно сделать вот что:

  1. Идем в Build Settings нужного проекта в xcode
  2. Находим раздел “Apple LLVM 8.0 - Custom Compiler Flags”
  3. Находим там пункт “Other Warning Flags”
  4. В него для требуемой настройки сборки (например Debug) добавляем такой флаг “-Wpartial-availability”

После этого Xcode начинает отображать такие места как #warning

Для того, чтобы Xcode не писал #warning’и для тех мест где вы делаете это намерянно, можно переопределить (где-нибудь в pre-compile заголовках) такие макро:

#define START_IGNORE_PARTIAL _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wpartial-availability\"")
#define END_IGNORE_PARTIAL _Pragma("clang diagnostic pop")

Поваренная книга ARC

 
 

  • Про ARC можно почитать тут: Transitioning to ARC Release Notes
  • Майк Эш (Mike Ash) написал хорошую статью в его Friday Q&As
  • Детальная техническая документация доступна на сайте CLANG’а проекта LLVM.

Описанные в этой статье рецепты подразумевают использование iOS 5 и выше. В iOS 4 не доступны weak ссылки, которые являются очень важной вещью. Я понимаю, что в конце 2016 года уже даже iOS 7 официально не поддерживается, но вдруг найдутся такие читатели, которые вынуждены поддерживать очень старый код.

В статье в примерах используется Objective-C. Примеры на Swift’е не привожу - так как там это во-первых все несколько проще, во-вторых, кажется не составит труда отобразить эти правила на Swift, если это необходимо.

Общее

  • скалярные ivar свойства должны использовать assign
@property (nonatomic, assign) int scalarInt;
@property (nonatomic, assign) CGFloat scalarFloat;
@property (nonatomic, assign) CGPoint scalarStruct;
  • ссылочные ivar свойства, на которые необходимо иметь сильную ссылку или это дочерние свойства (“вниз” по иерархии) должны использовать strong
@property (nonatomic, strong) id childObject;
  • ссылочные ivar свойства на родителя (“вверх” по иерархии) должны использовать weak. Кроме того, при ссылках “вне” иерархии (например, делегаты) лучше использовать weak как наиболее безопасный.
@property (nonatomic, weak) id parentObject;
@property (nonatomic, weak) NSObject <SomeDelegate> *delegate;
  • в блоках лучше использовать copy
@property (nonatomic, copy) SomeBlockType someBlock;
  • В dealloc
    • удаляем обсерверы (observers)
    • отписываемся от уведомлений
    • для всех не weak свойств устанавливаем nil
    • инвалидируем все таймеры
  • IBOutlet’ы должны быть weak за исключением корневых, которые должны быть strong.

Bridging

Из документации

id myId;
CFStringRef myCFRef;
NSString   *a = (__bridge NSString  *)myCFRef;          // тривиальное преобразование (noop)
CFStringRef b = (__bridge CFStringRef)myId;             // тривиальное преобразование (noop)
NSString   *c = (__bridge_transfer NSString  *)myCFRef; // -1 на CFRef
CFStringRef d = (__bridge_retained CFStringRef)myId;    // возвращает CFRef +1

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

  • __bridge - тривиальное преобразование (по отношению к управлению памятью) (noop)
  • __bridge_transfer - необходимо для преобразования CF ссылок в объекты Objective-C. ARC уменьшит счетчик ссылок объекта CF, поэтому убедитесь, что CFRef имеет +1.
  • __bridge_retained - необходимо для преобразования объектов Objective-C в CF ссылки. Эта операция даст вым CF ссылку с +1. Далее вы будете ответственны за вызов CFRelease у полученной CFRef ссылки где-нибудь в будущем.

NSError

Вездесущий NSError с точки зрения ARC непростой. Типовое соглашение Cocoa гласит, что ошибки обычно передаются как out-параметры (косвенные указатели).

В ARC out-параметры по-умолчанию __autoreleasing и должны реализовываться так:

- (BOOL)performWithError:(__autoreleasing NSError **)error
{
    // ... случилась ошибка ...
    if (error)
    {
        // записываем ее в out-параметр. ARC автоматически autorelease'ит
        *error = [[NSError alloc] initWithDomain:@""
                                            code:-1
                                        userInfo:nil];
        return NO;
    }
    else
    {
        return YES;
    }
}

При использовании out-параметров, вы как правило будет использовать __autoreleasing на ваших *error объектах примерно таким образом:

NSError __autoreleasing *error = error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK)
{
    // обрабатываем ошибку.
}

Если __autoreleasing не указать, компилятор автоматически добавит для вас временный autoreleasing объект. Такое решение необходимо как компромисс для обеспечения обратной совместимости. В далекие времена iOS 5 существовали такие настройки компилятора, которые не добавляли автоматически __autoreleasing.

@autoreleasepool

Используйте @autoreleasepool внутри циклов, когда:

  • в нем очень много итераций
  • в одной итерации создается большое количество временных объектов
// если someArray большой
for (id obj in someArray)
{
    @autoreleasepool
    {
        // или вы создаете много
        // временных объектов тут
    }
}

Создание и уничтожение autorelease пулов через @autoreleasepool дешевле чем даром. Не волнуйтесь использовать их внутри цикла. Если же этих заверений недостаточно, можете воспользоваться профайлером.

Blocks

В общем, блоки просто работают, однако есть несколько исключений.

Перед тем как добавлять блоки в коллекцию, необходимо предварительно их скопировать (copy).

someBlockType someBlock = ^{NSLog(@"hi");};
[someArray addObject:[someBlock copy]];

retain-циклы особенно опасны в блоках. Вы могли видеть такой warning:

warning: capturing 'self' strongly in this
block is likely to lead to a retain cycle
[-Warc-retain-cycles,4]
SomeBlockType someBlock = ^{
    [self someMethod];
};

Идея здесь такая. self имеет сильную ссылку на someBlock, someBlock “захватывает” и держит сильную ссылку на self. В следующем примере мы имеем дело с тем же retain циклом, но он уже менее очевиден:

// блок захватит self сильной ссылкой
SomeBlockType someBlock = ^{
    BOOL isDone = _isDone;  // _isDone - это ivar в self
};

Более безопасно (правда малость многословно) это можно сделать с использованием weakSelf:

__weak SomeObjectClass *weakSelf = self;

SomeBlockType someBlock = ^{
    SomeObjectClass *strongSelf = weakSelf;
    if (strongSelf == nil)
    {
        // Оригинальный self здесь не существует.
        // Игнорируйте, уведомите о или каким-то еще способ обработайте этот случай.
    }
    else
    {
        [strongSelf someMethod];
    }
};

Иногда, необходимо отдельно позаботиться о конкретных объектах, чтобы избежать retain-циклов: если someObject будет захвачен сильной ссылок в блоке, который использует someObject, вам потребуется weakSomeObject для решения проблемы.

SomeObjectClass *someObject = ...
__weak SomeObjectClass *weakSomeObject = someObject;

someObject.completionHandler = ^{
    SomeObjectClass *strongSomeObject = weakSomeObject;
    if (strongSomeObject == nil)
    {
        // Оригинальный someObject здесь не существует.
        // Игнорируйте, уведомите о или каким-то еще способ обработайте этот случай.
    }
    else
    {
        // отлично, ТЕПЕРЬ мы можем что-нибудь сделать с someObject
        [strongSomeObject someMethod];
    }
};

Использование CG из NS или UI

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor;
// делаем что-нибудь с redRef.

Этот примерн имеет несколько малозаметных проблем. Когда вы создаете redRef, если redColor более нигде не используется, то он удаляется сразу после комментария.

Проблема здесь в том, что redColor “владеет” redRef, и когда redRef используется, redColor’а уже может не быть. Кроме того, такие проблемы редко проявляют себя в симуляторе. Более вероятно, что эта проблема “выстрелит” где-нибудь на устройстве, у которого осталось мало свободной памяти.

Существует несколько workaround’ов. Обычно, необходимо лишь убедиться, что redColor существует все время, пока вы используете redRef.

Самый простой способ добиться этого - использовать __autoreleasing.

UIColor * __autoreleasing redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor;

Теперь, redColor не будет уничтожен до тех пор в будущем, пока метод не вернет управление. Поэтому мы можем спокойно использовать redRef в рамках нашего метода.

Другой путь - получить +1 на redRef:

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = CFRetain(redColor.CGColor);
// используем redRef и когда закончим - удаляем его:
CFRelease(redRef)

Важно, что необходимо вызывать CFRetain() в той же строке, в которой вы получаете redColor.CGColor. redColor будет уничтожен сразу после последнего использования, следующий код работать НЕ будет:

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor; // redColor будет удален сразу после этого ...
CFRetain(redRef);  // Тут будет крэш ...
...

Есть еще один интересный момент относительно строки “Тут будет крэш”. Как правило, крэш в этой строке в симуляторе не случится, но обязательно произойдет на реальном устройстве.

Singletons

Связаны с ARC только косвенно. Существует огромное количество реализаций singleton’ов “на коленке” (некоторые из них даже переопределяют retain и release).

Вот правильная реализация singleton’а которую стоит использовать:

+ (MyClass *)singleton
{
    static MyClass *sharedMyClass = nil;
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        sharedMyClass = [[self alloc] init];
    });
    return sharedMyClass;
}

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

// определяем статическую переменную вне метода singleton'а
static MyClass *__sharedMyClass = nil;

+ (MyClass *)singleton
{
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        __sharedMyClass = [[self alloc] init];
    });
    return __sharedMyClass;
}

// Только для использования библиотекой тестирования!!!
- (void)destroyAndRecreateSingleton
{
    __sharedMyClass = [[self alloc] init];
}

Заметки об IoC и DI

 
 

IoC - паттерн инверсии управления (Inversion of Control), почитать можно тут DI - паттерн внедрения зависимостей (Dependency Injection), почитать можно тут

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

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

protocol Inbox {
    func allLetters() -> [Mail]
}

class EmailInbox : Inbox { ... }

class MailService
{
    var inbox: Inbox = EmailInbox("null@example.com")

    func mail(with recipient: String) -> [Mail]
    {
        let letters = inbox.allLetters()
        return letters.filter { $0.recipient == recipient }
    }
}

Примерчик простой, однако даже в нем уже есть проблема - класс MailService крепко-накрепко связан с классом EmailInbox, несмотря даже на то, что мы честно выделили протокол Inbox. Теперь (например, в другом проекте) потребовалось использовать MailService, но при этом Inbox там реализован по другому (например через файлы). Это приведет к тому, что придется код переписывать, не получится просто взять и использовать.

Ситуацию можно немного ухудшить, написав примерно так.

class MailService
{
    func mail(with recipient: String) -> [Mail]
    {
        return EmailInbox.shared.allLetters().filter { $0.recipient == recipient }
    }
}

Теперь внешнему наблюдателю сразу даже будет непонятно, что внутри класса есть зависимость. Да при детальном анализе кода будет ясно, но если класс не из 10 строк, а из 1000? По этой же причине опасны singleton’ы.

Для обеспечения переносимости кода, для возможности организовать хорошее тестирование (например заmock’ать какие-нибудь зависимости) и т.д. с этим надо что-то делать. И вот тут-то и появляются разного рода умные слова, такие как Dependency Injection, Inversion of Control и иногда Service Locator. Как правило, считается что они значат примерно одно и то же, хотя если разбираться детальнее разница между ними есть и большая.

Самый первый и самый с одной стороны простой термин - Dependency Injection или по-русски “внедрение зависимостей”. Вообще, это комплекс мероприятий в результате которого зависимости оказываются в вашем объекте. Даже наш простой примерчик в общем делает dependency injection в самом простом его виде - зависимость просто создается. Внедрять зависимости можно спрашивая их у кого-то (см. Service Locator) или кто-то их нам установит автоматически (см. Inversion of Control).

let mailService = MailService(...)
let inbox = EmailInbox(...)

mailServie.inbox = inbox // dependency injection

Service Locator - это специальный объект (или фасад) у которого вы всегда сможете спросить нужную зависимость. Как правило, он либо знает где ее взять (на то он и locator), либо он сам хранит их у себя (см. паттерн реестр). Когда кому-то нужна какая-то зависимость, этот кто-то через механизмы singleton’а (или еще как) идет к Service Locator’у и спрашивает нужную зависимость.

class MailService
{
    var inbox: Inbox = ServiceLocator.shared.inbox // service locator
    ...
}

Третий термин (Inversion of Control) рассмотрим подробнее.

Но сначала давайте разберемся с тем, что за “control” инвертируется. Когда-то давно, когда приложения были консольными, control-flow приложения выглядел примерно так: мы запрашиваем с устройства ввода у пользователя данные, обрабатываем их и выводим на устройство вывода. Потоком управления управляло (пардон за тавталогию) само приложение. Когда появились графические UI приложение вместо того, чтобы управлять циклом приложения стало обрабатывать событие, отдав управление операционной системе. Таким образом получилась инверсия control-flow приложения. Таким же образом обстоит дело с инверсией при внедрении зависимостей при использовании паттерна Inversion of Control - управление временем жизни объекта и внедрением зависимостей делегируется специальному объекту, в задачу которого входит создание всех необходимых объектов и установка ссылок между ними. Этот специальный объект может называться сборщиком (assembler), контейнером (inversion of control container, см. Spring Framework) или еще как-нибудь, сути его это не меняеет.

class IoCContainer // Inversion of Control container (assembler)
{
    let mailService: MailService!
    let inbox: Inbox

    func build()
    {
        create()
        internlink()
        post()
    }

    private func create() {
        mailService = MailService()
        inbox = EmailInbox(...)
    }

    private func interlink() {
        mailService.inbox = inbox
    }

    private func post() {
        mailService.postCreate()
    }
}

Главное отличие контейнера от сервис локатора в том, что контейнер конкретный класс как правило не видит - зависимости для него появляются автомагически (automagically), а service locator видит и даже может быть хранит на него ссылку, а если еще и сильную… Кроме того, при использовании контейнера вы можете например иметь циклическую ссылку (с учетом конечно всех требований ARC и weak), или выполнить что-то после того, как весь граф (не дерево! и это важно) зависимостей будет собран (см. IoCContainer.post)

TODO в swift в Xcode

 
 

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

// TODO: we need to convert to float math for better precision
let sum = 1 + 2
let avg = sum / 2

И хотелось бы, чтобы Xcode каким-либо образом это подсвечивал, например как это было в старом добром Objective-C

#warning we need to convert to float math for better precision
int sum = 1 + 2;
int avg = sum / 2;

Но в Xcode “из коробки” увы нет такой поддержки для Swift. Быть может она появится в будущем, сейчас же предлагаю применить черную магию и написать скрипт. Для этого идем в Build Phases и создаем “Run Script”, поместить его можно в конце. Содержание скрипта такое:

TAGS="TODO:|FIXME:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

Xcode после сборки начнет подсвечивать ваши TODO’шки так

highlight todos

Если вы хотите подсвечивать еще например // ERROR: как ошибки, можно скрипт сделать таким

TAGS="TODO:|FIXME:"
ERRORTAG="ERROR:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$|($ERRORTAG).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/" | perl -p -e "s/($ERRORTAG)/ error: \$1/"

Xcode сделает так

highlight errors

Придумал не сам, нашел тут и перевел.

Yandex Mobile Contest

   
   

import Foundation

let ymc = Event("Yandex Mobile Contest", at:"2016-10-17")

let attendee = AnyPerson()
if attendee.haveInterest(in: "Мобильная разработка") {
    attendee.takePart(in: ymc) && attendee.winPrize(of: ymc)
}

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