Objects should have clearly defined tasks, such as modeling specific information/presenting visual content/controlling the flow of information, and so on.

As mentioned in previous articles, class interfaces define how objects interact with the outside world and help the outside world accomplish those tasks.

In some cases, we may want to add/extend behaviors for existing classes in specific situations. For example, we might often need to display a string on the user interface. Rather than creating an object that draws a string every time, it might be nice to provide the NSString class with the ability to draw itself onto the screen.

In this case, adding generic functionality to the interface of the existing class is not the best option. Drawing capabilities are not used most of the time for string objects, and in the case of the NSString class, because it is a framework class, we cannot modify its original interface and implementation.

Inheriting existing classes is not a good solution either — we might want to have this functionality not just on NSString classes, but on all subclasses of NSString classes (such as NSMutableString). And, while NSString is available on both OS X and iOS, the code drawn may differ on different platforms, requiring a different subclass for each platform.

Therefore, Objective-C provides categories and extensions that allow us to add our custom methods to existing classes.

Classification (Category)

If we need to add methods to an existing class to make it easier to add functionality to our application, the easiest way is to use categories.

The syntax for declaring a category is similar to the syntax for declaring a class interface, using the @interface keyword, but unlike a class interface, it does not specify inheritance. Instead, it wraps the category name in parentheses ().

@interface ClassName (CategoryName)
 
@end
Copy the code

We can declare a classification for any class, even if we don’t have the source code to implement it (such as Cocoa or Cocoa Touch classes). Any method we declare in a classification is available in the corresponding class and its subclasses. At run time, there is no difference between the methods implemented in the native class and the methods implemented in the classification.

Taking the XYZPerson class from the previous article as an example, we might frequently need to display a list of people:

Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
Copy the code

We can implement this requirement by categorizing:

#import "XYZPerson.h"
 
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end
Copy the code

In this case, the XYZPersonNameDisplayAdditions classification lastNameFirstNameString declare a return a string of new method

Typically the classification declaration is placed in a separate header file, and the implementation code is placed in a separate source file. In XYZPerson example, we may classify the above statement is placed in a XYZPerson + XYZPersonNameDisplayAddtions. H header file.

Although methods added from the classification are visible to all instances of it and subclass instances, we still need to import corresponding header files when using these methods, otherwise warnings and errors will occur at compile time.

The classification is implemented as follows:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
 
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end
Copy the code

Once we have declared and implemented the classification, we can use these methods in other classes:

