JLConsoleLog

What is JLConsoleLog?

JLConsoleLog is an awesome tool In-App to help swift developer log information in both development and production environment. You won't miss any key and useful logs about the bugs in non-debug mode. You also can integrate it in your project's backdoor toolkit, it will help you solve vital issues.

How to operate it?

JLConsoleLog supports three types of style (display mode) — Floating, Bubble and FullScreen

Floating

Bubble

FullScreen

There are four buttons on the option view of floating mode. The first button is setting button where you can clear all logs from this console, filter categories and levels. The second one is for switching between floating and fullscreen mode. If the third one is pressed, the console will be a translucent bubble that only displays warning and error count. The last one is close button.

The floating console could become translucent automatically after 5s, if you don't touch it. Additionally, you can drag floating console and bubble to anywhere to avoid disturb you.

While you tap a log cell, you can enter the detail page of log.


Performance monitor is a new function. You can invoke a monitor chart from bubble button now.

How to use it in your project?

The JLConsoleController is the console's controller opened for developers. It contains a shared instance. You could set style and logEnabled via it. While you set its style (display mode), the console will immediately show in terms of you given. Like this:

if JLConsoleController.shared.style == .Hidden {
     JLConsoleController.shared.style = .Floating //show console in floating fashion
        } else {
    JLConsoleController.shared.style = .Hidden //hide console
        }

If logEnabled is true, the console will collect log data, vice verse.

JLConsoleController.shared.logEnabled = true

A series functions are offered to log.

func JLLevelLog(level: JLConsoleLogLevel, category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...)

func JLVerboseLog( category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...) 

func JLDebugLog( category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...)

func JLInfoLog( category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...)

func JLWarningLog( category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...)

func JLErrorLog( category: JLConsoleLogCategory, hasFollowingAction:Bool = false, needPrint:Bool = false, contextData:Dictionary<String,Any> , formats: String...)

The JLConsoleLogLevel is an enum to sort by different levels. The numbers of warning and error are displayed on option view and bubble.

The JLConsoleLogCategory is your business category and is an alias of String. You can define your own categories met your demand, such as Video, TrackPage, Commodity Detail… If you need to filter your categories, you must register in this way in your code:

let SubPageTestLog:JLConsoleLogCategory = "com.consolelog.mybusiness" //declare a category

JLConsoleController.shared.register(newCategory: SubPageTestLog) //register it

The parameter, contextData, is a serializable Dictionary. The data will be shown on the detail page in Json.

The parameters, formats, is variadic parameters of String. The first value will be shown on the cell's title in console.

If needPrint equals true, the log information will print in your Xcode console in Debug environment.

Otherwise, JLConsoleController provides a followingAction to operate other actions when you finish logging. For example, you can send a track point log to statistics server such as Firebase in followingAction closure. Meanwhile, please don't forget to set hasFollowingAction as true while you log.

JLErrorLog(category: SubPageTestLog, hasFollowingAction: true ,needPrint: true, contextData: ["test":5], formats: "Error!",#function,String(#line))

This is an error log example.


Performance Monitor

JLConsoleLog provides a performance monitor. You can add these to turn on it.

JLConsoleController.shared.performanceMonitable = true

The git address is https://github.com/jacklandrin/JLConsoleLog

Requirement

  • iOS 12
  • Swift 5.2
  • Xcode 11

Reference

PerformanceMonitor

Talk about Aspect Oriented Programming in iOS

1. What's the Aspect Oriented Programming?

The Apsect Oriented Programming (AOP) is usually used in backend development. But it's more and more popular in clients programming recent days.

You can find out the definition in Wikipedia:

In computing, AOP is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification, such as "log all function calls when the function's name begins with 'set'". This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. AOP forms a basis for aspect-oriented software development.

2. How to implement AOP in iOS?

When we develop a statistic system of client, sometimes we don't want to add any codes to the business logic. AOP is a good choice for some situation of logging. We can hook methods in which we need to make some logs. Such as logging page appear and disappear.

Method Swizzling

In iOS programming, we can implement AOP by Method Swizzling. Method Swizzling is way that replace original method with another.

Talking about some Runtime knowledge is necessary to how to explain Method Swizzling. Method in Objective-C consists of SEL(Selector) and IMP(implemention).

  • Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

  • IMP is a pointer to the start of the function that implements the method.

Method Swizzling exchanges IMPs mapped selectors in order to replace methods. Implementing a Method Swizzling, the core C language API at runtime is:

OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
 __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

Method Swizzling Example

Suppose we want to print out a log statement whenever -viewDidLoad() is called on a UIViewController. Creating a category of UIViewController, and implement its +load().

#import "UIViewController+swizzling.h"
#import @implementation UIViewController (swizzling)
 
+ (void)load {
    [super load];
    // class_getInstanceMethod() get method structure from method list
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(swizzling_ViewDidLoad));
    
    if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
    }
}
 
