Pull to refresh
50.48
Joom
Международная группа компаний

Как писать на Objective-C в 2018 году. Часть 1

Reading time10 min
Views27K

Большинство iOS-проектов частично или полностью переходят на Swift. Swift — замечательный язык, и за ним будущее разработки под iOS. Но язык нераздельно связан с инструментарием, а в инструментарии Swift есть недостатки.


В компиляторе Swift по-прежнему находятся баги, которые приводят к его падению или генерации неправильного кода. У Swift нет стабильного ABI. И, что очень важно, проекты на Swift собираются слишком долго.


В связи с этим существующим проектам может быть выгоднее продолжать разработку на Objective-C. А Objective-C уже не тот, что был раньше!


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



let и var


В Objective-C больше не нужно явно указывать типы переменных: еще в Xcode 8 появилось расширение языка __auto_type, а до Xcode 8 выведение типов было доступно в Objective-C++ (при помощи ключевого слова auto с появлением C++0X).


Для начала добавим макросы let и var:


#define let __auto_type const
#define var __auto_type

// Было
NSArray<NSString *> *const items = [string componentsSeparatedByString:@","];

void(^const completion)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    // ...
};

// Стало
let items = [string componentsSeparatedByString:@","];

let completion = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    // ...
};

Если раньше писать const после указателя на Objective-C класс было непозволительной роскошью, то теперь неявное указание const (через let) стало само собой разумеющимся. Особенно заметна разница при сохранении блока в переменную.


Для себя мы выработали правило использовать let и var для объявления всех переменных. Даже когда переменная инициализируется значением nil:


- (nullable JMSomeResult *)doSomething {
    var result = (JMSomeResult *)nil;

    if (...) {
        result = ...;
    }

    return result;
}

Единственное исключение — когда надо гарантировать, что переменной присваивается значение в каждой ветке кода:


NSString *value;

if (...) {
    if (...) {
        value = ...;
    } else {
        value = ...;
    }
} else {
    value = ...;
}

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


И напоследок: чтобы использовать let и var для переменных типа id, нужно отключить предупреждение auto-var-id (добавить -Wno-auto-var-id в "Other Warning Flags" в настройках проекта).


Автовывод типа возвращаемого значения блока


Немногие знают, что компилятор умеет выводить тип возвращаемого значения блока:


let block = ^{
    return @"abc";
};

// `block` имеет тип `NSString *(^const)(void)`

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


  1. Если в блоке есть несколько операторов return, возвращающих значения разных типов.


    let block1 = ^NSUInteger(NSUInteger value){
        if (value > 0) {
            return value;
        } else {
            // `NSNotFound` имеет тип `NSInteger`
            return NSNotFound;
        }
    };
    
    let block2 = ^JMSomeBaseClass *(BOOL flag) {
        if (flag) {
            return [[JMSomeBaseClass alloc] init];
        } else {
            // `JMSomeDerivedClass` наследуется от `JMSomeBaseClass`
            return [[JMSomeDerivedClass alloc] init];
        }
    };

  2. Если в блоке есть оператор return, возвращающий nil.


    let block1 = ^NSString * _Nullable(){
        return nil;
    };
    
    let block2 = ^NSString * _Nullable(BOOL flag) {
        if (flag) {
            return @"abc";
        } else {
            return nil;
        }
    };

  3. Если блок должен возвращать BOOL.


    let predicate = ^BOOL(NSInteger lhs, NSInteger rhs){
        return lhs > rhs;
    };


Выражения с оператором сравнения в языке C (и, следовательно, в Objective-C) имеют тип int. Поэтому лучше взять за правило всегда явно указывать возвращаемый тип BOOL.


Generics и for...in


В Xcode 7 в Objective-C появились generics (точнее, lightweight generics). Надеемся, что вы их уже используете. Но если нет, то можно посмотреть сессию WWDC или прочитать здесь или здесь.


Мы для себя выработали правило всегда указывать generic-параметры, даже если это id (NSArray<id> *). Таким образом можно легко отличить legacy-код, в котором generic-параметры еще не указаны.


Имея макросы let и var, мы ожидаем, что сможем использовать их в цикле for...in:


let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"];

for (let item in items) {
    NSLog(@"%@", item);
}

Но такой код не скомпилируется. Скорее всего, __auto_type не стали поддерживать в for...in, потому что for...in работает только с коллекциями, реализующими протокол NSFastEnumeration. А для протоколов в Objective-C нет поддержки generics.


