If you have read Apple's documentation on Generics and were left wondering how you could use this technology in your own projects, this post is for you! You will learn how to take full advantage of generics in your every-day code as well as how to avoid the constant type-casting that usually results from creating generalized code.
The Problem with Generalized Code
Every good app developer needs a base of generic code they can use from the iOS projects they create. For example, in this blog series I have used the mmBusinessObject class in several projects. This class encapsulates Core Data, and using it in your projects helps you avoid repetitive code.
However, one of the downsides of using generalized code is that you often have to downcast from general types to more specific types many times in your project.
Let's look at a sample that demonstrates this problem. You can download the sample project for this post at this link.
- Open the GenericsDemo project in Xcode.
- The Project Navigator contains most of the classes shown in Figure 1 class diagram.
Figure 1 - Business object class diagram |
On the left side of the diagram are the business classes. Person and Company are subclasses of ABusinessObject, which is in turn a subclass of mmBusinessObject.
On the right side of the diagram are the entity classes. PersonEntity and CompanyEntity are subclasses of NSManagedObject.
The mmBusinessObject class contains methods that create and manipulate entities. Since this is a custom framework-level class, its methods return instances of NSManagedObject. This allows you to use the mmBusinessObject class from any iOS project.
Your project-specific business classes such as Person and Company, inherit these methods which return NSManagedObject instances. However, they need to downcast the generalized NSManagedObject into PersonEntity and CompanyEntity objects, to access entity-specific properties such as firstName, lastName, and personID (Figure 1).
Let's take a closer look at this in code so you fully understand the relationship between these classes and the problem with generalized code.
- Select the mmBusinessObject.swift file in the Project Navigator. Near the top of the file is the createEntity method, which returns an NSManagedObject:
Below this method is the getAllEntities method, which returns an array of NSManagedObjects:
- The Person and Company classes inherit these methods and therefore return the same types as mmBusinessObject. To verify this, select the ViewController.swift file in the Project Navigator. Check out the code in the viewDidLoad method:
This code creates a Person object, then calls its getAllEntities and createEntity methods. The second block of code creates a Company object and calls the same methods on that object.
- Click on the personList variable and then go to the Quick Help Inspector on the right side of the Xcode window (Figure 2).
Figure 2 - personList holds an array of NSManagedObjects. |
It indicates the variable's type is an array of NSManagedObjects. This is determined by Swift's type inference, based on the getAllEntities method's return type.
- Click on the personEntity variable. The Quick Help Inspector indicates it is of type NSManagedObject, based on the createEntity method's return type (Figure 3).
Figure 3 - personEntity is of type NSManagedObject. |
- Click on the companyList variable and you can see it holds an array of NSManagedObjects. Then click on the companyEntity variable and you can see it is of the type NSManagedObject. That's because the getAllEntities and createEntity methods are inherited by both the Person and Company classes, and both methods return objects of type NSManagedObject.
- Let's try to access the properties of the PersonEntity object. Below the code that creates a PersonEntity object, add the last line of code in Figure 4, which tries to access the firstName property.
Figure 4 - Try to access the PersonEntity object's firstName property. |
The firstName property doesn't appear in the Code Completion list because the personEntity variable is of type NSManagedObject. You have to downcast to PersonEntity to see this property.
- Delete the partial code you just entered, and let's add some code to perform the downcast.
A Non-Generic Solution
Let's look at one option of downcasting the return values of these inherited methods without generics.
- In the viewDidLoad method, you could change the code as follows (but don't):
This solution works. The return value of each method is downcast to the correct type of entity. However, this is a real pain in the neck. Every time you call a business object method (and there are many more methods than I am showing here), you have to write code that performs a downcast.
It's much less work to have each business object return the downcast type instead. To do this without generics, you add methods to the Person and classes that override existing methods or add completely new methods to return the desired types. For example:
This is a more acceptable solution. It accomplishes the goal of business objects returning PersonEntity and CompanyEntity objects. However, when you consider all the methods in the mmBusinessObject class (about 20 in the full version) and multiply that times the number of business objects in each project (20-30 in a large project), you quickly find yourself overriding several hundred methods. There's got to be a better way!
Enter generic methods.
The Generic Method Solution
There are a few ways you can incorporate generics in your projects. Let's start with a minimalist approach and create a generic method.
- Since we are going to change the mmBusinessObject class to implement generics, let's leave the current class intact, and edit mmBusinessObjectGeneric, which is a copy of the class with a different name.
- Select the ABusinessObject.swift file in the Project Navigator. Remember, this class is the superclass of both the Person and Company classes (Figure 1), so changing its superclass also changes the heritage of the these other classes. Near the top of the class, change the superclass to mmBusinessObjectGeneric:
- Select the mmBusinessObjectGeneric.swift file in the Project Navigator.
- Near the top of the code file, locate the createEntity method (Figure 5).
Figure 5 - The createEntity method in its "specific" form |
This method is not generic. It returns a specific value of type NSManagedObject. When we convert it to a generic method, any code that calls this method can specify the type of the return value.
- Let's take the first step in making this method generic. Between the name of the method and the parentheses, add <T>:
This is a type parameter. It declares the letter T as a placeholder for a type within this method. This is similar in concept as using n as a placeholder in an equation. The type parameter is replaced by an actual type when the method is called.
There is nothing magical about the letter T. You can specify a different letter or set of letters as the placeholder, but it's conventional to use the letter T, which stands for "Type".
When you declare a type parameter for a method, it can be used to specify:
- The method's parameter types
- The method's return type
- Types in the body of the method
Let's make use of this method's type parameters now.
- Replace the createEntity method's NSManagedObject return type with the T placeholder:
- Next, change the as NSManagedObject operation at the bottom of the method to as T:
Figure 6 shows the three places you have used the type parameter. Now you're ready to call this generic method to see how it works at run time.
Figure 6 - createEntity as a generic method |
Testing the Generic Method
- Select ViewController.swift in the Project Navigator.
- Press Command+B to build the project. This displays a Cannot convert the expression's type '()' to type 'T' error for these lines of code:
The compiler is complaining that you have not supplied enough information to determine the type it should substitute for the T placeholder in the createEntity method.
- If you have used generics in other languages, your first instinct may be to pass the type to the method between angle brackets like this:
However, this produces the compiler error Cannot explicitly specialize a generic function.
- Even though you can't explicitly specify the type in this way, you can specify the type of the variable where the method's return value is stored:
This allows the compiler to infer that PersonEntity can replace the T placeholder as the createEntity method's return type. Go ahead and make this change.
- Do same for the CompanyEntity object:
- To see this code in action, set a breakpoint on the line of code shown in Figure 7.
Figure 7 - Set a breakpoint on the call to createEntity. |
- Click Xcode's Run button and execution stops on the breakpoint you set.
- Click the Step Into button in the Debug toolbar at the top of the Debug area. This moves execution to the createEntity method. The Variables View shows the T placeholder type is now PersonEntity (Figure 8). This is generics at work!
Figure 8 - The T placeholder type is CompanyEntity. |
- Click the Step Out button to run the createEntity method. This returns you to the viewDidLoad method, where you can see in the Variables View that the personEntity variable is of type PersonEntity (Figure 9).
Figure 9 - createEntity returns a PersonEntity type. |
In this example, Swift was able to determine the correct type to substitute for T using type inference on the method's return value. This also works for methods with generic parameters as you will see in just a bit.
Although generics are working properly in this example, you must still specify the type of the return variable. This doesn't completely solve the initial problem of having to explicitly declare types when accessing business object methods. There must be another way!
Enter generic classes.
Declaring Generic Classes
If you have used Swift's Array or Dictionary class, you have already used generic classes. When you declare an Array or Dictionary, you specify the type of values contained in the collection. For example, the following code declares an array of type String:
Swift lets you declare your own custom generic classes. Let's try it with mmBusinessObjectGeneric.
- Select mmBusinessObjectGeneric.swift in the Project Navigator.
- Add the following generic class declaration at the top of the code file:
This type parameter declares that T is used as a placeholder for a type within the entire class.
The : NSManagedObject in the declaration is a type constraint. It specifies mmBusinessObjectGeneric only works with types of NSManagedObject or its subclasses. This allows the class to work with PersonEntity and ConpanyEntity objects (which are subclasses of NSManagedObject), but not with objects of a completely different type such as String or Integer.
- Since the generic class declaration encompasses the entire class, we can now remove the generic type declaration from the createEntity method.
- Press Command+B to build the project and you will get several compiler errors.
- To examine one of these errors, in the Project Navigator, select the ABusinessObject.swift file. You can see the compiler error Reference to generic type 'mmBusinessObjectGeneric' requires arguments in <...> (Figure 10).
Figure 10 - The ABusinessObject compiler error |
Now that mmBusinessObjectGeneric is declared as a generic class, we need to include a type argument in angle brackets whenever we reference it.
- Since mmBusinessObjectGeneric only works with instances of NSManagedObject or its subclasses, change the mmBusinessObjectGeneric reference to:
- Press Command+B to build the project. This generates a new compiler error, Classes derived from generic classes must also be generic (Figure 11).
Figure 11 - Subclasses of generic classes must also be generic! |
This is another requirement of generics in Swift. It means we must also declare ABusinessObject as a generic class.
- Add the following generic declaration to make ABusinessObject a generic class:
Since mmBusinessObjectGeneric only works with instances of NSManagedObject, we should declare ABusinessObject to do the same.
- Rather than repeating NSManagedObject twice in the class declaration, change the type of the mmBusinessObjectGeneric reference to T:
- Press Command+B again and you can see the error has moved further down the inheritance chain to the Person and Company classes.
- Select Person.swift in the Project Navigator and add the following generic declarations:
This declares Person as a generic class with T as a placeholder that only works with instances of the PersonEntity class. The T placeholder is used as a type argument for the ABusinessObject superclass.
- Select Company.swift in the Project Navigator and add the following generic declarations:
This declares Company as a generic class that uses T as a placeholder, and only works with instances of the CompanyEntity class. The T placeholder is used as a type argument for the ABusinessObject superclass.
depicts the new generic class hierarchy.
Figure 12 - The new class hierarchy |
- Press Command+B and there are no more compiler errors!
Now you're ready to take these generic classes for a test drive!
Testing the Generic Class
- Select the ViewController.swift file in the Project Navigator.
- Remove the explicit variable types from the code that calls the createEntity method on the Person object:
This is no longer necessary since the Person object effectively passes the PersonEntity type up through the inheritance chain where it is used in the createEntity method. Swift can now infer the return value type as PersonEntity.
- Do the same for the code that calls createEntity on the Company object:
- In the viewDidLoad method, click on the personEntity variable. The Quick Help Inspector on the right side of the Xcode window shows it is of type PersonEntity (Figure 13).
Figure 13 - personEntity is of type PersonEntity. |
- In the viewDidLoad method, click on the companyEntity variable. The Quick Help Inspector shows the variable is of type CompanyEntity (Figure 14).
Figure 14 - companyEntity is of type CompanyEntity. |
- Add the following code to the viewDidLoad method to prove the createEntity method is returning entities of the correct type:
- With the breakpoint still set on the line of code that creates a new PersonEntity (Figure 7), click Xcode's Run button. When the breakpoint is hit, click Step Into in the Debug toolbar to step into the createEntity method on the Person object. The Variables View shows the T placeholder is of type PersonEntity, as it should be (Figure 15)!
Figure 15 - The T placeholder is of type PersonEntity. |
- Click Step Out to return to the viewDidLoad method, and then click Step Over four times to run the code that displays the property values in the Console:
Name: Kevin McNeish
- Click Step Over twice, and then click Step In once to run the createEntity method on the Company object. The variables view shows the T placeholder is now of type CompanyEntity (Figure 16).
Figure 16 - The T placeholder is of type CompanyEntity. |
- Click Step Out, and then click Step Over four times to run the code that displays the property values in the Console:
Company: Apple - www.apple.com
- Click Xcode's Stop button.
We have created a generic class whose methods can return the type that we specify!
Implementing Other Generic Methods
Once you have set up the generic class inheritance hierarchy, it's easy to implement other generic methods. Let's try it out.
- Select the ViewController.swift file in the Project Navigator.
- Click on the personList and companyList variables and note that the Quick Help Inspector indicates they are of the Array<NSManagedObject>? type.
- Select the mmBusinessObjectGeneric.swift file in the Project Navigator. Change the getAllEntities method's return type and the last line of code's as operation to Array<T>? as shown here:
- Select the ViewController.swift file in the Project Navigator. Click on the personList and companyList variables and you can see their types are now Array<PersonEntity>? and Array<CompanyEntity>? just as they should be!
- Feel free to run the project again, and check out the personList and companyList arrays first hand.
Conclusion
Generics is the gift that keeps on giving, and provides even further benefits for methods inherited from a generic class. They allow you to create generic custom framework classes you can use in all your projects without the mundane task of constantly downcasting!