//  define method by myself, replacing original method
- (void)swizzling_ViewDidLoad {
    NSLog(@"Log something... for %@",[self class]);
    [self swizzling_ViewDidLoad];
}
@end

3. Aspects

Obviously, if we use this way to hook every method needed to add log, it will be a huge project. Maybe there is a third party libarary that we use easier. YES, Aspects is an awesome tool for AOP in Objective-C.

Aspects provides two methods for developer:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

An instance method and a class method, they have same name and same parameters. It's thread safe for all invoking, and Aspects makes some perfermance loss due to message passing of Objective-C. Otherwise I don't suggest use Aspects for methods in frequent invoked.

Example

I use the Aspects instead of complex Method Swizzling, hook -viewWillApear() in all UIViewControllers.

#import "ViewControllerLogger.h"
#import @implementation ViewControllerLogger
 
- (instancetype)init {
    if (self = [super init]) {
        [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
            NSLog(@"ViewController will appear:--> %@", NSStringFromClass([aspectInfo.instance class]));
        } error:NULL];
    }
}
@end

Disadvantage

There are some disadvantages at Aspects.

  • Perfermance loss

    I have memtioned that above paragraph.

  • Can't hook class method

    If you hook a class method, the block won't callback and you'll get failure said Aspects: Block signature doesn't match (null). The reason is that runtime just obtain member method list, if you want to get class method, you must get it from MetaClass of this class. You can get MetaClass by object_getClass(newClass).

    Fortunately, there is a gay coding a function for class method hooking:

    static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);
    //TODO:Edit by JackYong
    Method targetMethod;
    IMP targetMethodIMP;
    if (class_isMetaClass(klass)) {
    targetMethod = class_getClassMethod(klass, selector);
    targetMethodIMP = method_getImplementation(targetMethod);
    } else {
    targetMethod = class_getInstanceMethod(klass, selector);
    targetMethodIMP = method_getImplementation(targetMethod);
    }

How to check if a Value is valid in NSArray or NSDictionary?

I often encounter a problem in my work, that the value from json of server response is not match client’s data structure, in paticular PHP server. The result of this problem is application frequently crashing. So in order to solve this problem, I designed a serises of ways to try.

1. Checking Validity Function

I create a Class to check if a value is valid. It contains many functions that check with argument of value:

#import <Foundation/Foundation.h>

@interface CheckValidObject : NSObject

BOOL isValidValue(id object);
BOOL isValidObject(id object, Class aClass);
BOOL isValidNSDictionary(id object);
BOOL isValidNSArray(id object);
BOOL isValidNSString(id object);
BOOL isValidNSURL(id object);
BOOL isValidNSNumber(id object);

id getValidObjectFromArray(NSArray *array, NSInteger index);
id getValidObjectFromDictionary(NSDictionary *dic, NSString *key);

void setValidObjectForDictionary(NSMutableDictionary *dic, NSString*key, id value);
void addValidObjectForArray(NSMutableArray *array, id value);
void addValidArrayForArray(NSMutableArray *array, NSArray *value);
void replaceValidObjectForArray(NSMutableArray *array, NSInteger index, id value);

void removeValidObjectFromArray(NSMutableArray *array, NSInteger index);
@end


@implementation CheckValidObject
BOOL isValidValue(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null])
    {
        return YES;
    }
    return NO;
}

BOOL isValidObject(id object, Class aClass)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && [object isKindOfClass:aClass])
    {
        return YES;
    }
    return NO;
}

BOOL isValidNSDictionary(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && ([object isKindOfClass:[NSDictionary class]]||[object isKindOfClass:[NSMutableDictionary class]]))
    {
        return ((NSDictionary*)object).allKeys.count>0?YES:NO;
    }
    return NO;
}

BOOL isValidNSArray(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && [object isKindOfClass:[NSArray class]])
    {
        return ((NSArray*)object).count>0?YES:NO;
    }
    return NO;
}

BOOL isValidNSString(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && [object isKindOfClass:[NSString class]])
    {
        return ((NSString*)object).length>0?YES:NO;
    }
    return NO;
}

BOOL isValidNSURL(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && [object isKindOfClass:[NSURL class]])
    {
        return isValidNSString([object absoluteString])?YES:NO;
    }
    return NO;
}

BOOL isValidNSNumber(id object)
{
    if (object!=nil && (NSNull *)object != [NSNull null] && [object isKindOfClass:[NSNumber class]] && ![((NSNumber*)object) isEqualToNumber:[NSDecimalNumber notANumber]])
    {
        return YES;
    }
    return NO;
}

id getValidObjectFromArray(NSArray *array, NSInteger index)
{
    if (isValidNSArray(array) && index<array.count) {
        return array[index];
    }
    return nil;
}

void removeValidObjectFromArray(NSMutableArray *array, NSInteger index)
{
    if (isValidNSArray(array) && index<array.count) {
        [array removeObjectAtIndex:index];
    }
}

id getValidObjectFromDictionary(NSDictionary *dic, NSString *key)
{
    if (isValidNSDictionary(dic) && isValidNSString(key)) {
        return dic[key];
    }
    return nil;
}

void setValidObjectForDictionary(NSMutableDictionary *dic, NSString*key, id value)
{
    if(value && isValidNSString(key))
    {
        [dic setValue:value forKey:key];
    }
}

void addValidObjectForArray(NSMutableArray *array, id value)
{
    if (array!=nil && (NSNull *)array != [NSNull null] && [array isKindOfClass:[NSArray class]])
    {
        if (value) {
            [array addObject:value];
        }
    }
}

void addValidArrayForArray(NSMutableArray *array, NSArray *value)
{
    if (array!=nil && (NSNull *)array != [NSNull null] && [array isKindOfClass:[NSArray class]])
    {
        if (isValidNSArray(value)) {
            [array addObjectsFromArray:value];
        }
    }
}

void replaceValidObjectForArray(NSMutableArray *array, NSInteger index, id value)
{
    if ((array!=nil && (NSNull *)array != [NSNull null] && [array isKindOfClass:[NSArray class]]) && value && index<array.count) {
        [array replaceObjectAtIndex:index withObject:value];
    }
}
@end


When we invoke them, such as setting an object in a dictionary, we write this line :

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
setValidObjectForDictionary(dict,@"name",nil);

Cause the value is nil, the key-value can’t be setted in the dict.

But this way has a defect, the function does not belong NSDictionary or NSArray. Perhaps adding some method in NSDictionary and NSArray is a better project.

2. Category

Using category is not a bad choice to adding some checking valid method in NSDictionary and NSArray. From the defination of categroy in runtime, we can learn class method, instance method, implementation of protocol and proporty are allowed to be added.

typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;

I created a category of NSDictionary for getting value called NSDictionary+TypeCast. I added different method for different value:

@interface NSDictionary (TypeCast)

- (BOOL)yxt_hasKey:(NSString *)key;

- (id)yxt_objectForKey:(NSString *)key __attribute__((deprecated));

- (id)yxt_unknownObjectForKey:(NSString*)key;

- (id)yxt_objectForKey:(NSString *)key class:(Class)clazz;

- (NSNumber *)yxt_numberForKey:(NSString *)key defaultValue:(NSNumber *)defaultValue;

- (NSNumber *)yxt_numberForKey:(NSString *)key;

- (NSString *)yxt_stringForKey:(NSString *)key defaultValue:(NSString *)defaultValue;

- (NSString *)yxt_stringForKey:(NSString *)key;

- (NSString*)yxt_validStringForKey:(NSString*)key;

- (NSArray *)yxt_stringArrayForKey:(NSString *)key defaultValue:(NSArray *)defaultValue;

- (NSArray *)yxt_stringArrayForKey:(NSString *)key;

- (NSDictionary *)yxt_dictForKey:(NSString *)key defaultValue:(NSDictionary *)defaultValue;

- (NSDictionary *)yxt_dictForKey:(NSString *)key;

- (NSDictionary *)yxt_validDictForKey:(NSString *)key;