Чтобы исправить этот недостаток, попробуем сделать свой макрос foreach. Первое, что приходит в голову: у всех коллекций в Foundation есть свойство objectEnumerator, и макрос мог бы выглядеть так:


#define foreach(object_, collection_) \
    for (typeof([(collection_).objectEnumerator nextObject]) object_ in (collection_))

Но для NSDictionary и NSMapTable метод протокола NSFastEnumeration итерируется по ключам, а не по значениям (нужно было бы использовать keyEnumerator, а не objectEnumerator).


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


@interface NSArray<__covariant ObjectType> (ForeachSupport)

@property (nonatomic, strong, readonly) ObjectType jm_enumeratedType;

@end

@interface NSDictionary<__covariant KeyType, __covariant ObjectType> (ForeachSupport)

@property (nonatomic, strong, readonly) KeyType jm_enumeratedType;

@end

#define foreach(object_, collection_) \
    for (typeof((collection_).jm_enumeratedType) object_ in (collection_))

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


// Было
for (MyItemClass *item in items) {
    NSLog(@"%@", item);
}

// Стало
foreach (item, items) {
    NSLog(@"%@", item);
}

Сниппет для Xcode
foreach (<#object#>, <#collection#>) {
    <#statements#>
}

Generics и copy/mutableCopy


Еще одно место, где в Objective-C отсутствует типизация, — это методы -copy и -mutableCopy (а также методы -copyWithZone: и -mutableCopyWithZone:, но их мы не вызываем напрямую).


Чтобы избежать необходимости явного приведения типов, можно переобъявить методы с указанием возвращаемого типа. Например, для NSArray объявления будут такими:


@interface NSArray<__covariant ObjectType> (TypedCopying)

- (NSArray<ObjectType> *)copy;

- (NSMutableArray<ObjectType> *)mutableCopy;

@end

let items = [NSMutableArray<NSString *> array];
// ...

// Было
let itemsCopy = (NSArray<NSString *> *)[items copy];

// Стало
let itemsCopy = [items copy];

warn_unused_result


Раз уж мы переобъявили методы -copy и -mutableCopy, было бы неплохо гарантировать, что результат вызова этих методов будет использован. Для этого в Clang есть атрибут warn_unused_result.


#define JM_WARN_UNUSED_RESULT __attribute__((warn_unused_result))

@interface NSArray<__covariant ObjectType> (TypedCopying)

- (NSArray<ObjectType> *)copy JM_WARN_UNUSED_RESULT;

- (NSMutableArray<ObjectType> *)mutableCopy JM_WARN_UNUSED_RESULT;

@end

Для следующего кода компилятор сгенерирует предупреждение:


let items = @[@"a", @"b", @"c"];

[items mutableCopy]; // Warning: Ignoring return value of function declared with 'warn_unused_result' attribute.

overloadable


Немногие знают, что Clang позволяет переопределять функции в языке C (а следовательно, и в Objective-C). C помощью атрибута overloadable можно создавать функции с одинаковым названием, но с разными типами аргументов или с их разным количеством.


Переопределяемые функции не могут отличаться только лишь типом возвращаемого значения.


#define JM_OVERLOADABLE __attribute__((overloadable))

JM_OVERLOADABLE float JMCompare(float lhs, float rhs);

JM_OVERLOADABLE float JMCompare(float lhs, float rhs, float accuracy);

JM_OVERLOADABLE double JMCompare(double lhs, double rhs);

JM_OVERLOADABLE double JMCompare(double lhs, double rhs, double accuracy);

Boxed expressions


В далеком 2012 году в сессии WWDC 413 Apple представила литералы для NSNumber, NSArray и NSDictionary, а также boxed expressions. Подробно о литералах и boxed expressions можно прочитать в документации Clang.


// Литералы
@YES                      // [NSNumber numberWithBool:YES]
@NO                       // [NSNumber numberWithBool:NO]
@123                      // [NSNumber numberWithInt:123]
@3.14                     // [NSNumber numberWithDouble:3.14]
@[obj1, obj2]             // [NSArray arrayWithObjects:obj1, obj2, nil]
@{key1: obj1, key2: obj2} // [NSDictionary dictionaryWithObjectsAndKeys:obj1, key1, obj2, key2, nil]

