IOS Faceted TableView-AOPTableView

This is an open source project of the company long ago, written by a big bull, which has been used in the project. Today, I have some time to send it to see how to realize it. After reading it, I feel quite fruitful, so I write this article and share it with students who need it. The library open source address: MeetYouDevs/IMYAOPTableView

An overview of

WHY AOP TableView

About why using AOP in MeetYouDevs/IMYAOPTableView this library have been mentioned in the introduction to the, mainly for access to advertising in our data flow of this kind of scenario, the most primitive way is to request data and advertising, according to the rules of the merged data, The process of displaying business data and advertising data is shown in the figure below. The disadvantage of this scheme is that there is obvious coupling. Advertising and normal business are coupled together, but it also violates the principle of single responsibility in the design principle. Therefore, this way is not elegant enough, and the maintenance cost in the later stage is also relatively large.

 


So how to solve this problem? How do you elegantly solve this problem in a non-intrusive way? The answer is to use AOP to allow normal business and advertising to be processed in parallel and independently, as shown in the figure below


HOW DESIGN AOP TableView

How do you design an AOP-usable TableView? One of the points mentioned in the design is that there is no problem that can’t be solved by adding a layer. If not, add another layer! . AOP TableView also exists this processing layer, undertake the following responsibilities: 1, inject non-business advertising content; 2. Forwarding different business to different processors; 3. Deal with the conversion relationship between display, business and advertising; There are also some auxiliary methods.

The following figure is AOPTableView design class diagram, IMYAOPTableViewUtils class is this layer, in order to conform to the principle of single responsibility in the design, through classification, the function of this class is split into multiple different modules, For example, IMYAOPTableViewUtils (UITableViewDelegate) handles delegate forwarding, IMYAOPTableViewUtils (UITableViewDataSource) handles dataSource forwarding, Mainly completed the following transactions

  • Inject the corresponding position of the AD content
  • Set up the AOP
  • As the real Delegate/DataSource of the TableView
  • Handles forwarding Delegate/DataSource methods to business or ads
  • Handle delegate forwarding ->IMYAOPTableViewUtils (UITableViewDelegate)
  • IMYAOPTableViewUtils (UITableViewDataSource)


Set up the AOP


After creating the IMYAOPTableViewUtils object, you need to inject the AOP Class. The main steps are as follows:

  • Save the Delegate/DataSource ->injectTableView method for the business
  • Set the TableView delegate/dataSource to IMYAOPBaseUtils -> injectFeedsView
  • Dynamically create a subclass of TableView -> makeSubclassWithClass
  • And set the business to the TableView isa pointer -> bindingFeedsView method to handle
  • Set the aop method -> setupAopClass method to dynamically create a subclass of TableView

In particular: dynamically create subclasses and add AOP methods to dynamically create subclasses. The final processing methods for that subtype will be in the _IMYAOPTableView class

- (void)injectTableView {
    UITableView *tableView = self.tableView;

    _origDataSource = tableView.dataSource;
    _origDelegate = tableView.delegate;

    [self injectFeedsView:tableView];
}


#pragma mark - Infuses AOP classes

