Skip to content

Instantly share code, notes, and snippets.

@smileyborg
Last active May 26, 2020 12:08
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save smileyborg/d513754bc1cf41678054 to your computer and use it in GitHub Desktop.
Save smileyborg/d513754bc1cf41678054 to your computer and use it in GitHub Desktop.
Backwards compatible macros for Objective-C nullability annotations and generics
/**
* The following preprocessor macros can be used to adopt the new nullability annotations and generics
* features available in Xcode 7, while maintaining backwards compatibility with earlier versions of
* Xcode that do not support these features.
*/
#if __has_feature(nullability)
# define __ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
# define __ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
# define __NULLABLE nullable
#else
# define __ASSUME_NONNULL_BEGIN
# define __ASSUME_NONNULL_END
# define __NULLABLE
#endif
#if __has_feature(objc_generics)
# define __GENERICS(class, ...) class<__VA_ARGS__>
# define __GENERICS_TYPE(type) type
#else
# define __GENERICS(class, ...) class
# define __GENERICS_TYPE(type) id
#endif
/**
* Here are some examples of the above macros being used.
* The comments below each line represent the output of the preprocessor (e.g. what the compiler will see)
* when using both Xcode 7 and Xcode 6.
*/
#import "Xcode7Macros.h"
__ASSUME_NONNULL_BEGIN
// Xcode 7: NS_ASSUME_NONNULL_BEGIN
// Xcode 6:
@interface __GENERICS(MyClass, GenericType1, GenericType2) : NSObject
// Xcode 7: @interface MyClass<GenericType1, GenericType2> : NSObject
// Xcode 6: @interface MyClass : NSObject
@property (nonatomic, strong) __GENERICS(NSArray, __GENERICS(NSDictionary, GenericType1, GenericType2) *) *arrayOfDictionaries;
// Xcode 7: @property (nonatomic, strong) NSArray<NSDictionary<GenericType1, GenericType2> *> *arrayOfDictionaries;
// Xcode 6: @property (nonatomic, strong) NSArray *arrayOfDictionaries;
- (__NULLABLE __GENERICS_TYPE(GenericType2))someMethodWithAParameter:(__NULLABLE NSString *)param;
// Xcode 7: - (nullable GenericType2)someMethodWithAParameter:(nullable NSString *)param;
// Xcode 6: - (id)someMethodWithAParameter:(NSString *)param;
@end
__ASSUME_NONNULL_END
// Xcode 7: NS_ASSUME_NONNULL_END
// Xcode 6:
@smileyborg
Copy link
Author

Some notes:

  • You may want to add your own prefix to the names of these macros to avoid any namespace conflicts, e.g. __TF_NULLABLE
  • If you're using audited regions (NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END) you shouldn't need to use the __nonnull annotation, so it's not included in the above macros. However, if you find a need for it, it's trivial to add a macro (following the same pattern as __NULLABLE).
  • Yes, code that uses these macros isn't the most beautiful thing in the world, but it gets the job done!

If you're interested in seeing a more extensive example of how this looks and works in real code, see this commit to the INTUGroupedArray project on GitHub.

@bdash
Copy link

bdash commented Jun 13, 2015

  • NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END are in the OS X 10.10.3 SDK that ships with Xcode 6.3, so you can benefit from them prior to Xcode 7's release.
  • It's preferable to use Clang's __has_feature tests to determine whether these new features can be used rather than inferring based on the SDK version being used. For instance, nullability can be detected using #if __has_feature(nullability), and generics with #if __has_feature(objc_generics).

@smileyborg
Copy link
Author

@bdash Excellent points, the gist has been updated to reflect this!

@e28eta
Copy link

e28eta commented Jun 17, 2015

Why introduce your own symbols for assume nonnull and nullable? Why not simply provide a shim that defines the Apple symbols as noops if they're not available?

#if !(__has_feature(nullability))
#define NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_END
#define nullable
#endif

It doesn't solve for generics, but it makes your code forward compatible too, and readable by anyone familiar with the standard SDK.

@smileyborg
Copy link
Author