// Boxed expressions
@(boolVariable)           // [NSNumber numberWithBool:boolVariable]
@(intVariable)            // [NSNumber numberWithInt:intVariable)]

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


// Оборачивание `NSDirectionalEdgeInsets` в `NSValue`
let insets = (NSDirectionalEdgeInsets){ ... };
let value = [[NSValue alloc] initWithBytes:&insets objCType:@encode(typeof(insets))];

// ...

// Получение `NSDirectionalEdgeInsets` из `NSValue`
var insets = (NSDirectionalEdgeInsets){};
[value getValue:&insets];

Для некоторых классов определены вспомогательные методы и свойства (наподобие метода +[NSValue valueWithCGPoint:] и свойства CGPointValue), но это все равно не так удобно, как boxed expression!


И в 2015 году Алекс Денисов сделал патч для Clang, позволяющий использовать boxed expressions для оборачивания любых структур в NSValue.


Чтобы наша структура поддерживала boxed expressions, нужно просто добавить атрибут objc_boxable для структуры.


#define JM_BOXABLE __attribute__((objc_boxable))

typedef struct JM_BOXABLE JMDimension {
    JMDimensionUnit unit;
    CGFloat value;
} JMDimension;

И мы можем использовать синтаксис @(...) для нашей структуры:


let dimension = (JMDimension){ ... };

let boxedValue = @(dimension); // Имеет тип `NSValue *`

Получать структуру обратно по-прежнему придется через метод -[NSValue getValue:] или метод категории.


В CoreGraphics определен свой макрос CG_BOXABLE, и boxed expressions уже поддержаны для структур CGPoint, CGSize, CGVector и CGRect.


Для остальных часто используемых структур мы можем добавить поддержку boxed expressions самостоятельно:


typedef struct JM_BOXABLE _NSRange NSRange;
typedef struct JM_BOXABLE CGAffineTransform CGAffineTransform;
typedef struct JM_BOXABLE UIEdgeInsets UIEdgeInsets;
typedef struct JM_BOXABLE NSDirectionalEdgeInsets NSDirectionalEdgeInsets;
typedef struct JM_BOXABLE UIOffset UIOffset;
typedef struct JM_BOXABLE CATransform3D CATransform3D;

Compound literals


Еще одна полезная конструкция языка — compound literal. Compound literals появились еще в GCC в виде расширения языка, а позже были добавлены в стандарт C11.


Если раньше, встретив вызов UIEdgeInsetsMake, мы могли только гадать, какие отступы мы получим (надо было смотреть объявление функции UIEdgeInsetsMake), то с compound literals код говорит сам за себя:


// Было
UIEdgeInsetsMake(1, 2, 3, 4)
// Стало
(UIEdgeInsets){ .top = 1, .left = 2, .bottom = 3, .right = 4 }

Еще удобнее использовать такую конструкцию, когда часть полей равны нулю:


(CGPoint){ .y = 10 }
// вместо
(CGPoint){ .x = 0, .y = 10 }

(CGRect){ .size = { .width = 10, .height = 20 } }
// вместо
(CGRect){ .origin = { .x = 0, .y = 0 }, .size = { .width = 10, .height = 20 } }

(UIEdgeInsets){ .top = 10, .bottom = 20 }
// вместо
(UIEdgeInsets){ .top = 20, .left = 0, .bottom = 10, .right = 0 }

Конечно, в compound literals можно использовать не только константы, но и любые выражения:


textFrame = (CGRect){
    .origin = {
        .y = CGRectGetMaxY(buttonFrame) + textMarginTop
    },
    .size = textSize
};