- (void)injectFeedsView:(UIView *)feedsView {
    // Set the TableView's delegate to IMYAOPBaseUtils
    // Set the TableView dataSource to IMYAOPBaseUtils
    struct objc_super objcSuper = {.super_class = [self msgSendSuperClass], .receiver = feedsView};
    ((void(*) (void *, SEL, id(a))void *)objc_msgSendSuper)(&objcSuper, @selector(setDelegate:), self);
    ((void(*) (void *, SEL, id(a))void *)objc_msgSendSuper)(&objcSuper, @selector(setDataSource:), self);
    
    self.origViewClass = [feedsView class];
    // Dynamically subclass TableView
    Class aopClass = [self makeSubclassWithClass:self.origViewClass];
    if(! [self.origViewClass isSubclassOfClass:aopClass]) {
        // isa-swizzle: set the ISA pointer to the TableView subclass
        [selfbindingFeedsView:feedsView aopClass:aopClass]; }}/** isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle: isa-swizzle
- (void)bindingFeedsView:(UIView *)feedsView aopClass:(Class)aopClass {
    id observationInfo = [feedsView observationInfo];
    NSArray *observanceArray = [observationInfo valueForKey:@"_observances"];
    // remove the old KVO
    for (id observance in observanceArray) {
        NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
        id observer = [observance valueForKey:@"_observer"];
        if (keyPath && observer) {
            [feedsView removeObserver:observer forKeyPath:keyPath];
        }
    }
    object_setClass(feedsView, aopClass);
    /// Add a new KVO
    for (id observance in observanceArray) {
        NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
        id observer = [observance valueForKey:@"_observer"];
        if (observer && keyPath) {
            void *context = NULL;
            NSUInteger options = 0;
            @try {
                Ivar _civar = class_getInstanceVariable([observance class]."_context");
                if (_civar) {
                    context = ((void* (*) (id, Ivar))(void *)object_getIvar)(observance, _civar);
                }
                Ivar _oivar = class_getInstanceVariable([observance class]."_options");
                if (_oivar) {
                    options = ((NSUInteger(*) (id, Ivar))(void *)object_getIvar)(observance, _oivar);
                }
                /// For some reason, iOS11 returns a value filled with 8 bytes. 128
                if (options >= 128) {
                    options -= 128; }}@catch (NSException *exception) {
                IMYLog(@ "% @", exception.debugDescription);
            }
            if (options == 0) {
                options = (NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew); } [feedsView addObserver:observer forKeyPath:keyPath options:options context:context]; }}}#pragma mark - install aop method
/** Dynamically subclass TableView */
- (Class)makeSubclassWithClass:(Class)origClass {
    NSString *className = NSStringFromClass(origClass);
    NSString *aopClassName = [kAOPFeedsViewPrefix stringByAppendingString:className];
    Class aopClass = NSClassFromString(aopClassName);

    if (aopClass) {
        return aopClass;
    }
    aopClass = objc_allocateClassPair(origClass, aopClassName.UTF8String, 0);

    // Set the aop method of the dynamically created subclass. The real processing method is the aop_ prefix method in the _IMYAOPTableView class
    [self setupAopClass:aopClass];

    objc_registerClassPair(aopClass);
    return aopClass;
}

/** Sets the AOP method for dynamically created subclasses, leaving out */
- (void)setupAopClass:(Class)aopClass {
    /// Pure manual knock
    [self addOverriteMethod:@selector(class) aopClass:aopClass];
    [self addOverriteMethod:@selector(setDelegate:) aopClass:aopClass];
    / /...
    
    ///UI Calling
    [self addOverriteMethod:@selector(reloadData) aopClass:aopClass];
    [self addOverriteMethod:@selector(layoutSubviews) aopClass:aopClass];
    [self addOverriteMethod:@selector(setBounds:) aopClass:aopClass];
    / /...
    ///add real reload function
    [self addOverriteMethod:@selector(aop_refreshDataSource) aopClass:aopClass];
    [self addOverriteMethod:@selector(aop_refreshDelegate) aopClass:aopClass];
    / /...

    // Info
    [self addOverriteMethod:@selector(numberOfSections) aopClass:aopClass];
    [self addOverriteMethod:@selector(numberOfRowsInSection:) aopClass:aopClass];
    / /...

    // Row insertion/deletion/reloading.
    [self addOverriteMethod:@selector(insertSections:withRowAnimation:) aopClass:aopClass];
    [self addOverriteMethod:@selector(deleteSections:withRowAnimation:) aopClass:aopClass];
    / /...

    // Selection
    [self addOverriteMethod:@selector(indexPathForSelectedRow) aopClass:aopClass];
    [self addOverriteMethod:@selector(indexPathsForSelectedRows) aopClass:aopClass];
    / /...

    // Appearance
    [self addOverriteMethod:@selector(dequeueReusableCellWithIdentifier:forIndexPath:) aopClass:aopClass];
}

- (void)addOverriteMethod:(SEL)seletor aopClass:(Class)aopClass {
    NSString *seletorString = NSStringFromSelector(seletor);
    NSString *aopSeletorString = [NSString stringWithFormat:@"aop_%@", seletorString];
    SEL aopMethod = NSSelectorFromString(aopSeletorString);
    [self addOverriteMethod:seletor toMethod:aopMethod aopClass:aopClass];
}

- (void)addOverriteMethod:(SEL)seletor toMethod:(SEL)toSeletor aopClass:(Class)aopClass {
    // The implClass here is _IMYAOPTableView in AOPTableViewUtils
    Class implClass = [self implAopViewClass];
    Method method = class_getInstanceMethod(implClass, toSeletor);
    if (method == NULL) {
        method = class_getInstanceMethod(implClass, seletor);
    }
    const char *types = method_getTypeEncoding(method);
    IMP imp = method_getImplementation(method);
    // Add aopClass which is the method to create the subtype kIMYAOP_UITableView. The actual method is in the _IMYAOPTableView class
    class_addMethod(aopClass, seletor, imp, types);
}
Copy the code

_IMYAOPTableView’s job is to convert the business rules into real list rules when the business side directly uses the TableView’s corresponding methods. For example, the following business side calls cellForRowAtIndexPath and goes to the following method: In this case, indexPath is the business’s own indexPath, such as the fifth position visible in the list, but there are two ads in front of it. In the logic of the business side, the indexPath corresponds to the third position, so it needs to be corrected to return the correct indexPath. Get the corresponding Cell so that there is no problem

- (UITableViewCell *)aop_cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    AopDefineVars;
    if (aop_utils) {
        // Fix the indexPath used by the business to be the real indexPath
        indexPath = [aop_utils feedsIndexPathByUser:indexPath];
    }
    aop_utils.isUICalling += 1;
    UITableViewCell *cell = AopCallSuperResult_1(@selector(cellForRowAtIndexPath:), indexPath);
    aop_utils.isUICalling -= 1;
    return cell;
}
Copy the code

Using AOP

Non-business data insertion

The IMYAOPBaseUtils class provides two methods for processing non-business data

// insert sections and indexPaths
- (void)insertWithSections:(nullable NSArray<__kindof IMYAOPBaseInsertBody *> *)sections;
- (void)insertWithIndexPaths:(nullable NSArray<__kindof IMYAOPBaseInsertBody *> *)indexPaths;

/ / implementation
- (void)insertWithIndexPaths:(NSArray<IMYAOPBaseInsertBody *> *)indexPaths {
    NSArray<IMYAOPBaseInsertBody *> *array = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(IMYAOPBaseInsertBody *_Nonnull obj1, IMYAOPBaseInsertBody *_Nonnull obj2) {
        return [obj1.indexPath compare:obj2.indexPath];
    }];

    NSMutableDictionary *insertMap = [NSMutableDictionary dictionary];
    [array enumerateObjectsUsingBlock:^(IMYAOPBaseInsertBody *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        NSInteger section = obj.indexPath.section;
        NSInteger row = obj.indexPath.row;
        NSMutableArray *rowArray = insertMap[@(section)];
        if(! rowArray) { rowArray = [NSMutableArray array];
            [insertMap setObject:rowArray forKey:@(section)];
        }
        while (YES) {
            BOOL hasEqual = NO;
            for (NSIndexPath *inserted in rowArray) {
                if (inserted.row == row) {
                    row++;
                    hasEqual = YES;
                    break; }}if (hasEqual == NO) {
                break; }}NSIndexPath *insertPath = [NSIndexPath indexPathForRow:row inSection:section];
        [rowArray addObject:insertPath];
        obj.resultIndexPath = insertPath;
    }];
    self.sectionMap = insertMap;
}
Copy the code

The insertWithIndexPaths call inserts non-business advertising data, where the inserted data is location

/// simple rows inserts
- (void)insertRows {
    NSMutableArray<IMYAOPTableViewInsertBody *> *insertBodys = [NSMutableArray array];
    /// randomly generated 5 positions to insert
    for (int i = 0; i < 5; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:arc4random() % 10 inSection:0];
        [insertBodys addObject:[IMYAOPTableViewInsertBody insertBodyWithIndexPath:indexPath]];
    }
    /// Empty old data
    [self.aopUtils insertWithSections:nil];
    [self.aopUtils insertWithIndexPaths:nil];

    // insert new data. The same row is incremented in the order of the array
    [self.aopUtils insertWithIndexPaths:insertBodys];

    // call reloadData of tableView to refresh the page
    [self.aopUtils.tableView reloadData];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@ "% @".self.aopUtils.allModels);
    });
}
Copy the code

The above code calls are used in the demo. SectionMap stores the following data: Key is section, value is the IndexPath array of all inserted data under the corresponding section. SectionMap data is used to process the mapping between real data and business data

The userIndexPathByFeeds method uses sectionMap to handle transformations between real indexPath and business indexPath

If the real indexPath is (0-5) and two ads are inserted in front of it, the indexPath will be fixed to the business indexPath, i.e. (0-3). If the position is the advertising position, So return nil
- (NSIndexPath *)userIndexPathByFeeds:(NSIndexPath *)feedsIndexPath {
    if(! feedsIndexPath) {return nil;
    }
    NSInteger section = feedsIndexPath.section;
    NSInteger row = feedsIndexPath.row;

    NSMutableArray<NSIndexPath *> *array = self.sectionMap[@(section)];
    NSInteger cutCount = 0;
    for (NSIndexPath *obj in array) {
        if (obj.row == row) {
            cutCount = - 1;
            break;
        }
        if (obj.row < row) {
            cutCount++;
        } else {
            break; }}if (cutCount < 0) {
        return nil;
    }
    /// If the position is not an advertisement, it is converted to a logical index
    section = [self userSectionByFeeds:section];
    NSIndexPath *userIndexPath = [NSIndexPath indexPathForRow:row - cutCount inSection:section];
    return userIndexPath;
}
Copy the code

The AOP proxy method callback


As shown in the figure above, IMYAOPTableViewUtils acts as the delegate and dataSource of the TableView as the middle layer, and handles the forwarding of the corresponding event to the specific handler in the modified class: the business side or the non-business advertising side

Such as the following for the cell agent method tableView: cellForRowAtIndexPath:, indexPath fixes will first, and then judge is a business and non-business, then use different dataSource to carry on the corresponding processing, code has made a comment, Details are included in the explanatory notes

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    kAOPUICallingSaved;
    kAOPUserIndexPathCode;
    UITableViewCell *cell = nil;
    if ([dataSource respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) {
        cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
    }
    if(! [cell isKindOfClass:[UITableViewCell class]]) {
        cell = [UITableViewCell new];
        if (dataSource) {
            NSAssert(NO.@"Cell is Nil");
        }
    }
    kAOPUICallingResotre;
    return cell;
}

// macro definition of the code segment, the user is to determine whether the position is used by the business IndexPath, if yes return business DataSource->origDataSource, otherwise return non-business DataSource-> DataSource
#define kAOPUserIndexPathCode \
    NSIndexPath *userIndexPath = [self userIndexPathByFeeds:indexPath]; \
    id<IMYAOPTableViewDataSource> dataSource = nil;                     \
    if (userIndexPath) {                                                \
        dataSource = (id)self.origDataSource; \ indexPath = userIndexPath; The \}else {                                                            \
        dataSource = self.dataSource;                                   \
        isInjectAction = YES; \} \if (isInjectAction) {                                               \
        self.isUICalling += 1; The \}If the real indexPath is (0-5) and two ads are inserted in front of it, the indexPath will be fixed to the business indexPath, i.e. (0-3). If the position is the advertising position, So return nil
- (NSIndexPath *)userIndexPathByFeeds:(NSIndexPath *)feedsIndexPath {
    if(! feedsIndexPath) {return nil;
    }
    NSInteger section = feedsIndexPath.section;
    NSInteger row = feedsIndexPath.row;

    NSMutableArray<NSIndexPath *> *array = self.sectionMap[@(section)];
    NSInteger cutCount = 0;
    for (NSIndexPath *obj in array) {
        if (obj.row == row) {
            cutCount = - 1;
            break;
        }
        if (obj.row < row) {
            cutCount++;
        } else {
            break; }}if (cutCount < 0) {
        return nil;
    }
    /// If the position is not an advertisement, it is converted to a logical index
    section = [self userSectionByFeeds:section];
    NSIndexPath *userIndexPath = [NSIndexPath indexPathForRow:row - cutCount inSection:section];
    return userIndexPath;
}
Copy the code

The end of the

Write this first, if improper place please comment