@e28eta That is one approach, but as you mentioned that wouldn't work for the backwards-compatible generics. Aside from being consistent with both of these features, I think using new, unique macro names feels a little safer and less invasive than redefining Apple's existing macros. It's also very easy to find-replace these custom macros with the real counterparts once backwards compatibility is no longer a concern. Ultimately, it's up to you to decide which approach to use though!

@mickeyreiss
Copy link

@smileyborg I think this might still be missing a shim for __nullable, which is necessary in some declarations with block parameters:

- (void)doBlock:(void (^)(NSString __nullable* arg))completionBlock;

I ended up adapting your snippet like this:

#if __has_feature(nullability)
#   define BT_ASSUME_NONNULL_BEGIN      NS_ASSUME_NONNULL_BEGIN
#   define BT_ASSUME_NONNULL_END        NS_ASSUME_NONNULL_END
#   define BT_NULLABLE                  nullable
#   define __BT_NULLABLE                __nullable
#else
#   define BT_ASSUME_NONNULL_BEGIN
#   define BT_ASSUME_NONNULL_END
#   define BT_NULLABLE
#   define __BT_NULLABLE
#endif

:octocat:

@smileyborg
Copy link
Author

@mickeyreiss Yes, __nullable is necessary for some C-like APIs, so defining another macro for it like you did may be required.
Also, the reason for the __ prefix in front of all these macros is to protect them a bit, since these are going to have to live in a public header that gets imported by clients of your library, but your clients probably don't want these macros showing up in the global (autocomplete) namespace.

@ricardopereira
Copy link

What about value type definitions for NSArray or key value types for NSDictionary that are only available for Xcode 7? Do you have any idea?

e.g.:

// Xcode 6
NSArray *authParams; //<-- will produce AnyObject

// Xcode 7
NSArray<NSURLQueryItem *> *authParams;

@ricardopereira
Copy link

Using __apple_build_version__ for the check? What do you think?

// http://en.wikipedia.org/wiki/Xcode#Toolchain_Versions
if defined(__apple_build_version__)
     if __apple_build_version__ >= 700059

@smileyborg
Copy link
Author

@ricardopereira I'm not sure what you're asking here.

@ricardopereira
Copy link

I want to support NSArray *authParams and NSArray<NSURLQueryItem *> *authParams. Any thoughts? /cc @bdash @mickeyreiss

@smileyborg
Copy link
Author

@ricardopereira and why doesn't this work for you?

__GENERICS(NSArray, NSURLQueryItem *) *authParams;

@ricardopereira
Copy link

Oh, sorry. Didn't know.
Thanks.

@colasbd
Copy link

colasbd commented Oct 20, 2015

Xcode inserts automatically code such as

saveOperation.modifyRecordsCompletionBlock = 
^(NSArray <CKRecord *> * _Nullable savedRecords,
  NSArray <CKRecordID *> * _Nullable deletedRecordIDs,
  NSError * _Nullable operationErro) {...};

which fails at being compiled with Xcode 6, because of NSArray <CKRecord *> *. Is it possible to help Xcode 6 compiling?

@smileyborg
Copy link
Author

@colasjojo You can probably just omit (delete) the _Nullable and <CKRecord *>/<CKRecordID *> entirely and it should continue to compile fine on Xcode 7 with no change in behavior. If you want to preserve the type information then you should try converting the syntax to use the macros in this gist, something like:

saveOperation.modifyRecordsCompletionBlock = 
^(__GENERICS(NSArray, CKRecord *) * _Nullable savedRecords,
  __GENERICS(NSArray, CKRecordID *) * _Nullable deletedRecordIDs,
  NSError * _Nullable operationErro) {...};

(You would need to also define a new macro for _Nullable and substitute that if you're trying to support Xcode 6.2 and earlier.)

@samjarman
Copy link

@smileyborg I believe the macro breaks bridging into swift?

@smileyborg
Copy link
Author

@samjarman They shouldn't, these are being used by projects that bridge to Swift. Are you seeing a specific issue?

@hrieke
Copy link

hrieke commented May 26, 2020

Very cool, do you have a license for your code that we can use?
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment