Cómo escribir en Objective-C en 2018. Parte 1

La mayoría de los proyectos de iOS cambian parcial o totalmente a Swift. Swift es un gran lenguaje, y con él yace el futuro del desarrollo de iOS. Pero el lenguaje está inextricablemente vinculado con el kit de herramientas, y hay inconvenientes en el kit de herramientas Swift.


El compilador Swift todavía tiene errores que hacen que se bloquee o genere código incorrecto. Swift no tiene un ABI estable. Y, lo que es más importante, los proyectos Swift han estado en curso durante demasiado tiempo.


En este sentido, los proyectos existentes pueden ser más rentables para continuar el desarrollo del Objetivo-C. ¡Y Objective-C no es lo que solía ser!


En esta serie de artículos, le mostraremos las características útiles y las mejoras de Objective-C, con las cuales se vuelve mucho más agradable escribir código. Todos los que escriban en Objective-C encontrarán algo interesante para ellos.



let y var


Objective-C ya no necesita especificar explícitamente tipos de variables: en Xcode 8, apareció la extensión de lenguaje __auto_type , y antes de que la inferencia de tipos Xcode 8 estuviera disponible en Objective-C ++ (usando la palabra clave auto con el advenimiento de C ++ 0X).


Primero, agreguemos las macros let y 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) { // ... }; 

Si solía escribir const después de un puntero a una clase Objective-C, era un lujo inadmisible, pero ahora la declaración implícita de const (vía let ) se ha dado por sentado. La diferencia es especialmente notable al guardar un bloque en una variable.


Para nosotros, desarrollamos una regla para usar let y var para declarar todas las variables. Incluso cuando una variable se inicializa a nil :


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

La única excepción es cuando necesita asegurarse de que a una variable se le asigne un valor en cada rama de código:


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

Solo de esta forma obtendremos una advertencia del compilador si olvidamos asignar un valor a una de las ramas.


Y finalmente: para usar let y var para variables de tipo id , necesita deshabilitar la advertencia auto-var-id (agregue -Wno-auto-var-id a las "Otras banderas de advertencia" en la configuración del proyecto).


Valor de retorno del tipo de bloqueo automático


Pocas personas saben que el compilador puede inferir el tipo del valor de retorno de un bloque:


 let block = ^{ return @"abc"; }; // `block`   `NSString *(^const)(void)` 

Es muy conveniente. Especialmente si escribe código reactivo usando ReactiveObjC . Pero hay una serie de restricciones bajo las cuales debe especificar explícitamente el tipo del valor de retorno.


  1. Si hay varias return en un bloque que devuelven valores de diferentes tipos.


     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. Si hay una return en el bloque que devuelve nil .


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

  3. Si el bloque debe devolver BOOL .


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


Las expresiones con un operador de comparación en C (y, por lo tanto, en Objective-C) son de tipo int . Por lo tanto, es mejor hacer una regla para especificar siempre explícitamente el tipo de retorno BOOL .


Genéricos y for...in


En Xcode 7, los genéricos (o más bien, genéricos ligeros) aparecieron en Objective-C. Esperamos que ya los uses. Pero si no, puede ver la sesión de WWDC o leerla aquí o aquí .


Para nosotros, hemos desarrollado una regla para especificar siempre parámetros genéricos, incluso si es id ( NSArray<id> * ). Por lo tanto, es fácil distinguir entre el código heredado en el que los parámetros genéricos aún no se han especificado.


Con las macros let y var , esperamos que podamos usarlas en un bucle for...in :


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

Pero ese código no se compila. Lo más probable es que __auto_type no sea compatible for...in , porque for...in solo funciona con colecciones que implementan el protocolo NSFastEnumeration . Y para los protocolos en Objective-C no hay soporte para genéricos.


Para solucionar este inconveniente, intente hacer su macro foreach . Lo primero que viene a la mente: todas las colecciones en Foundation tienen una propiedad objectEnumerator , y la macro podría verse así:


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

Pero para NSDictionary y NSMapTable método de protocolo NSMapTable NSFastEnumeration sobre las claves, no los valores (necesitaría usar keyEnumerator , no objectEnumerator ).


Tendremos que declarar una nueva propiedad que solo se usará para obtener el tipo en la expresión 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_)) 

Ahora nuestro código se ve mucho mejor:


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

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

Genéricos y copy / mutableCopy


Otro lugar donde la escritura no está disponible en Objective-C son los -mutableCopy y -mutableCopy (así como los -copyWithZone: y -mutableCopyWithZone: pero no los llamamos directamente).


Para evitar la necesidad de conversiones explícitas, puede volver a declarar métodos con el tipo de retorno. Por ejemplo, para NSArray declaraciones serían:


 @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


Como hemos vuelto a declarar los -mutableCopy y -mutableCopy , sería bueno garantizar que se utilizará el resultado de llamar a estos métodos. Para hacer esto, Clang tiene el atributo 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 

Para el siguiente código, el compilador generará una advertencia:


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

overloadable


Pocos saben que Clang le permite redefinir funciones en el lenguaje C (y, por lo tanto, en Objective-C). Usando el atributo overloadable , puede crear funciones con el mismo nombre, pero con diferentes tipos de argumentos o con diferentes números.


Las funciones reemplazables no pueden diferir solo en el tipo del valor de retorno.


 #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); 

Expresiones en recuadro


En 2012, en una sesión WWDC 413, Apple introdujo literales para NSNumber , NSArray y NSDictionary , así como expresiones en recuadro. Puede leer más sobre literales y expresiones en recuadros en la documentación de 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)] 

