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 auto
with the appearance of C ++ 0X).
First, add macros let
and 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 const
after 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 var
to 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 let
and var
for variable type id
, you need to disable the warning auto-var-id
(add -Wno-auto-var-id
in "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.
If there are several statements in the block
return
that 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]; } };
If there is an operator in the block
return
that returnsnil
.let block1 = ^NSString * _Nullable(){ returnnil; }; let block2 = ^NSString * _Nullable(BOOL flag) { if (flag) { return@"abc"; } else { returnnil; } };
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 let
and var
we 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_type
they did not support you in for...in
, because it for...in
works 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 NSDictionary
and NSMapTable
the protocol method is NSFastEnumeration
iterated 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);
}
foreach (<#object#>, <#collection#>) {
<#statements#>
}
Generics and copy
/mutableCopy
Another place where there is no typing in Objective-C is methods -copy
and -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 NSArray
ad 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 -copy
and -mutableCopy
it 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, overloadable
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 , 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 .NSNumber
NSArray
NSDictionary
// Литералы
@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_boxable
to 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
, CGVector
and 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
};
(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_END
in all the .m
files. 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_END
to an existing .m
file, we immediately add the missing ones nullable
, null_resettable
and _Nullable
in 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 let
also 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 _Nullable
or _Nonnull
to __auto_type
any not affect anything.
- (NSString *)test:(nullableNSString *)string {
let tmp = string;
return tmp; // Нет предупреждения
}
To suppress the warning, 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 __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_); \
})
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-conversion
works imperfectly: the compiler does not yet understand that the nullable
variable after checking for inequality nil
can 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 @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 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.