Xcode: Monitoring your public API — Part 2
In Part 1 we looked at how we could generate the public API of our Objective-C/Swift frameworks in the Swift language. But what if we want to see how our public API looks in ObjC? Most people have moved on from ObjC so it may not be such a big concern for many, but some of the devs using your frameworks might still be using it from ObjC, so it may be worth making sure you’re not breaking the public API for them unexpectedly.
Objective-C
In an ObjC framework the public API is defined by the umbrella header, which includes all the public headers that exist inside the framework. One approach to generating a listing of the public API is to walk through each public header listed there and include everything that’s defined in them.
I’ve created a script that does exactly this in the GitHub example project, which generates the following output for the ObjC framework:
FOUNDATION_EXPORT double ObjC_FrameworkVersionNumber;
FOUNDATION_EXPORT const unsigned char ObjC_FrameworkVersionString[];
typedef NSWindow XPWindow;
typedef UIWindow XPWindow;
@interface OCClass : NSObject
- (NSInteger)getInt;
- (NSString* _Nonnull)intToStringWith:(NSInteger)integer;
- (NSString* _Nullable)tryIntToStringWith:(NSInteger)integer;
- (XPWindow* _Nonnull)getWindow;
- (void)macFunction;
- (void)iOSFunction;
@end
typedef NS_ENUM(NSUInteger, EnumValue) {
kOne,
kTwo,
kThree
};
struct OCStruct {
enum EnumValue enumValue;
};
That looks pretty good, all the types defined in the code are showing up, even for different platforms (see the inclusion of the macFunction and iOSFunction methods), so you only need to run it for one platform and you get the public API of everything (the script could be modified to only show the output for a single platform if desired).
Swift
Swift frameworks already have a file that describes the public API in ObjC, the <framework name>-Swift.h file. It’s the file you’d import into your ObjC code if you wanted to make use of your own Swift code within your framework.
The raw -Swift.h file has quite a lot of irrelevant noise at the top (at least for what we want from the file), so I put together a script in the GitHub example project that would remove that and just leave what we’re really interested in.
For the Swift framework here’s the output:
// Generated by Apple Swift version 5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)
SWIFT_CLASS(“_TtC15Swift_Framework6SClass”)
@interface SClass : NSObject
- (NSInteger)getInt SWIFT_WARN_UNUSED_RESULT;
- (NSString * _Nonnull)intToString:(NSInteger)int_ SWIFT_WARN_UNUSED_RESULT;
- (NSString * _Nullable)tryIntToString:(NSInteger)int_ SWIFT_WARN_UNUSED_RESULT;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
Well, it’s a bit less than we might have expected, but you have to remember that many Swift types and features aren’t available to ObjC, so the Swift enum is left out and even the Swift class would have been left out had it not been correctly marked for exposure to Swift by inheriting from NSObject and tagging each function with @objc.
Conclusion
Nice, with part 1 and part 2 we can now generate listings of our public API in both ObjC and Swift, for both ObjC and Swift frameworks! Integrate it all into our way of working (pipelines, pull requests etc.) and we’ll never unexpectedly break our public APIs again (maybe)!
Disclaimer: I’m no shellscript expert by any stretch of the imagination so these scripts may make your skin crawl, who knows! Happy to receive feedback!
As with the scripts in part 1, you should keep an eye on how long these scripts take to run. You may well find that the more code you have in your frameworks the longer they take to execute and might noticeably slow down your build time. You may want to run them only in your CI pipelines (or manually if you don’t have CI!).
As pointed out in the updated version of part 1, the solution I gave to generate the Swift API from an ObjC framework actually only works if your framework is compatible with macOS. So if you blindly import UIKit, for example, it will fail to generate the API. You can put a load of #ifs into your code to make it compile, but you then just get the macOS API out, so if you’re actually just supporting iOS then quite possibly a lot of your public API wouldn’t be included. Hopefully in part 3 I can present a better solution! And we still haven’t covered multi-language frameworks yet!