Adding an Apple Watch App or Widget to Cordova
Tags: ios, cordova, ionic, applewatch
Published: 2021-04-16 19:31:14

## Intro

If you want to add native code to Cordova you’re pretty much on your own because the framework won’t help you with it.
Trying to add an iOS 14 widget and an Apple Watch app cost me 2 weeks of digging around in Cordovas source code and figuring out the Xcode project file. This article hopes to save you those 2 weeks.

## The method

So obviously the first thing you would try when doing anything that has to do with a native feature in Cordova is you wold look for a plugin that does it for you. Unfortunately the few plugins that exist don’t work anymore. So we need to get our hands dirty ourselves.

The biggest pain point when integrating native features in Cordova is that the Xcode project file is created on every build  overwriting any change you did manually.

The birds eye view of what I did is:
1. Build the Cordova app to generate the Xcode config
2. Add the iOS 14 widget and the Apple Watch target
3. See what parts of the Xcode config step 2 changed
4. Write a script to make those changes on every build

## Build the Cordova app to generate the Xcode config

This part is very simple. Just build your app the way you always do. After you build the app, Cordova will have regenerated the Xcode project file at `platforms/ios/yourapp.xcodeproj/project.pbxproj`. Store this file at a different location or check it in to git, you will compare it with a later version.

##  Add the iOS 14 widget and the Apple Watch target

Now add your iOS 14 or Apple Watch target. If you have both I would recommend doing them one by one.
Since there are plenty of tutorials on this part I won’t go into detail here.

## See what parts of the Xcode config step 2 changed

Now that you have two Xcode config files we need to compare the contents. Since the files are sort of json _(they aren’t real json and can’t be parsed as such)_ you could just compare them with a graphical diffing tool such as `Xcode -> Open Developer Tool > FileMerge`. It is helpful to have an understanding of how the Xcode config file works.
The way I found very helpful though was to convert both Xcode config files into json and then using an online tool to compare them. Apple provides a tool to convert legacy plist _(which is what the Xcode config is)_ into json.
- convert with `plutil -convert json project.pbxproj`
- diff the json with http://jsondiff.com/

## Write a script to make those changes on every build
Birds eye view:
- Cordova build
- Runs add watch / widget hook
- Hook converts Xcode config to json
- Hook adds stuf to the Xcode config as json
- Hook converts the Xcode config back to the legacy plist format so Cordova is happy

Now this is the hard part. You might want to break it into steps and keep comparing the result of your script with the Xcode config you got after adding the Watch app or iOS 14 widget.

Because there are no good parsers for the Xcode config _(except the one of CocaPods maybe but that’s Ruby and I don’t know Ruby yet)_ I opted to just convert the Xcode config in a Cordova hook to json and edit it.

Since the Xcode config is just a legacy plist file Xcode will recognize it if it is converted to json. So we just leave it as json right? Well I wish it was that easy but there is also Cordova and that has it’s own Xcode config parser that is actually quite bad [cordova-node-xcode](https://github.com/apache/cordova-node-xcode). It must have the legacy plist format  and to make matters even worth it also needs the comments preserved.

What I ended up doing was converting the json back to the legacy plist format that Cordova know how to handle. Since there is no code for that already I wrote it.

### Converting json Xcode config to legacy plist format

The Xcode config in legacy plist format looks like this

```json
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 46;
	objects = {

    // add sections here

	};
	rootObject = "...projectid..." /* Project object */;
}
```

The `objects` object holds Xcode config sections. Each object has a property named `isa`. For Cordova to recognize this file you need to loop through all the objects in your json and sort them by the isa. Then wrap each ISA section with the following comments.
```
/* Begin PBXBuildFile section */

// ...

/* End PBXBuildFile section */
```

Where `PBXBuildFile` in this example is just on ISA but you would have to do this for all the ISAs in your Xcode config.

_If you’re curios why on earth we’d need to retain the comments https://github.com/apache/cordova-node-xcode/blob/master/lib/parser/pbxproj.js#L235﻿. This is unfortunately the way the “parser” for Cordova is build._

Also see http://www.monobjc.net/xcode-project-file-format.html﻿ for information on the Xcode config file format.

### Adding the script as Cordova hook
The script you just wrote needs to be run by Cordova on each build process.

In your `config.xml` add
```xml
    <hook src="hooks/convert.py" type="before_run" />
    <hook src="hooks/convert.py" type="before_build" />
```

#### Cordova bugs
There are plenty of bugs in Cordova making our lives harder here that we need to adress.

Cordovas `ProjectFile.js` has code that overwirtes the bundle id of every Xcode build target to the one defined in `config.xml`. This is bad because the widget and watch targets need differing bundle ids.
Some upcoming version of Cordova might include a fix but for now an easy solution is to:
- Fix the file on the fly with our hook. See https://github.com/apache/cordova-ios/issues/764#issuecomment-669317730 for details on how the file needs to be patched.

- If you are using the iOS 14 widget or if your Apple Watch app is written in Swift you also need to patch a line in Cordovas config that breaks Swift code.
Comment out the following line
`SWIFT_OBJC_BRIDGING_HEADER = $(PROJECT_DIR)/$(PROJECT_NAME)/Bridging-Header.h` in `build.xcconfig`. This has to do with https://issues.apache.org/jira/browse/CB-10072.
Note though that if you have a Cordova plugin that is written in swift it might break by changing that line and you’d need a different solution. Since support for most Cordova plugins was dropped long ago and are written in ObjC it wasn’t an issue for me.

There is one more bug worth mentioning although I didn’t find a way to do anything about it https://github.com/apache/cordova-ios/issues/1049﻿ it basically prevents building the app without a real smartphone attached if you have the watch target.

And certainly not the last Cordova bug only the last I ran into. You can’t set a version lower than iOS 14 because for Cordova you need to set every target to the same minal version and the widget (at least if you wrote it with SwiftUI) requies iOS 14.

## Resources
- http://www.monobjc.net/xcode-project-file-format.html﻿
- https://stackoverflow.com/questions/32133576/what-tools-support-editing-project-pbxproj-files
- https://github.com/kronenthaler/mod-pbxproj/wiki#basic-usage
- https://github.com/microsoft/xcodeproj﻿
- http://www.monobjc.net/xcode-project-file-format.html﻿
- https://github.com/CocoaPods/CocoaPods/issues/1275#issuecomment-22391200﻿