#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
    XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
                                                    lastName:@"Doe"];
    XYZShoutingPerson *shoutingPerson =
                        [[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
                                                            lastName:@"Robinson"];
 
    NSLog(@"The two people are %@ and %@",
         [person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end
Copy the code

Classification not only gives us the ability to add methods to existing classes, we can also use classification to separate the implementation of a complex class into multiple files to manage. For example, when we want to customize a UI element, if the geometry calculation, color, gradient, etc., is particularly complex, we can separate the drawing related code and manage it in a separate category. We can also create different categories for different platforms (OS X/iOS) to provide different implementations.

Classes can be used to declare instance methods or class methods, but in most cases it is not appropriate to declare additional attributes. While it is syntactically possible to declare attributes in a classification declaration, it is not possible to declare additional instance variables in a classification. This means that the compiler does not automatically synthesize any instance variables for us, nor does it synthesize any accessors (setters/getters) for properties. We can implement getter/setter methods ourselves in our classification implementation, but we have no way to store instance variables corresponding to attributes unless they are already in the original class.

The only way to add attributes to an existing class and generate corresponding instance variables is to use class extensions, which I’ll explain later.

Note: Cocoa and Cocoa Touch contain a large number of categories added to existing classes in the framework.

In mentioned in this paper for nsstrings classes provide drawing function in the OS X are implemented through NSStringDrawing classification, in which contains drawAtPoint: withAttributes: and drawInRect: withAttributes: Methods. In iOS, it is achieved by UIStringDrawing classification, including drawAtPoint:withFont: and drawInRect:withFont: and other methods.

Avoid class method name conflicts

Because methods declared in a classification are added to existing classes, we need to be careful with method names.

If the method name declared in the class is the same as the method name in the original class, or the method name in another class (even the method name in the parent class), the behavior of calling the method at run time is uncertain. This is not likely to happen when we write categories for our own classes, but it can cause problems if we are adding categories for Cocoa or Cocoa Touch.

Suppose we’re writing an application that needs to interact with a remote Web service, we might want to Base64 encode strings frequently. So we might define a class for NSString, adding a method that returns a Base64 encoded string, so we might add a class method called base64EncodedString.

If we link to another framework that also happens to define a method called base64EncodedString, the problem arises: When the base64EncodedString method is called at run time, only one of the two methods is called, and it is uncertain which one will be called.

There is another situation that can cause problems: if we add a classification method to the Cocoa/Cocoa Touch class, then the Cocoa/Cocoa Touch class adds this method to the original class in a subsequent update. For example, the NSSortDescriptor class, which describes how collection objects should be sorted, has always had an initialization method of initWithKey: Ascending. However, early OS X and iOS versions did not provide corresponding class factory methods.

According to the habits, this class factory method should be called sortDescriptorWithKey: ascending:, so we might have added NSSortDescriptor classification, provide the factory method. That way, on earlier VERSIONS of OS X or iOS, the code worked as we expected, But after Mac OS X and iOS 4.0 10.6 NSSortDescriptor class is added sortDescriptorWithKey: ascending: method, at this point in our classification method and the method of the system class created a method name conflicts.

To avoid this problem, it is best to prefix method names when adding classes to the framework, just as we do when naming our own custom classes. We can use the same three letters as the class prefix, lowercase it and the name of the method itself to follow the method name naming rules. For the previous NSSortDescriptor, for example, we might define a category of the following form:

@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
Copy the code

This means that we can be confident that our code will be called at run time as expected. Since our code form is similar to the following, the ambiguity is removed:

NSSortDescriptor *descriptor =
               [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];
Copy the code

Class extensions (Extension)

Class extensions are somewhat similar to classifications, but we can only add class extensions at compile time to classes for which we already have source code (both classes and class extensions are compiled at compile time). Methods declared in class extensions are implemented in the @implementation block of the original class. Therefore, we cannot add class extensions to a class in the framework, such as classes in Cocoa/Cocoa Touch, such as NSString.

The syntax for declaring class extensions is similar to classification:

@interface ClassName ()
 
@end
Copy the code

Since there is no name in parentheses, class extensions are often referred to as anonymous categories

Unlike normal classification, class extension can add attributes and instance variables to a class. If we add an attribute to a class extension like this:

@interface XYZPerson ()
@property NSObject *extraProperty;
@end
Copy the code

The compiler automatically synthesizes the associated access methods (setters/getters) and instance variables in the implementation of the class.

If we add methods to a class extension, those methods must be implemented in the implementation of the class.

We can also add custom instance variables to the class extension {} :

@interface XYZPerson () { id _someCustomInstanceVariable; }... @endCopy the code

Use class extensions to hide private information

The main interface of a class defines how it interacts with the outside world, that is, it is the public interface of the class.

Class extensions often use additional private methods and attributes to better extend the public interface. For example, define an attribute as readonly in the class interface, but define it as readwrite in the class extension. In this way, the attribute value can be directly modified inside the class, and the write permission of the attribute can be controlled externally.

For example, the XYZPerson class might add an attribute called uniqueIdentifier to store the ID number.

The uniqueIdentifier is read-only, and the class interface provides a method for assigning a uniqueIdentifier.

@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
Copy the code

This means that the uniqueIdentifier cannot be set directly. If the uniqueIdentifier needs to be reassigned, the assignUniqueIdentifier method needs to be called.

To give XYZPerson the ability to modify the value of the attribute, we can add a class extension at the top of the implementation file to redefine the uniqueIdentifier attribute:

@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
 
@implementation XYZPerson
...
@end
Copy the code

Note: Readwrite Attribute is optional because it is the default attribute and can be explicitly declared when redefining an attribute for emphasis and clarification.

In this case, the compiler automatically synthesizes a setter method, so the implementation of the XYZPerson class can use setter methods or dot syntax directly to set property values.

Add the class extension to the implementation file of the XYZPerson class. The information in the class extension is private to the XYZPerson class. If other objects try to set properties, the compiler will generate an error.

Note: The above example redeclares the uniqueIdentifier and sets it to readWrite by adding a class instance. Whether or not other source files know it is redeclared in a class extension, its setter method: setUniqueIdentifier: method is present at run time for every object.

The compiler generates an error when trying to call a private method in another source file or when trying to assign a value to a readonly modified property, but you can circumvente the error by calling these methods dynamically at runtime, such as using the performSeletor:… We can circumvent class constraints in this way when necessary, but the common class interface should always correctly define external behavior.

If we want to make “private” methods or properties visible to individual classes, such as related classes in the framework. We can declare the class extension in a new header file and import the header file in the classes that need to use it. It also happens that the same class has two header files, such as xyzPerson.h and xyzPersonPrivate.h. But when we publish the framework, we should only open the xyzPerson.h header to the public.

Add functionality to existing classes in other ways

Classification and extension make it convenient to add functionality to existing classes, but sometimes they are not the best solution.

One of the main goals of object-oriented languages is to write reusable code, that is, to make classes reusable in as many situations as possible. If we are creating a view class to describe an object and display its information on the screen, we should consider whether the class can be reused in multiple situations.

One way to avoid hard-coding layout and UI content code is to use inheritance, where subclasses override methods and leave those decisions to subclasses. While this makes the class easy to reuse, we need to create a new subclass every time we want to use the class.

Another way is to create a proxy object for the class. All decisions that might limit reusability are handled by proxy objects, so decisions are deferred to runtime. A common example is TableView and TableView proxy. To improve the reusability of a TableView, the TableView offloads its content decisions to another object at runtime. This is covered in more detail in a later article, as you can refer to the official documentation: Working with Protocols.

Add directly from the Objective-C runtime

Objective-c provides dynamic behavior through its Objective-C runtime system.

Many decisions — such as which method to call after a message has been sent — are uncertain at compile time, but will be decided at run time when the program is running. Objective-c is not just a language compiled to machine code, it provides a runtime system to execute that machine code dynamically.

We can interact directly with the runtime system by, for example, adding an Associative reference to an object. Unlike class extensions, associated references do not affect the declaration or implementation of existing classes, which means that we can use them to modify source code that we do not have access to (such as classes in the framework).

Associative references associate one object with another, much like properties or instance variables. For more information about Associative References, please refer to the official document Associative References. For more information about the Runtime, please refer to the Objective-C Runtime Programming Guide.

practice

  1. forXYZPersonClass to add a category, add some extra methods, such as displaying names in different ways.
  2. forNSStringAdd a class to which a method is added to draw the all-caps string that the object represents at a certain point. You can do this by callingNSStringDrawingMethods in the classification to complete the actual drawing.
  3. As the originalXYZPersonClass add two read-only attributes representing the person’s height and weight, and addmeasureWeightandmeasureHeightMethod. Implement the above method to set the appropriate values for the properties by extending the class and redeclaring them as readable and writable.

Customizing Existing Classes