How to write on Objective-C in 2018. Part 1

    Most iOS-projects partially or fully switch to Swift. Swift is a great language, and the future of iOS development is up to it. But the language is inseparably connected with the toolkit, and there are drawbacks in the Swift toolkit.


    In the Swift compiler, there are still bugs that lead to its fall or the generation of incorrect code. Swift does not have a stable ABI. And, very importantly, Swift projects are going too long.


    In this regard, existing projects may be more profitable to continue development on Objective-C. And Objective-C is not what it used to be!


    In this series of articles, we show the useful features and improvements of Objective-C, with which writing code becomes much more pleasant. Anyone who writes in Objective-C will find something interesting for themselves.



    let and var


    In Objective-C, you no longer need to explicitly specify the types of variables: even in Xcode 8, an extension of the language appeared __auto_type, and before Xcode 8, type inference was available in Objective-C ++ (using a keyword autowith the appearance of C ++ 0X).


    First, add macros letand 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) {
        // ...
    };

    If previously writing constafter a pointer to an Objective-C class was an unaffordable luxury, now the implicit indication const(through let) has become a matter of course. The difference is especially noticeable when saving a block to a variable.


    For ourselves, we have developed a rule to use let, and varto declare all variables. Even when the variable is initialized with the value nil:


    - (nullable JMSomeResult *)doSomething {
        var result = (JMSomeResult *)nil;
        if (...) {
            result = ...;
        }
        return result;
    }

    The only exception is when it is necessary to ensure that the variable is assigned a value in each branch of the code:


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

    Only in this way will we get a compiler warning if we forget to assign a value to one of the branches.


    And finally: to use letand varfor variable type id, you need to disable the warning auto-var-id(add -Wno-auto-var-idin "Other Warning Flags" in the project settings).


    Auto output of block return type


    Few people know that the compiler can output the return type of the block:


    let block = ^{
        return@"abc";
    };
    // `block` имеет тип `NSString *(^const)(void)`

    It is very convenient. Especially if you write a "reactive" code using ReactiveObjC . But there are a number of restrictions under which you need to explicitly specify the type of the return value.


    1. If there are several statements in the block returnthat return values ​​of different types.


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

    2. If there is an operator in the block returnthat returns nil.


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

    3. If the block should return BOOL.


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


    Expressions with a comparison operator in the C language (and, therefore, in Objective-C) are of type int. Therefore, it is better to take the rule to always explicitly specify the return type BOOL.


    Generics and for...in


    In Xcode 7, generics (more precisely, lightweight generics) appeared in Objective-C. We hope you already use them. But if not, then you can watch the WWDC session or read here or here .


    We have developed a rule for ourselves to always specify generic parameters, even if it is id( NSArray<id> *). This way you can easily distinguish the legacy code in which the generic parameters are not yet specified.


    Having macros letand varwe expect to be able to use them in a loop for...in:


    let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"];
    for (let item in items) {
        NSLog(@"%@", item);
    }

    But such code will not compile. Most likely, __auto_typethey did not support you in for...in, because it for...inworks only with collections that implement the protocol NSFastEnumeration. And for protocols in Objective-C, there is no generics support.


    To correct this deficiency, try to make your own macro foreach. The first thing that comes to mind: all collections in Foundation have a property objectEnumerator, and a macro could look like this:


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

    But for NSDictionaryand NSMapTablethe protocol method is NSFastEnumerationiterated by keys, not by values ​​(it would be necessary to use keyEnumerator, not objectEnumerator).


    We will need to declare a new property that will be used only to get the type in the expression typeof:


    @interfaceNSArray<__covariantObjectType> (ForeachSupport)@property (nonatomic, strong, readonly) ObjectType jm_enumeratedType;
    @end@interfaceNSDictionary<__covariantKeyType, __covariantObjectType> (ForeachSupport)@property (nonatomic, strong, readonly) KeyType jm_enumeratedType;
    @end#define foreach(object_, collection_) \for (typeof((collection_).jm_enumeratedType) object_ in (collection_))

    Now our code looks much better:


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

    Xcode Snippet
    foreach (<#object#>, <#collection#>) {
        <#statements#>
    }

    Generics and copy/mutableCopy


    Another place where there is no typing in Objective-C is methods -copyand -mutableCopy(as well as methods -copyWithZone:and -mutableCopyWithZone:, but we do not call them directly).


    To avoid the need for explicit type casting, you can redeclare methods with the return type. For example, for an NSArrayad would be:


    @interfaceNSArray<__covariantObjectType> (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


    Since we have redeclared the methods -copyand -mutableCopyit would be nice to ensure that the result of calling these methods will be used. There is an attribute in Clang for this warn_unused_result.


    #define JM_WARN_UNUSED_RESULT __attribute__((warn_unused_result))

    @interfaceNSArray<__covariantObjectType> (TypedCopying)
    - (NSArray<ObjectType> *)copy JM_WARN_UNUSED_RESULT;
    - (NSMutableArray<ObjectType> *)mutableCopy JM_WARN_UNUSED_RESULT;
    @end

    For the following code, the compiler will generate a warning:


    let items = @[@"a", @"b", @"c"];
    [items mutableCopy]; // Warning: Ignoring return value of function declared with 'warn_unused_result' attribute.

    overloadable


    Few know that Clang allows you to redefine functions in the C language (and, therefore, in Objective-C). C using the attribute, overloadableyou can create functions with the same name, but with different types of arguments or with different numbers.


    Overridden functions cannot differ only by the type of return value.


    #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


    Back in 2012, Apple introduced literals for , and as well as boxed expressions in the WWDC 413 session . Details on the literals and boxed expressions can be found in the Clang documentation .NSNumberNSArrayNSDictionary


    // Литералы
    @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)]

    With the help of literals and boxed expressions, you can easily get an object representing a number or a boolean value. But to get an object that wraps the structure, you need to write some code:


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

    For some classes, helper methods and properties are defined (like the method +[NSValue valueWithCGPoint:]and properties CGPointValue), but this is still not as convenient as the boxed expression!


    And in 2015, Alex Denisov made a patch for Clang, allowing the use of boxed expressions to wrap any structures in NSValue.


    For our structure to support boxed expressions, you just need to add an attribute objc_boxableto the structure.


    #define JM_BOXABLE __attribute__((objc_boxable))

    typedefstruct JM_BOXABLE JMDimension {
        JMDimensionUnit unit;
        CGFloat value;
    } JMDimension;

    And we can use the syntax @(...)for our structure:


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

    You still have to get the structure back through -[NSValue getValue:]the category method or method.


    In CoreGraphics defined your macro CG_BOXABLE, and boxed expressions are already supported for structures CGPoint, CGSize, CGVectorand CGRect.


    For the rest of the frequently used structures, we can add support for boxed expressions on our own:


    typedefstruct JM_BOXABLE _NSRange NSRange;
    typedefstruct JM_BOXABLE CGAffineTransformCGAffineTransform;
    typedefstruct JM_BOXABLE UIEdgeInsetsUIEdgeInsets;
    typedefstruct JM_BOXABLE NSDirectionalEdgeInsetsNSDirectionalEdgeInsets;
    typedefstruct JM_BOXABLE UIOffsetUIOffset;
    typedefstruct JM_BOXABLE CATransform3DCATransform3D;

    Compound literals


    Another useful language construct is compound literal . Compound literals appeared in GCC as a language extension, and were later added to the C11 standard.


    If before, having met the call UIEdgeInsetsMake, we could only guess what we would get indents (we had to watch the function declaration UIEdgeInsetsMake), then with compound literals the code speaks for itself:


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

    It is even more convenient to use such a construction, when part of the fields are zero:


    (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 }

    Of course, in compound literals you can use not only constants, but also any expressions:


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

    Xcode Snippets
    (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


    In Xcode 6.3.2, nullability-annotations appeared in Objective-C . Apple developers added them to import the Objective-C API into Swift. But if something is added to the language, then you should try to put it in your service. And we will tell you how to use nullability in an Objective-C project and what are the limitations.


    To refresh knowledge, you can watch the WWDC session .


    The first thing we did was start writing macros NS_ASSUME_NONNULL_BEGIN/ NS_ASSUME_NONNULL_ENDin all the .mfiles. In order not to do this by hand, we patch file templates right in Xcode.


    We also began to properly arrange the nullability for all private properties and methods.


    If we add macros NS_ASSUME_NONNULL_BEGIN/ NS_ASSUME_NONNULL_ENDto an existing .mfile, we immediately add the missing ones nullable, null_resettableand _Nullablein the whole file.


    All useful compiler warnings related to nullability are enabled by default. But there is one extreme warning that I would like to include: -Wnullable-to-nonnull-conversion(set in "Other Warning Flags" in the project settings). The compiler issues this warning when a variable or expression with a nullable type is implicitly cast to a nonnull type.


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

    Unfortunately for __auto_type(and therefore letalso var) this warning does not work. In the type inferred through __auto_type, the nullability annotation is discarded. And, judging by the comment of the Apple developer in rdar: // 27062504 , this behavior will not change. Experimentally observed that the addition of _Nullableor _Nonnullto __auto_typeany not affect anything.


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

    To suppress the warning, nullable-to-nonnull-conversionwe wrote a macro that makes "force unwrap". The idea is taken from the macro RBBNotNil. But due to the behavior __auto_typemanaged to get rid of the auxiliary class.


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

    Macro usage example JMNonnull:


    @interfaceJMRobot : NSObject@property (nonatomic, strong, nullable) JMLeg *leftLeg;
    @property (nonatomic, strong, nullable) JMLeg *rightLeg;
    @end@implementationJMRobot
    - (void)stepLeft {
        [self step:JMNonnull(self.leftLeg)]
    }
    - (void)stepRight {
        [self step:JMNonnull(self.rightLeg)]
    }
    - (void)step:(JMLeg *)leg {
        // ...
    }
    @end

    Note that at the time of this writing, the warning nullable-to-nonnull-conversionworks imperfectly: the compiler does not yet understand that the nullablevariable after checking for inequality nilcan be interpreted as nonnull.


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

    In Objective-C ++ code, this restriction can be circumvented by using the construct if let, since Objective-C ++ allows variable declarations in operator statements if.


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

    useful links


    There are also a number of more well-known macros and key words that I would like to mention: the keyword @available, macro NS_DESIGNATED_INITIALIZER, NS_UNAVAILABLE, NS_REQUIRES_SUPER, NS_NOESCAPE, NS_ENUM, NS_OPTIONS(or your own macros for the same attributes) and the macro @keypathof libextobjc library. We also recommend that you look at the rest of the libextobjc library .



    → The code for the article is laid out in gist .


    Conclusion


    In the first part of the article, we tried to talk about the main features and simple language enhancements that make it much easier to write and maintain Objective-C code. In the next section, we will show how you can further increase your productivity with the help of enums as in Swift (they are also Case classes; they are Algebraic data types , ADT) and the possibility of implementing methods at the protocol level.


    Also popular now: