Last active
February 10, 2024 08:48
-
-
Save TheDreamsWind/01dc6b393216f1134e2a5140e13acb61 to your computer and use it in GitHub Desktop.
[SO-a/74386650/5690248] `TDWHasObserver` category leverages private API information to tell whether an object is an existing observer of the instance
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+TDWHasObserver.h | |
// | |
// Created by Aleksandr Medvedev on 09.11.2022. | |
// | |
#import <Foundation/Foundation.h> | |
NS_ASSUME_NONNULL_BEGIN | |
OBJC_EXTERN NSErrorDomain const TDWObserverLookupErrorDomain; | |
typedef NS_ENUM(int16_t, TDWObserverLookupError) { | |
TDWObserverLookupErrorUnknown = -1, | |
TDWObserverLookupErrorInconsistentLayout = 100, | |
}; | |
__attribute__((objc_direct_members)) | |
@interface NSObject (TDWHasObserver) | |
- (BOOL)tdw_hasObserver:(id)observer | |
forKeyPath:(nullable NSString *)keyPath | |
context:(nullable void *)context | |
error:(NSError *__autoreleasing _Nullable *_Nullable)error; | |
@end | |
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+TDWHasObserver.m | |
// | |
// Created by Aleksandr Medvedev on 09.11.2022. | |
// | |
#define INIT_IVAR_MACRO(ivar, name, ivarList, errorFormat, errorPtr) if (!ivar) { \ | |
ivar = [self tdw_p_findIvarByName:name inList:ivarList];\ | |
if (!ivar) {\ | |
*errorPtr = [NSError errorWithDomain:TDWObserverLookupErrorDomain\ | |
code:TDWObserverLookupErrorInconsistentLayout\ | |
userInfo:@{\ | |
NSLocalizedDescriptionKey: [NSString stringWithFormat:errorFormat, name],\ | |
NSDebugDescriptionErrorKey: [self tdw_p_makeDebugInformation]\ | |
}];\ | |
}\ | |
} | |
#import "NSObject+TDWHasObserver.h" | |
#import <objc/runtime.h> | |
NSErrorDomain const TDWObserverLookupErrorDomain = @"the.dreams.wind.error_domain.observer_lookup"; | |
__attribute__((objc_direct_members)) | |
@implementation NSObject (TDWHasObserver) | |
#pragma mark Actions | |
- (BOOL)tdw_hasObserver:(id)observer forKeyPath:(NSString *)keyPath context:(void *)context error:(NSError **)error { | |
NSError *internalError; | |
BOOL result = NO; | |
NSArray *observances = [self tdw_p_obtainObservancesWithError:&internalError]; | |
if (observances && !internalError) { | |
Ivar *observanceIvarList = NULL; | |
Ivar *observerIvar = NULL; | |
Ivar *keyPathIvar = NULL; | |
Ivar *contextIvar = NULL; | |
for (id observance in observances) { | |
if (!observanceIvarList) { | |
observanceIvarList = class_copyIvarList([observance class], NULL); | |
if (!observanceIvarList) { | |
// TODO: report error | |
result = NO; | |
break; | |
} | |
} | |
INIT_IVAR_MACRO(observerIvar, | |
@"_observer", | |
observanceIvarList, | |
@"Could not find %@ ivar position for the observance object", | |
&internalError) | |
if (internalError) { | |
result = NO; | |
break; | |
} | |
if (object_getIvar(observance, *observerIvar) != observer) { | |
continue; | |
} | |
if (!keyPath && !context) { | |
// Search by observer only | |
result = YES; | |
break; | |
} else { | |
BOOL keyPathFound = [self tdw_p_handleSearchForKeyPath:keyPath | |
observance:observance | |
ivarPtr:keyPathIvar | |
ivarList:observanceIvarList | |
error:&internalError]; | |
if (internalError) { | |
result = NO; | |
break; | |
} | |
BOOL contextFound = [self tdw_p_handleSearchForContext:context | |
observance:observance | |
ivarPtr:contextIvar | |
ivarList:observanceIvarList | |
error:&internalError]; | |
if (internalError) { | |
result = NO; | |
break; | |
} | |
if (keyPathFound && contextFound) { | |
result = YES; | |
break; | |
} | |
} | |
} | |
free(observanceIvarList); | |
} | |
if (error && internalError) { | |
*error = internalError; | |
} | |
return result; | |
} | |
#pragma mark Private | |
- (BOOL)tdw_p_handleSearchForKeyPath:(NSString *)keyPath observance:(id)observance ivarPtr:(Ivar *)ivarPtr | |
ivarList:(Ivar *)ivarList error:(NSError **)error { | |
if (!keyPath) { | |
// No keyPath, thus any value is acceptable | |
return YES; | |
} | |
INIT_IVAR_MACRO(ivarPtr, @"_property", ivarList, @"Could not find %@ ivar position for the observance class", error) | |
if (*error) { | |
return NO; | |
} | |
id keyValueProperty = object_getIvar(observance, *ivarPtr); | |
static Ivar keyPathIvar; | |
static dispatch_once_t token; | |
dispatch_once(&token, ^{ | |
Ivar *keyValuePropertyIvarList = class_copyIvarList([keyValueProperty superclass], NULL); | |
keyPathIvar = *[self tdw_p_findIvarByName:@"_keyPath" inList:keyValuePropertyIvarList]; | |
free(keyValuePropertyIvarList); | |
}); | |
if (!keyPathIvar) { | |
*error = [NSError errorWithDomain:TDWObserverLookupErrorDomain | |
code:TDWObserverLookupErrorInconsistentLayout | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"Could not find keyPath ivar position", | |
NSDebugDescriptionErrorKey: [self tdw_p_makeDebugInformation] | |
}]; | |
return NO; | |
} | |
return [keyPath isEqual:object_getIvar(keyValueProperty, keyPathIvar)]; | |
} | |
- (BOOL)tdw_p_handleSearchForContext:(void *)context observance:(id)observance ivarPtr:(Ivar *)ivarPtr | |
ivarList:(Ivar *)ivarList error:(NSError **)error { | |
if (!context) { | |
// No context, thus any value is acceptable | |
return YES; | |
} | |
INIT_IVAR_MACRO(ivarPtr, @"_context", ivarList, @"Could not find %@ ivar position for the observance class", error) | |
if (*error) { | |
return NO; | |
} | |
return (__bridge void *)object_getIvar(observance, *ivarPtr) == context; | |
} | |
- (NSArray *)tdw_p_obtainObservancesWithError:(NSError **)error { | |
id info = (id)self.observationInfo; | |
if (!info) { | |
return nil; | |
} | |
Ivar *ivarList = class_copyIvarList([info class], NULL); | |
if (!ivarList) { | |
*error = [NSError errorWithDomain:TDWObserverLookupErrorDomain | |
code:TDWObserverLookupErrorUnknown | |
userInfo:@{ | |
NSLocalizedDescriptionKey: @"Could not extract ivar list for the observation of the class", | |
NSDebugDescriptionErrorKey: [self tdw_p_makeDebugInformation] | |
}]; | |
return nil; | |
} | |
NSArray *observances; | |
for (typeof(ivarList) ivarPtr = ivarList; *ivarPtr; ++ivarPtr) { | |
id ivar = object_getIvar(info, *ivarPtr); | |
if ([ivar isKindOfClass:[NSArray class]]) { | |
// Assumes the observables are found | |
observances = ivar; | |
break; | |
} | |
} | |
free(ivarList); | |
return observances; | |
} | |
- (Ivar *)tdw_p_findIvarByName:(NSString *)substring inList:(Ivar *)ivarList { | |
for (typeof(ivarList) ivarPtr = ivarList; *ivarPtr; ++ivarPtr) { | |
// Naive search | |
if (!strcmp(ivar_getName(*ivarPtr), substring.UTF8String)) { | |
return ivarPtr; | |
} | |
} | |
return NULL; | |
} | |
- (NSString *)tdw_p_makeDebugInformation { | |
return [NSString stringWithFormat:@"Observable class requested: %@\nInstance: %@;\nObservation class: %@\nInstance: %@", | |
[self class], self, [(id)self.observationInfo class], self.observationInfo]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment