Skip to content

Instantly share code, notes, and snippets.

@TheDreamsWind
Last active February 10, 2024 08:48
Show Gist options
  • Save TheDreamsWind/01dc6b393216f1134e2a5140e13acb61 to your computer and use it in GitHub Desktop.
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
//
// 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
//
// 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