Compile-time keypath safety macro for Objective-C

When using KVO or KVC in Objective-C, we encounter opportunities where we have to write a property name as a string, such as valueForKey:, valueForKeyPath:, willAccessValueForKey:, willChangeValueForKey:, etc.

Specifying a property name as a string means that if the property name is changed, the string part must also be explicitly changed at the same time. If the modification is forgotten, the compiler will not warn us and the code will crash at run-time.

Using NSStringFromSelector() and @selector() together as follows makes the compiler to be able to issue a warning. So, this is a famous solution.

Famous code:

// Use NSStringFromSelector() with @selector()
[self valueForKey:NSStringFromSelector(@selector(param_))];

While this technique is valid, it will cost some little overhead because NSStringFromSelector() operates at run-time. And also, it can't handle keypaths. So, it's time to think about techniques which enable compile-time warnings if possible without using NSStringFromSelector().

Are there any techniques to take advantage of the warning function in @selector(), and at the same time specify a property name as a string? See below codes.

How this works:

// Activate compile-time keypath check with @selector()
// Single level keypath
[self valueForKey:((void)@selector(param_), @"param_")];

// Multi level keypath
[self valueForKeyPath:((void)@selector(items_), (void)@selector(intValue), @"items_.intValue")];

In this example, the return values of @selector()s are cast to void and ignored, while the commas are used to join the syntaxes and express a property string. Also, since the result must represent a single string, the entire syntaxes are enclosed in ().

For multi level keypaths, it seems good to gather @selector()s in front and return a joined string at the end.

To achieve this usefully, we can take the way to implement codes as below by using the technique of pseudo-recursible macro.

One thing to consider, a problem is caused by the warning function in @selector(), we can't avoid the property name contamination because it is indistinguishable from a property name that validly exists in another class. However, this implementation is a realistic solution, I think.


// General use pseudo-recursible macro algorithm (maximum 16 level)
#define VA_ARGS_CONCAT_IMPL(x, y)\
#define VA_ARGS_CONCAT(x, y)\
    16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
#define VA_ARGS_COUNT_INDEX(a1, a2, a3, a4, a5, a6, a7, a8,\
    a9, a10, a11, a12, a13, a14, a15, a16, n, ...)\
#define VA_ARGS_COUNT_IMPL(...)\
#define VA_ARGS_COUNT(...)\
#define VA_ARGS_FOR_1(f, g, a)\
#define VA_ARGS_FOR_2(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_1(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_3(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_2(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_4(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_3(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_5(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_4(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_6(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_5(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_7(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_6(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_8(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_7(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_9(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_8(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_10(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_9(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_11(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_10(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_12(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_11(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_13(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_12(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_14(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_13(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_15(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_14(f, g, __VA_ARGS__))
#define VA_ARGS_FOR_16(f, g, a, ...)\
    g(f(a), VA_ARGS_FOR_15(f, g, __VA_ARGS__))
#define VA_ARGS_FOR(...)\

// Utility macros
#define VA_ARGS_GLUE_NONE(x, y)\
    x y
    x; y
#define VA_ARGS_GLUE_COMMA(x, y)\
    x, y
#define VA_ARGS_GLUE_STRING_DOT(x, y)\
    x @"." y

// KeyPath safety macro algorithm (Core)
#define keyof_impl_selector(param)\
#define keyof_impl_string(param)\
#define keyof(param)\
    (keyof_impl_selector(param), keyof_impl_string(param))
#define keypathof_impl(prefix, suffix, ...)\
    VA_ARGS_FOR(__VA_ARGS__)(keyof_impl_selector, VA_ARGS_GLUE_COMMA, __VA_ARGS__),\
    prefix VA_ARGS_FOR(__VA_ARGS__)(keyof_impl_string, VA_ARGS_GLUE_STRING_DOT, __VA_ARGS__) suffix
#define keypathof(...)\
    (keypathof_impl(, , __VA_ARGS__))

// KeyPath safety macro algorithm (Extended)
#define keypath_count_of(...)\
    (keypathof_impl(, @".@count", __VA_ARGS__))
#define avg_keypath_of(...)\
    (keypathof_impl(@"@avg.", , __VA_ARGS__))
