CocoaPods/Xcodeproj — pull request #34 by irrationalfab (2012-10-12 14:31:28+00:00)
Title: Refactor

This is a refactor of the core of Xcodeproj, started from the discussion with @pbernery (#31).

Given the reach of this patch I cleaned the canvas as much as needed, sacrificing diff readability, in order to allow me to actually complete it. As a consequence, there might be some minor regressions.

The specs are not ready, but the patch was tested with CocoaPods and the examples build (except `RelativePathProject` and `RKTwitter` where I have unrelated problems with the workspaces).

##### Architecture

The callback architecture was dropped in favor of reference counting. With this refactor Xcodeproj builds a tree of objects and then converts the tree to hash, instead of using a hash for manipulating the project. On serialization the hash is built from the tree.

###### Reference counting

Objects keep a list of their referrers and add themselves to the project `objects_by_uuid` (`objects` key of the plist) dictionary accordingly. To delete and object just removing all the references to it will suffice (see #26).

The `root_object` is referenced by the project itself. As a consequence any object that is referenced by it or one of its children will be preserved. This implies that we need to have a good understanding of the model, specifically of which attributes are relationships and which not (see safety section).

###### Safety

To prevent information loss (when editing existing projects) we need to know in detail which attributes correspond to relationships. Instead of implementing heuristics I decided to just __raise when xcodeproj finds an unknown attribute__. I think that we already have a good coverage of the model and a couple of CocoaPods rcs should bring us up to speed with the 99% of the PBXClasses and their relative attributes. Now adding an attribute is really trivial.

The patch also implements runtime type checking for the attributes to prevent project corruption. Combinend with the reference counting which prevents orphans XCodeProj should be more solid.

###### ObjectList

- `ObjectList` is now an array subclass with all its flexibility and sorting ability (see #6).
- If an object method returns an `ObjectList` it is a model relationship, otherwise, if it returns an array, it is a convenience method which doesn't reflects the model (see #7).
- `ObjectLists` cannot create anymore new objects (see API Changes highlights). This was done because object lists might contain different isas and thus the semantics where not consistent as the method was applicable only for certain objects.

###### Other benefits

- __clarity__: it is easier to understand the big picture.
- __performance__: there is no need for callbacks anymore.
- __more ruby syntax__: now you always work with ruby objects, instead of hashes.
- __deletion__: it is easy to delete an object, just remove all the references to it. In pull request #26 there was no obvious way to ensure that the project was in a consistent state (i.e. no orphan objects in `objects_by_uuid`).

##### API Changes highlights

- Objects cannot be initialized anymore with a hash which is replaced for a more rubesque syntax:
```ruby
file = project.new(PBXFileReference)
file.name = 'My File'
```

- ObjectLists cannot initialize new objects anymore. Instead convenience methods are added which can perform a more advanced behavior and are easier to reason about. Another benefit is that models attributes DSL is separated:
```ruby
# old syntax
file = main_group.files.new

# new syntax (creates the file in the Pods group, making it if needed)
file = main_group.new_file('../Classes/MyClass.m', 'Pods')
```

- Some methods where moved to where they make more sense:
```ruby
project.new_static_library('Pods', :ios)
```

- The where syntax was dropped as now `ObjectLists` respond to the more powerful select:
```ruby
files = children.select { |obj| obj.class == PBXFileReference }
```

##### Performance

Additionally to the drop of callbacks there is also another performance optimization where UUIDs are generated in batches of 10.000, so the search for duplicates is not performed for every new object.

The following test were performed after a successful install of CocoaPods, so there was no installation of the `AWSiOSSDK` and all the time was spent generating the `Pods.xcodeproj` project.

```shell
# AWSiOSSDK is a good test because it has a lot of files
$ find Pods/AWSiOSSDK | wc -l
3001

# and generates a plist with almost 80k lines
$ wc -l Pods/Pods.xcodeproj/project.pbxproj
79766 Pods/Pods.xcodeproj/project.pbxproj

# CocoaPods 0.15.1
$ time pod install --no-integrate --silent
pod install --no-integrate --silent 216,50s user 1,45s system 83% cpu 4:19,68 total

# Modified CocoaPods for this patch
$ time dpod install --no-integrate --silent
COCOA_PODS_ENV=development ~/Documents/GitHub/CP/CocoaPods/bin/pod install 8,03s user 0,67s system 55% cpu 15,802 total
```

__Note__: The patch of CocoaPods also includes a performance optimization which is responsible for half of the gains.

16 seconds vs. more than 4 mins, not that bad.