- (NSDictionary *)yxt_dictionaryWithValuesForKeys:(NSArray *)keys;

- (NSArray *)yxt_arrayForKey:(NSString *)key defaultValue:(NSArray *)defaultValue;

- (NSArray *)yxt_arrayForKey:(NSString *)key;

- (NSArray *)yxt_validArrayForKey:(NSString*)key;

- (float)yxt_floatForKey:(NSString *)key defaultValue:(float)defaultValue;

- (float)yxt_floatForKey:(NSString *)key;

- (double)yxt_doubleForKey:(NSString *)key defaultValue:(double)defaultValue;

- (double)yxt_doubleForKey:(NSString *)key;

- (BOOL)yxt_boolForKey:(NSString *)key defaultValue:(BOOL)defaultValue;

- (BOOL)yxt_boolForKey:(NSString *)key;

- (int)yxt_intForKey:(NSString *)key defaultValue:(int)defaultValue;

- (int)yxt_intForKey:(NSString *)key;

- (void)yxt_enumerateKeysAndUnknownObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block;

- (void)yxt_enumerateKeysAndArrayObjectsUsingBlock:(void (^)(id key, NSArray *obj, BOOL *stop))block;

- (void)yxt_enumerateKeysAndDictObjectsUsingBlock:(void (^)(id key, NSDictionary *obj, BOOL *stop))block;

- (void)yxt_enumerateKeysAndStringObjectsUsingBlock:(void (^)(id key, NSString *obj, BOOL *stop))block;

Like this, developers can use these method as NSDictionary’s method. I enumerated some possible types in above code. One of the things, we should pay attention to is we’d better to add unify prefix to method name, like yxt_. This can avoid different categories of same class containing same name method to cover each other. And the implementation is :

**
 *  return value mapping with given key from current dictionary.
 */
#define OFK [self objectForKey:key]

@implementation NSDictionary (TypeCast)


- (BOOL)yxt_hasKey:(NSString *)key
{
    return (OFK != nil);
}

#pragma mark - NSObject

- (id)yxt_objectForKey:(NSString *)key
{
    return OFK;
}

- (id)yxt_unknownObjectForKey:(NSString*)key
{
    return OFK;
}


- (id)yxt_objectForKey:(NSString *)key class:(Class)clazz
{
    id obj = OFK;
    if ([obj isKindOfClass:clazz])
    {
        return obj;
    }

    return nil;
}

#pragma mark - NSNumber

- (NSNumber *)yxt_numberForKey:(NSString *)key defaultValue:(NSNumber *)defaultValue
{
    return yxt_numberOfValue(OFK, defaultValue);
}

- (NSNumber *)yxt_numberForKey:(NSString *)key
{
    return [self yxt_numberForKey:key defaultValue:nil];
}

#pragma mark - NSString

- (NSString *)yxt_stringForKey:(NSString *)key defaultValue:(NSString *)defaultValue;
{
    return yxt_stringOfValue(OFK, defaultValue);
}

- (NSString *)yxt_stringForKey:(NSString *)key;
{
    return [self yxt_stringForKey:key defaultValue:nil];
}


- (NSString *)yxt_validStringForKey:(NSString *)key
{
    NSString *stringValue = [self yxt_stringForKey:key];
    if (stringValue.length) {
        return stringValue;
    }
    return nil;
}

#pragma mark - NSArray of NSString

- (NSArray *)yxt_stringArrayForKey:(NSString *)key defaultValue:(NSArray *)defaultValue
{
    return yxt_stringArrayOfValue(OFK, defaultValue);
}

- (NSArray *)yxt_stringArrayForKey:(NSString *)key;
{
    return [self yxt_stringArrayForKey:key defaultValue:nil];
}

#pragma mark - NSDictionary

- (NSDictionary *)yxt_dictForKey:(NSString *)key defaultValue:(NSDictionary *)defaultValue
{
    return yxt_dictOfValue(OFK, defaultValue);
}

- (NSDictionary *)yxt_dictForKey:(NSString *)key
{
    return [self yxt_dictForKey:key defaultValue:nil];
}

- (NSDictionary *)yxt_validDictForKey:(NSString *)key
{
    NSDictionary *dictionary = [self yxt_dictForKey:key];
    if (dictionary.count) {
        return dictionary;
    }
    return nil;
}