Сниппеты для Xcode
(NSRange){ .location = <#location#>, .length = <#length#> }

(CGPoint){ .x = <#x#>, .y = <#y#> }

(CGSize){ .width = <#width#>, .height = <#height#> }

(CGRect){
    .origin = {
        .x = <#x#>,
        .y = <#y#>
    },
    .size = {
        .width = <#width#>,
        .height = <#height#>
    }
}

(UIEdgeInsets){ .top = <#top#>, .left = <#left#>, .bottom = <#bottom#>, .right = <#right#> }

(NSDirectionalEdgeInsets){ .top = <#top#>, .leading = <#leading#>, .bottom = <#bottom#>, .trailing = <#trailing#> }

(UIOffset){ .horizontal = <#horizontal#>, .vertical = <#vertical#> }

Nullability


В Xcode 6.3.2 в Objective-C появились nullability-аннотации. Разработчики Apple добавили их для импортирования Objective-C API в Swift. Но если что-то добавлено в язык, то надо постараться поставить это себе на службу. И мы расскажем, как используем nullability в Objective-C проекте и какие есть ограничения.


Чтобы освежить знания, можно посмотреть сессию WWDC.


Первое, что мы сделали, — это начали писать макросы NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END во всех .m-файлах. Чтобы не делать этого руками, мы патчим шаблоны файлов прямо в Xcode.


Мы стали также правильно расставлять nullability для всех приватных свойств и методов.


Если мы добавляем макросы NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END в уже существующий .m-файл, то сразу дописываем недостающие nullable, null_resettable и _Nullable во всем файле.


Все полезные предупреждения компилятора, связанные с nullability, включены по умолчанию. Но есть одно экстремальное предупреждение, которое хотелось бы включить: -Wnullable-to-nonnull-conversion (задается в "Other Warning Flags" в настройках проекта). Компилятор выдает это предупреждение, когда переменная или выражение с nullable-типом неявно приводится к nonnull-типу.


+ (NSString *)foo:(nullable NSString *)string {
    return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull'
}

К сожалению, для __auto_type (а следовательно, и let и var) это предупреждение не срабатывает. В типе, выведенном через __auto_type, отбрасывается nullability-аннотация. И, судя по комментарию разработчика Apple в rdar://27062504, это поведение уже не изменится. Экспериментально замечено, что добавление _Nullable или _Nonnull к __auto_type ни на что не влияет.


- (NSString *)test:(nullable NSString *)string {
    let tmp = string;
    return tmp; // Нет предупреждения
}

Для подавления предупреждения nullable-to-nonnull-conversion мы написали макрос, который делает "force unwrap". Идея взята из макроса RBBNotNil. Но за счет поведения __auto_type удалось избавиться от вспомогательного класса.


#define JMNonnull(obj_) \
    ({ \
        NSCAssert(obj_, @"Expected `%@` not to be nil.", @#obj_); \
        (typeof({ __auto_type result_ = (obj_); result_; }))(obj_); \
    })

Пример использования макроса JMNonnull:


@interface JMRobot : NSObject

@property (nonatomic, strong, nullable) JMLeg *leftLeg;
@property (nonatomic, strong, nullable) JMLeg *rightLeg;

@end

@implementation JMRobot

- (void)stepLeft {
    [self step:JMNonnull(self.leftLeg)]
}

- (void)stepRight {
    [self step:JMNonnull(self.rightLeg)]
}

- (void)step:(JMLeg *)leg {
    // ...
}

@end

Отметим, что на момент написания статьи предупреждение nullable-to-nonnull-conversion работает неидеально: компилятор пока не понимает, что nullable-переменную после проверки на неравенство nil можно воспринимать как nonnull.


- (NSString *)foo:(nullable NSString *)string {
    if (string != nil) {
        return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull'
    } else {
        return @"";
    }
}

В Objective-C++ коде можно обойти это ограничение, использовав конструкцию if let, поскольку Objective-C++ допускает объявление переменных в выражении оператора if.


- (NSString *)foo:(nullable NSString *)stringOrNil {
    if (let string = stringOrNil) {
        return string;
    } else {
        return @"";
    }
}

Полезные ссылки


Есть также ряд более известных макросов и ключевых слов, которые хотелось бы упомянуть: ключевое слово @available, макросы NS_DESIGNATED_INITIALIZER, NS_UNAVAILABLE, NS_REQUIRES_SUPER, NS_NOESCAPE, NS_ENUM, NS_OPTIONS (или свои макросы для тех же атрибутов) и макрос @keypath из библиотеки libextobjc. Советуем также посмотреть остальные возможности библиотеки libextobjc.



→ Код для статьи выложен в gist.


Заключение


В первой части статьи мы постарались рассказать об основных возможностях и простых улучшениях языка, которые существенно облегчают написание и поддержку Objective-C кода. В следующей части мы покажем, как можно еще увеличить свою продуктивность с помощью enum'ов как в Swift (они же Case-классы; они же Алгебраические типы данных, ADT) и возможности реализации методов на уровне протокола.

Tags:
Hubs:
Total votes 37: ↑34 and ↓3+31
Comments49

Articles

Information

Website
www.joom-group.com
Registered
Founded
Employees
501–1,000 employees
Location
Латвия