📜 ⬆️ ⬇️

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 cause it to crash or generate 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 the same as it was before!


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, the language extension __auto_type , and before Xcode 8, type inference was available in Objective-C ++ (using the auto keyword with the appearance of C ++ 0X).


To begin with, add the let and var macros:


 #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 earlier to write const after a pointer to an Objective-C class was an impermissible luxury, now the implicit indication of 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 var to declare all variables. Even when the variable is initialized to nil :


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

The only exception is when it is necessary to ensure that a 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 let and var for variables of type id , you need to turn off the warning auto-var-id (add -Wno-auto-var-id to "Other Warning Flags" in the project settings).


Auto withdrawal 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 return in the block that return values ​​of different types.


     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. If there is a return in the block, it returns nil .


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

  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 follow the rule of always explicitly specifying 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 have not yet been specified.


Having the let and var macros, we expect to be able to use them in the for...in loop:


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

But such code will not compile. Most likely, __auto_type not supported in for...in , because for...in works only with collections that implement the NSFastEnumeration protocol. And for protocols in Objective-C, there is no generics support.


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


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

But for NSDictionary and NSMapTable protocol NSFastEnumeration iterated by keys, not by values ​​(you would use keyEnumerator , not objectEnumerator ).


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


 @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_)) 

Now our code looks much better:


 //  for (MyItemClass *item in items) { NSLog(@"%@", item); } //  foreach (item, items) { NSLog(@"%@", item); } 

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

Generics and copy / mutableCopy


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


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


 @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


Since we have redeclared the methods -copy and -mutableCopy , it would be nice to guarantee that the result of calling these methods will be used. To do this, the Clang attribute is 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 

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). Using the overloadable attribute, you 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 NSNumber , NSArray and NSDictionary , as well as boxed expressions, in the WWDC 413 session . Details on the literals and boxed expressions can be found in the Clang documentation .


 //  @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 wrapping a 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 +[NSValue valueWithCGPoint:] method and CGPointValue properties), but this is still not as convenient as the boxed expression!


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


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


 #define JM_BOXABLE __attribute__((objc_boxable)) 

 typedef struct 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 -[NSValue getValue:] structure back through the -[NSValue getValue:] method or the category method.


CoreGraphics defines its own CG_BOXABLE macro, and boxed expressions are already supported for CGPoint , CGSize , CGVector and CGRect .


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


 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


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 earlier, having met the UIEdgeInsetsMake call, we could only guess what we would get indents (we had to watch the declaration of the UIEdgeInsetsMake function), 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 your knowledge, you can watch the WWDC session .


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


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


If we add the NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END to an existing .m file, we immediately add the missing nullable , null_resettable and _Nullable in the entire file.


All useful nullability compiler warnings 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:(nullable NSString *)string { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } 

Unfortunately, for __auto_type (and therefore let and 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. It is experimentally observed that adding _Nullable or _Nonnull to __auto_type does not affect anything.


 - (NSString *)test:(nullable NSString *)string { let tmp = string; return tmp; //   } 

To suppress the nullable-to-nonnull-conversion we wrote a macro that makes "force unwrap". The idea is taken from the macro RBBNotNil . But due to the behavior of __auto_type managed 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_); \ }) 

An example of using the JMNonnull macro:


 @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 

Note that at the time of this writing, the warning nullable-to-nonnull-conversion works imperfectly: the compiler does not yet understand that a nullable variable can be regarded as nonnull after checking for nil inequality.


 - (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 @""; } } 

In Objective-C ++ code, you can get around this restriction by using the if let construct, since Objective-C ++ allows variable declarations in an if .


 - (NSString *)foo:(nullable NSString *)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 , macros NS_DESIGNATED_INITIALIZER , NS_UNAVAILABLE , NS_REQUIRES_SUPER , NS_NOESCAPE , NS_ENUM , NS_OPTIONS (or your own macros for the same attributes) and macro @keypath of 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 support Objective-C code. In the next part, 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.


')

Source: https://habr.com/ru/post/431236/


All Articles