- (NSDictionary *)yxt_dictionaryWithValuesForKeys:(NSArray *)keys
{
    return [self dictionaryWithValuesForKeys:keys];
}

#pragma mark - NSArray

- (NSArray *)yxt_arrayForKey:(NSString *)key defaultValue:(NSArray *)defaultValue
{
    return yxt_arrayOfValue(OFK, defaultValue);
}

- (NSArray *)yxt_arrayForKey:(NSString *)key
{
    return [self yxt_arrayForKey:key defaultValue:nil];
}

-(NSArray *)yxt_validArrayForKey:(NSString *)key
{
    NSArray *array = [self yxt_arrayForKey:key];
    if (array.count) {
        return array;
    }
    return nil;
}

#pragma mark - Float

- (float)yxt_floatForKey:(NSString *)key defaultValue:(float)defaultValue;
{
    return yxt_floatOfValue(OFK, defaultValue);
}

- (float)yxt_floatForKey:(NSString *)key;
{
    return [self yxt_floatForKey:key defaultValue:0.0f];
}

#pragma mark - Double

- (double)yxt_doubleForKey:(NSString *)key defaultValue:(double)defaultValue;
{
    return yxt_doubleOfValue(OFK, defaultValue);
}

- (double)yxt_doubleForKey:(NSString *)key;
{
    return [self yxt_doubleForKey:key defaultValue:0.0];
}

#pragma mark - BOOL

- (BOOL)yxt_boolForKey:(NSString *)key defaultValue:(BOOL)defaultValue;
{
    return yxt_boolOfValue(OFK, defaultValue);
}

- (BOOL)yxt_boolForKey:(NSString *)key;
{
    return [self yxt_boolForKey:key defaultValue:NO];
}

#pragma mark - Int

- (int)yxt_intForKey:(NSString *)key defaultValue:(int)defaultValue;
{
    return yxt_intOfValue(OFK, defaultValue);
}

- (int)yxt_intForKey:(NSString *)key;
{
    return [self yxt_intForKey:key defaultValue:0];
}

#pragma mark - Enumerate

- (void)yxt_enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
    [self enumerateKeysAndObjectsUsingBlock:block];
}

- (void)yxt_enumerateKeysAndUnknownObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
    [self enumerateKeysAndObjectsUsingBlock:block];
}

- (void)yxt_enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block withCastFunction:(id (*)(id, id))castFunction
{
    [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        id castedObj = castFunction(obj, nil);
        if (castedObj)
        {
            block(key, castedObj, stop);
        }
    }];
}

- (void)yxt_enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block classes:(id)object, ...
{
    if (!object) return;
    NSMutableArray* classesArray = [NSMutableArray array];
    id paraObj = object;
    va_list objects;
    va_start(objects, object);
    do
    {
        [classesArray addObject:paraObj];
        paraObj = va_arg(objects, id);
    } while (paraObj);
    va_end(objects);

    [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        BOOL allowBlock = NO;
        for (int i = 0; i < classesArray.count; i++)
        {
            if ([obj isKindOfClass:[classesArray objectAtIndex:i]])
            {
                allowBlock = YES;
                break;
            }
        }
        if (allowBlock)
        {
            block(key, obj, stop);
        }
    }];
}

- (void)yxt_enumerateKeysAndArrayObjectsUsingBlock:(void (^)(id key, NSArray *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsUsingBlock:block withCastFunction:yxt_arrayOfValue];
}

- (void)yxt_enumerateKeysAndDictObjectsUsingBlock:(void (^)(id key, NSDictionary *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsUsingBlock:block withCastFunction:yxt_dictOfValue];
}

- (void)yxt_enumerateKeysAndStringObjectsUsingBlock:(void (^)(id key, NSString *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsUsingBlock:block withCastFunction:yxt_stringOfValue];
}

- (void)yxt_enumerateKeysAndNumberObjectsUsingBlock:(void (^)(id key, NSNumber *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsUsingBlock:block withCastFunction:yxt_numberOfValue];
}

#pragma mark - Enumerate with Options

- (void)yxt_enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
    [self enumerateKeysAndObjectsWithOptions:opts usingBlock:block];
}

- (void)yxt_enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, id obj, BOOL *stop))block withCastFunction:(id (*)(id, id))castFunction
{
    [self enumerateKeysAndObjectsWithOptions:opts usingBlock:^(id key, id obj, BOOL *stop) {
        id castedObj = castFunction(obj, nil);
        if (castedObj)
        {
            block(key, castedObj, stop);
        }
    }];
}

- (void)yxt_enumerateKeysAndArrayObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, NSArray *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsWithOptions:opts usingBlock:block withCastFunction:yxt_arrayOfValue];
}

- (void)yxt_enumerateKeysAndDictObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, NSDictionary *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsWithOptions:opts usingBlock:block withCastFunction:yxt_dictOfValue];
}

- (void)yxt_enumerateKeysAndStringObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, NSString *obj, BOOL *stop))block
{
    [self yxt_enumerateKeysAndObjectsWithOptions:opts usingBlock:block withCastFunction:yxt_stringOfValue];
}

@end

If we put these code in subproject of Cocoapods, the header will be globally declared. So we don’t need to include the .h in every class.

But as an architect I soon noticed that some developers don’t know the category’s existence. They still used the methods that Objective-C provides. Therefore I must make some limitations.

3. Making some Limitations

Last post I told about the core of method, you should know
if a method isn’t found in class, forwardingTargetForSelector will forward another instance to try to invoke the method. I can use this specify to avoid the methods of NSDictionary & NSArray that Objective-C provides.

#import "ResponseObjectProxy.h"

@interface ResponseObjectProxy()

@property (nonatomic, strong) NSObject *responseObject;
+ (id)proxyWithResponseObject:(id)anObject;
@end

@interface ResponseArrayProxy : ResponseObjectProxy

@end

@interface ResponseDictionaryProxy : ResponseObjectProxy

@end


@implementation ResponseObjectProxy

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.responseObject;
}

+ (id)proxyWithResponseObject:(id)anObject
{
    ResponseObjectProxy *proxy = [[self alloc] init];
    proxy.responseObject = anObject;
    return proxy;
}

+ (id)responseObjectWithObject:(id)obj
{
    #ifndef __OPTIMIZE__
       if ([obj isKindOfClass:NSDictionary.class])
       {
           NSMutableDictionary *dict = [NSMutableDictionary dictionary];
           [(NSDictionary *)obj enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
               [dict setObject:[self responseObjectWithObject:obj] forKey:key];
           }];
           return [ResponseDictionaryProxy proxyWithResponseObject:dict];
       }
       else if ([obj isKindOfClass:NSArray.class])
       {
           NSMutableArray *array = [NSMutableArray array];
           [(NSArray *)obj enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
               [array addObject:[self responseObjectWithObject:obj]];
           }];
           return [ResponseArrayProxy proxyWithResponseObject:array];
       }
    #endif
    return obj;
}

+ (id)objectWithResponseObject:(id)responseObj
{
    #ifndef __OPTIMIZE__
        if ([responseObj isKindOfClass:NSDictionary.class])
        {
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            [responseObj enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                [dict setObject:[self objectWithResponseObject:obj] forKey:obj];
            }];
            return dict;
        }
        else if ([responseObj isKindOfClass:NSArray.class])
        {
            NSMutableArray *array = [NSMutableArray array];
            [responseObj enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                [array addObject:[self objectWithResponseObject:obj]];
            }];
            return array;
        }
    #endif
    return responseObj;
}

- (id)throwException
{
    NSAssert(0, @"don't use this method");
    return nil;
}

@end

@implementation ResponseArrayProxy

- (id)objectAtIndex:(NSUInteger)index
{
    return [self throwException];
}

- (id)objectAtIndexedSubscript:(NSUInteger)idx
{
    return [self throwException];
}

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block
{
    [self throwException];
}

- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block
{
    [self throwException];
}

@end


@implementation ResponseDictionaryProxy

- (id)objectForKeyedSubscript:(id)key
{
    return [self throwException];
}

- (id)objectForKey:(id)aKey
{
    return [self throwException];
}

- (id)valueForKey:(NSString *)key
{
    return [self throwException];
}

- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
    [self throwException];
}

- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
    [self throwException];
}

@end

When the program recieves the response data, use the +responseObjectWithObject: method to make a proxy for dictionary or array. The developers will think the proxy is response object, but they are wrong. If they use methods that Objective-C provides, the proxy’s small name methods will be invoked and throw an assert. And the method not contained in the proxy, like category’s methods could be invoke normally. This way can limit develops to still use unsafe method.