Usando literales y expresiones en recuadros, puede obtener fácilmente un objeto que representa un número o un valor booleano. Pero para obtener un objeto que envuelva la estructura, debe escribir un código:


 //  `NSDirectionalEdgeInsets`  `NSValue` let insets = (NSDirectionalEdgeInsets){ ... }; let value = [[NSValue alloc] initWithBytes:&insets objCType:@encode(typeof(insets))]; // ... //  `NSDirectionalEdgeInsets`  `NSValue` var insets = (NSDirectionalEdgeInsets){}; [value getValue:&insets]; 

Los métodos y propiedades auxiliares se definen para algunas clases (como el método +[NSValue valueWithCGPoint:] y CGPointValue propiedades CGPointValue ), ¡pero esto no es tan conveniente como la expresión en recuadro!


Y en 2015, Alex Denisov creó un parche para Clang, que le permite usar expresiones en recuadro para envolver cualquier estructura en NSValue .


Para que nuestra estructura admita expresiones en recuadro, solo necesita agregar el atributo objc_boxable para la estructura.


 #define JM_BOXABLE __attribute__((objc_boxable)) 

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

Y podemos usar la sintaxis @(...) para nuestra estructura:


 let dimension = (JMDimension){ ... }; let boxedValue = @(dimension); //   `NSValue *` 

Todavía tiene que -[NSValue getValue:] estructura a través del método -[NSValue getValue:] o el método de categoría.


CoreGraphics define su propia macro, CG_BOXABLE , y las expresiones en recuadro ya son compatibles con las CGPoint , CGSize , CGVector y CGRect .


Para otras estructuras de uso común, podemos agregar soporte para expresiones en recuadro por nuestra cuenta:


 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; 

Literales compuestos


Otra construcción de lenguaje útil es el compuesto literal . Los literales compuestos aparecieron en GCC como una extensión de lenguaje, y luego se agregaron al estándar C11.


Si antes, habiendo cumplido la llamada a UIEdgeInsetsMake , solo podríamos adivinar qué sangría obtendríamos (tuvimos que mirar la declaración de la función UIEdgeInsetsMake ), entonces con literales compuestos el código habla por sí mismo:


 //  UIEdgeInsetsMake(1, 2, 3, 4) //  (UIEdgeInsets){ .top = 1, .left = 2, .bottom = 3, .right = 4 } 

Es aún más conveniente usar una construcción de este tipo cuando algunos de los campos son cero:


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

Por supuesto, en literales compuestos puede usar no solo constantes, sino también cualquier expresión:


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

Fragmentos para 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#> } 

Nulabilidad


En Xcode 6.3.2, las anotaciones de nulabilidad aparecieron en Objective-C. Los desarrolladores de Apple los agregaron para importar la API Objective-C en Swift. Pero si se agrega algo al idioma, entonces debe intentar ponerlo a su servicio. Y explicaremos cómo usamos la nulabilidad en un proyecto de Objective-C y cuáles son las limitaciones.


Para actualizar su conocimiento, puede ver la sesión de WWDC .


Lo primero que hicimos fue comenzar a escribir las NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END en todos los archivos .m . Para no hacer esto a mano, aplicamos parches a las plantillas de archivos directamente en Xcode.


También comenzamos a establecer correctamente la nulabilidad para todas las propiedades y métodos privados.


Si agregamos las macros NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END a un archivo .m existente, agregaremos inmediatamente el null_resettable , null_resettable y _Nullable en todo el archivo.


Todas las advertencias útiles del compilador de nulabilidad están habilitadas de forma predeterminada. Pero hay una advertencia extrema que me gustaría incluir: -Wnullable-to-nonnull-conversion (establecido en "Otros indicadores de advertencia" en la configuración del proyecto). El compilador genera esta advertencia cuando una variable o expresión con un tipo anulable se convierte implícitamente en un tipo no nulo.


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

Desafortunadamente, para __auto_type (y por lo tanto let y var ), esta advertencia no funciona. El tipo deducido mediante __auto_type descarta la anotación de nulabilidad. Y, a juzgar por el comentario del desarrollador de Apple en rdar: // 27062504 , este comportamiento no cambiará. Se ha observado experimentalmente que agregar _Nullable o _Nonnull a __auto_type no afecta nada.


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

Para suprimir la nullable-to-nonnull-conversion escribimos una macro que "fuerza el desenvolvimiento". Idea tomada de la macro RBBNotNil . Pero debido al comportamiento de __auto_type fue posible deshacerse de la clase auxiliar.


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

Un ejemplo de uso de la macro 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 

Tenga en cuenta que en el momento de la escritura, la advertencia de nullable-to-nonnull-conversion nil no funciona nullable-to-nonnull-conversion : el compilador aún no comprende que una variable nullable después de verificar la desigualdad nil puede interpretarse como no 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 @""; } } 

En el código Objective-C ++, puede sortear esta limitación utilizando la construcción if let , ya que Objective-C ++ permite declaraciones variables en la expresión de una if .


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

Enlaces utiles


También hay una serie de macros y palabras clave más conocidas que me gustaría mencionar: la palabra clave @available , las macros NS_DESIGNATED_INITIALIZER , NS_UNAVAILABLE , NS_REQUIRES_SUPER , NS_NOESCAPE , NS_ENUM , NS_OPTIONS (o sus macros para los mismos atributos) y la biblioteca @keypath macro. También le recomendamos que mire el resto de la biblioteca libextobjc .



→ El código del artículo se publica en gist .


Conclusión


En la primera parte del artículo, tratamos de hablar sobre las características principales y las mejoras simples del lenguaje, que facilitan enormemente la escritura y el soporte del código Objective-C. En la siguiente parte, le mostraremos cómo puede aumentar aún más su productividad con la ayuda de enumeraciones como en Swift (son clases de casos; son tipos de datos algebraicos , ADT) y la posibilidad de implementar métodos a nivel de protocolo.

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


All Articles