#define max_keypath_of(...)\
    (keypathof_impl(@"@max.", , __VA_ARGS__))
#define min_keypath_of(...)\
    (keypathof_impl(@"@min.", , __VA_ARGS__))
#define sum_keypath_of(...)\
    (keypathof_impl(@"@sum.", , __VA_ARGS__))
#define distinctUnionOfObjects_keypath_of(...)\
    (keypathof_impl(@"@distinctUnionOfObjects.", , __VA_ARGS__))
#define unionOfObjects_keypath_of(...)\
    (keypathof_impl(@"@unionOfObjects.", , __VA_ARGS__))
#define distinctUnionOfArrays_keypath_of(...)\
    (keypathof_impl(@"@distinctUnionOfArrays.", , __VA_ARGS__))
#define unionOfArrays_keypath_of(...)\
    (keypathof_impl(@"@unionOfArrays.", , __VA_ARGS__))
#define distinctUnionOfSets_keypath_of(...)\
    (keypathof_impl(@"@distinctUnionOfSets.", , __VA_ARGS__))

Usage 1:

// @"param_"
id value = [self valueForKey:keyof(param_)];

// @"items_.intValue"
id value = [self valueForKeyPath:keypathof(items_, intValue)];

// @"@sum.self"
id sum = [self.items_ valueForKeyPath:sum_keypath_of(self)];

Usage 2:

+ (NSSet<NSString*>*)keyPathsForValuesAffectingDerivedValue
    return [NSSet setWithObjects:keypathof(coeff_), keypathof(items_, floatValue), nil];

Usage 3:

static void* s_context = &s_context;

- (instancetype)init
    if ((self = [super init]) != nil) {
        [self addObserver:self forKeyPath:keypathof(value_) options:0 context:s_context];
        [self addObserver:self forKeyPath:keypath_count_of(items_) options:0 context:s_context];
    return self;

- (void)dealloc
    [self removeObserver:self forKeyPath:keypath_count_of(items_) context:s_context];
    [self removeObserver:self forKeyPath:keypathof(value_) context:s_context];

- (void)observeValueForKeyPath:(nullable NSString*)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id>*)change
                       context:(nullable void*)context
    if (context == s_context) {
        if (object == self) {
            if ([keyPath isEqualToString:keypathof(value_)]) {
                // do something...
            else if ([keyPath isEqualToString:keypath_count_of(items_)]) {
                // do something...
    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];


// NG
id count = [self.items_ valueForKey:keyof(@count)];
// OK
id count = [self.items_ valueForKeyPath:keypath_count_of(self)];

// NG
id avg = [self valueForKeyPath:keypathof(items_, @avg, self)];
// OK
id avg = [self.items_ valueForKeyPath:avg_keypath_of(self)];
// OK (alternative, more complex, run-time)
id avg = [self valueForKeyPath:[NSString stringWithFormat:@"%@.@avg.%@", keyof(items_), keyof(self)]];
To use keyPathsForValuesAffecting<key> with NSController family

When describing the relationship between dependent properties using KVO, it is well known that the compatibility between a class which inherits NSController, and keyPathsForValuesAffectingValueForKey: or keyPathsForValuesAffecting<key> is worse.

Specifically, changes in selectedObjects can't be handled automatically, such as when implementing derived properties that depends on the value of selectedObjects of NSArrayController.

We can implement the goal by explicitly observing property changing with addObserver and removeObserver, but it is unsatisfactory.

Why do only the properties of a class which inherits NSController not work properly? This seems to be due to the specification that NSKeyValueObservance class used internally by NSController always handles the context value as nil.

Example that don't work:

@property (strong) NSArrayController* ac_;
@property (strong, readonly) NSString* secondValue;

- (nullable NSString*)secondValue
    NSString* result = nil;

    if (self.ac_.selectedObjects.count >= 2) {
        result = self.ac_.selectedObjects[1];
    return result;

+ (NSSet<NSString*>*)keyPathsForValuesAffectingSecondValue
    return [NSSet setWithObjects:@"ac_.selectedObjects", nil];

Therefore, to fix this problem, make a subclass inherited from NSController and override addObserver and removeObserver like below.

Its point is when the observer is a kind of NSKeyValueObservance class, the context value is always overridden as nil.

Modified ArrayController:

@interface ModArrayController : NSArrayController

@implementation ModArrayController
- (void)addObserver:(NSObject*)observer
    [super addObserver:observer
               context:([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) ? (nil) : (context)];

- (void)removeObserver:(NSObject*)observer
    [super removeObserver:observer
                  context:([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) ? (nil) : (context)];

- (void)removeObserver:(NSObject*)observer
    NSMutableDictionary* dictionary;
    dictionary = NSThread.currentThread.threadDictionary;
    if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")] &&
            dictionary[@"NSKeyValueObservanceLockKey"] == nil) {
        dictionary[@"NSKeyValueObservanceLockKey"] = @YES;
        [super removeObserver:observer forKeyPath:keyPath context:nil];
        [dictionary removeObjectForKey:@"NSKeyValueObservanceLockKey"];
    else {
        [super removeObserver:observer forKeyPath:keyPath];

Use above modified class instead of NSArrayController. Just this solves the compatibility problem with keyPathsForValuesAffectingValueForKey: and keyPathsForValuesAffecting<key>.

This technique can be applied not only to NSArrayController, but also to any class which inherits NSController.

Recoverable DC Circuit Breaker

+1.8V ~ 28.0V で使用可能な直流電源用の過電流遮断器。

電源と負荷のあいだに挿入することにより、10mΩ の電流検出抵抗を介して過電流を検出し、出力 MOSFET を遮断する。トランジスタで組まれたラッチ回路により遮断状態は保持され、リセットスイッチを押すか、電源をオフにすることにより導通が復活する。

VR1 の半固定抵抗により制限電流値を最大 2.4A の範囲で設定できるが、基板の寸法を小さく設計したため 2A 程度が実用範囲である。電流検出部は、D1, D2 による簡易的な安定化電源により駆動されるため、2.4V 以下の入力時には、入力電圧の影響を受け制限電流値が低下する。





Gerber - top

Gerber - bottom

PCB - top x6

PCB - bottom x6

Index Value Model Mfr.Manufacturer @
R1 47K ohm (Thick Film Chip, 0805) (KOA) 1
R2 3.3K ohm (Thick Film Chip, 0805) (KOA) 1
R3 10K ohm (Thick Film Chip, 0805) (KOA) 1
R4 1K ohm (Thick Film Chip, 0805) (KOA) 1
R5, R7, R8, R9, R10, R14, R15 56K ohm (Thick Film Chip, 0805) (KOA) 7
R6, R11, R13, R16, R17 20K ohm (Thick Film Chip, 0805) (KOA) 5
R12 100K ohm (Thick Film Chip, 0805) (KOA) 1
R18 10m ohm PMR18EZPFU10L0 ROHM 1
VR1 10K ohm ST-4ETB103 Nidec Copal 1
C1, C2 10uF GRM21BR6YA106KE43 Murata 2
C3, C6 0.1uF CGA4J2X7R1H104K125AA TDK 2
C4 0.33uF CGA4J2X7R1H334K125AA TDK 1
C5, C7 0.47uF CGA4J3X7R1H474K125AB TDK 2
D1, D9 2mA S-202T SEMITEC 2
D2 2.4V UDZVTE-172.4B ROHM 1
D3, D4, D5, D6, D7, D8 RB501VM-40TE-17 ROHM 6
D10 Red GM4ZR83232AE SHARP 1
D11 18V UFZVTE-1718B ROHM 1
Q1, Q2, Q6, Q7, Q8 2SC2712-GR TOSHIBA 5
Q3, Q4, Q5, Q9 2SA1162-GR TOSHIBA 4
U1 INA190A2QDCKRQ1 Texas Instruments 1
U2 NJU7109F JRC 1
SW1 THAU13-AB-R Zhejiang Jianfu 1
(JP1), (JP2) OPT
CCXMonitor + CCXLive

Communication Cube X のための openFrameworks アプリケーション。CCXMonitor は、各個体のテレメトリデータを監視する。CCXLive は、各個体のキー押下状態を監視し、擬似キー押下信号を送信することができる。