iOS: implement horizontal list

This commit is contained in:
王劲鹏 2022-08-24 18:07:22 +08:00 committed by osborn
parent 87501e0eb3
commit 02259823da
5 changed files with 637 additions and 0 deletions

View File

@ -31,6 +31,8 @@
#import "DoricImageNode.h"
#import "DoricListNode.h"
#import "DoricListItemNode.h"
#import "DoricHorizontalListNode.h"
#import "DoricHorizontalListItemNode.h"
#import "DoricScrollerNode.h"
#import "DoricSliderNode.h"
#import "DoricSlideItemNode.h"
@ -143,6 +145,8 @@ - (void)innerRegister {
[self registerViewNode:DoricGestureContainerNode.class withName:@"GestureContainer"];
[self registerViewNode:DoricBlurEffectViewNode.class withName:@"BlurEffect"];
[self registerViewNode:DoricAeroEffectViewNode.class withName:@"AeroEffect"];
[self registerViewNode:DoricHorizontalListItemNode.class withName:@"HorizontalListItem"];
[self registerViewNode:DoricHorizontalListNode.class withName:@"HorizontalList"];
[self.loaderManager registerLoader:[[DoricBundleResourceLoader alloc]
initWithResourceType:@"mainBundle"

View File

@ -0,0 +1,25 @@
/*
* Copyright [2022] [Doric.Pub]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Created by jingpeng.wang on 2022/8/23.
//
#import <Foundation/Foundation.h>
#import "DoricStackNode.h"
@interface DoricHorizontalListItemNode : DoricStackNode
@end

View File

@ -0,0 +1,44 @@
/*
* Copyright [2022] [Doric.Pub]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Created by jingpeng.wang on 2022/8/23.
//
#import "DoricHorizontalListItemNode.h"
@interface DoricHorizontalListItemNode ()
@end
@implementation DoricHorizontalListItemNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
self.reusable = YES;
}
return self;
}
- (void)initWithSuperNode:(DoricSuperNode *)superNode {
[super initWithSuperNode:superNode];
self.reusable = YES;
}
- (void)blendView:(UIView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"identifier" isEqualToString:name] || [@"actions" isEqualToString:name]) {
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
@end

View File

@ -0,0 +1,25 @@
/*
* Copyright [2022] [Doric.Pub]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Created by jingpeng.wang on 2022/8/23.
//
#import <Foundation/Foundation.h>
#import "DoricSuperNode.h"
#import "DoricScrollableProtocol.h"
@interface DoricHorizontalListNode : DoricSuperNode<UICollectionView *> <DoricScrollableProtocol>
@end

View File

@ -0,0 +1,539 @@
/*
* Copyright [2022] [Doric.Pub]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Created by jingpeng.wang on 2022/8/23.
//
#import <JavaScriptCore/JavaScriptCore.h>
#import <DoricCore/Doric.h>
#import "DoricHorizontalListNode.h"
#import "DoricExtensions.h"
#import "DoricHorizontalListItemNode.h"
#import "DoricRefreshableNode.h"
#import "DoricJSDispatcher.h"
#import "DoricUtil.h"
#import "DoricExtensions.h"
@interface DoricHorizontalTableViewCell : UICollectionViewCell
@property(nonatomic, strong) DoricHorizontalListItemNode *doricHorizontalListItemNode;
@end
@implementation DoricHorizontalTableViewCell
@end
@interface DoricHorizontalTableView : UICollectionView
@end
@implementation DoricHorizontalTableView
- (CGSize)sizeThatFits:(CGSize)size {
return [super sizeThatFits:size];
}
@end
@interface DoricHorizontalListNode () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, NSString *> *itemViewIds;
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, NSNumber *> *itemWidths;
@property(nonatomic, strong) NSMutableDictionary <NSNumber *, NSArray *> *itemActions;
@property(nonatomic, assign) NSUInteger itemCount;
@property(nonatomic, assign) NSUInteger batchCount;
@property(nonatomic, copy) NSString *onLoadMoreFuncId;
@property(nonatomic, copy) NSString *renderItemFuncId;
@property(nonatomic, copy) NSString *loadMoreViewId;
@property(nonatomic, assign) BOOL loadMore;
@property(nonatomic, assign) NSInteger loadAnchor;
@property(nonatomic, strong) NSMutableSet <DoricDidScrollBlock> *didScrollBlocks;
@property(nonatomic, copy) NSString *onScrollFuncId;
@property(nonatomic, copy) NSString *onScrollEndFuncId;
@property(nonatomic, strong) DoricJSDispatcher *jsDispatcher;
@property(nonatomic, strong) UILongPressGestureRecognizer *longPress;
@property(nonatomic, strong) NSIndexPath *initialDragIndexPath;
@property(nonatomic, strong) NSIndexPath *currentDragIndexPath;
@property(nonatomic, copy) NSString *beforeDraggingFuncId;
@property(nonatomic, copy) NSString *onDraggingFuncId;
@property(nonatomic, copy) NSString *onDraggedFuncId;
@property(nonatomic, assign) NSUInteger rowCount;
@property(nonatomic, assign) BOOL needReload;
@end
@implementation DoricHorizontalListNode
- (instancetype)initWithContext:(DoricContext *)doricContext {
if (self = [super initWithContext:doricContext]) {
_itemViewIds = [NSMutableDictionary new];
_itemWidths = [NSMutableDictionary new];
_itemActions = [NSMutableDictionary new];
_batchCount = 15;
_loadAnchor = -1;
}
return self;
}
- (void)initWithSuperNode:(DoricSuperNode *)superNode {
[super initWithSuperNode:superNode];
if ([superNode isKindOfClass:[DoricRefreshableNode class]]) {
self.view.bounces = NO;
}
}
- (UICollectionView *)build {
UICollectionViewFlowLayout *collectionViewFlowLayout = [[UICollectionViewFlowLayout alloc] init];
[collectionViewFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
return [[[DoricHorizontalTableView alloc] initWithFrame:CGRectZero
collectionViewLayout:collectionViewFlowLayout] also:^(UICollectionView *it) {
it.dataSource = self;
it.delegate = self;
it.allowsSelection = NO;
it.showsHorizontalScrollIndicator = NO;
if (@available(iOS 11, *)) {
it.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressAction:)];
[it addGestureRecognizer:self.longPress];
[self.longPress setEnabled:NO];
}];
}
- (void)longPressAction:(UILongPressGestureRecognizer *)sender {
CGPoint locationInView = [sender locationInView:self.view];
NSIndexPath *indexPath = [self.view indexPathForItemAtPoint:locationInView];
if (sender.state == UIGestureRecognizerStateBegan) {
if (indexPath != nil) {
self.initialDragIndexPath = indexPath;
self.currentDragIndexPath = indexPath;
if (self.beforeDraggingFuncId != nil) {
[self callJSResponse:self.beforeDraggingFuncId, @(indexPath.row), nil];
}
}
} else if (sender.state == UIGestureRecognizerStateChanged) {
if ((indexPath != nil) && (indexPath != self.currentDragIndexPath)) {
NSString *fromValue = self.itemViewIds[@(self.currentDragIndexPath.row)];
NSString *toValue = self.itemViewIds[@(indexPath.row)];
self.itemViewIds[@(self.currentDragIndexPath.row)] = toValue;
self.itemViewIds[@(indexPath.row)] = fromValue;
[self.view moveItemAtIndexPath:self.currentDragIndexPath toIndexPath:indexPath];
if (self.onDraggingFuncId != nil) {
[self callJSResponse:self.onDraggingFuncId, @(self.currentDragIndexPath.row), @(indexPath.row), nil];
}
self.currentDragIndexPath = indexPath;
}
} else if (sender.state == UIGestureRecognizerStateEnded) {
if (self.onDraggedFuncId != nil) {
[self callJSResponse:self.onDraggedFuncId, @(self.initialDragIndexPath.row), @(self.currentDragIndexPath.row), nil];
}
}
}
- (void)blendView:(UICollectionView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"scrollable" isEqualToString:name]) {
self.view.scrollEnabled = [prop boolValue];
} else if ([@"bounces" isEqualToString:name]) {
self.view.bounces = [prop boolValue];
} else if ([@"itemCount" isEqualToString:name]) {
self.itemCount = [prop unsignedIntegerValue];
self.needReload = true;
} else if ([@"renderItem" isEqualToString:name]) {
if (![self.renderItemFuncId isEqualToString:prop]) {
self.loadAnchor = -1;
self.renderItemFuncId = prop;
[self.itemViewIds.allValues forEach:^(NSString *obj) {
[self removeSubModel:obj];
}];
[self.itemViewIds removeAllObjects];
self.needReload = true;
}
} else if ([@"batchCount" isEqualToString:name]) {
self.batchCount = [prop unsignedIntegerValue];
} else if ([@"onLoadMore" isEqualToString:name]) {
self.onLoadMoreFuncId = prop;
} else if ([@"loadMoreView" isEqualToString:name]) {
self.loadMoreViewId = prop;
} else if ([@"loadMore" isEqualToString:name]) {
BOOL loadMore = [prop boolValue];
if (loadMore != self.loadMore) {
self.loadMore = loadMore;
self.needReload = true;
}
} else if ([@"onScroll" isEqualToString:name]) {
self.onScrollFuncId = prop;
} else if ([@"onScrollEnd" isEqualToString:name]) {
self.onScrollEndFuncId = prop;
} else if ([@"scrolledPosition" isEqualToString:name]) {
dispatch_async(dispatch_get_main_queue(), ^{
NSUInteger pos = [prop unsignedIntegerValue];
if (pos < self.rowCount && pos >= 0) {
[view scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:pos inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}
});
} else if ([@"canDrag" isEqualToString:name]) {
bool canDrag = [prop boolValue];
[self.longPress setEnabled:canDrag];
} else if ([@"beforeDragging" isEqualToString:name]) {
self.beforeDraggingFuncId = prop;
} else if ([@"onDragging" isEqualToString:name]) {
self.onDraggingFuncId = prop;
} else if ([@"onDragged" isEqualToString:name]) {
self.onDraggedFuncId = prop;
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
- (void)blend:(NSDictionary *)props {
self.needReload = false;
[super blend:props];
if (self.needReload) {
dispatch_async(dispatch_get_main_queue(), ^{
self.rowCount = self.itemCount + (self.loadMore ? 1 : 0);
[self.view reloadData];
});
}
self.needReload = false;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.rowCount;
}
- (void)callLoadMore {
if (self.rowCount - 1 != self.loadAnchor) {
self.loadAnchor = self.rowCount - 1;
[self callJSResponse:self.onLoadMoreFuncId, nil];
}
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger position = (NSUInteger) indexPath.row;
NSDictionary *model = [self itemModelAt:position];
NSDictionary *props = model[@"props"];
NSString *identifier = props[@"identifier"] ?: @"doricCell";
self.itemActions[@(position)] = props[@"actions"];
if (self.loadMore
&& position >= self.rowCount - 1
&& self.onLoadMoreFuncId) {
identifier = @"doricLoadMoreCell";
[self callLoadMore];
}
[collectionView registerClass:[DoricHorizontalTableViewCell class] forCellWithReuseIdentifier:identifier];
DoricHorizontalTableViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
if (!cell.doricHorizontalListItemNode) {
DoricHorizontalListItemNode *itemNode = (DoricHorizontalListItemNode *) [DoricViewNode create:self.doricContext withType:@"HorizontalListItem"];
[itemNode initWithSuperNode:self];
cell.doricHorizontalListItemNode = itemNode;
cell.backgroundColor = [UIColor clearColor];
itemNode.view.height = collectionView.height;
[cell.contentView addSubview:itemNode.view];
} else {
[cell.doricHorizontalListItemNode reset];
}
DoricHorizontalListItemNode *node = cell.doricHorizontalListItemNode;
node.viewId = model[@"id"];
[node blend:props];
[node.view.doricLayout apply:CGSizeMake(collectionView.width, collectionView.height)];
[node requestLayout];
[self callItem:position width:node.view.width];
return cell;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger position = (NSUInteger) indexPath.row;
NSNumber *widthNumber = self.itemWidths[@(position)];
float width = 44.f;
if (widthNumber) {
width = [widthNumber floatValue];
}
return CGSizeMake(width, collectionView.height);
}
- (BOOL)collectionView:(UICollectionView *)collectionView canEditItemAtIndexPath:(NSIndexPath *)indexPath {
NSArray *actions = self.itemActions[@(indexPath.row)];
return actions.count > 0;
}
- (NSDictionary *)itemModelAt:(NSUInteger)position {
if (self.loadMore && position >= self.rowCount - 1) {
if (self.loadMoreViewId && self.loadMoreViewId.length > 0) {
return [self subModelOf:self.loadMoreViewId];
} else {
return nil;
}
}
NSString *viewId = self.itemViewIds[@(position)];
if (viewId && viewId.length > 0) {
return [self subModelOf:viewId];
} else {
NSInteger batchCount = self.batchCount;
NSInteger start = position;
while (start > 0 && self.itemViewIds[@(start - 1)] == nil) {
start--;
batchCount++;
}
DoricAsyncResult *result = [self pureCallJSResponse:@"renderBunchedItems", @(start), @(batchCount), nil];
NSArray *array = [result waitUntilResult:^(JSValue *models) {
return [models toArray];
}];
[array enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) {
NSString *thisViewId = obj[@"id"];
[self setSubModel:obj in:thisViewId];
NSUInteger pos = start + idx;
self.itemViewIds[@(pos)] = thisViewId;
}];
viewId = self.itemViewIds[@(position)];
if (viewId && viewId.length > 0) {
return [self subModelOf:viewId];
} else {
return nil;
}
}
}
- (void)blendSubNode:(NSDictionary *)subModel {
///Here async blend sub node because the item count need to be applied first.
NSUInteger currentCount = self.rowCount;
NSString *viewId = subModel[@"id"];
DoricViewNode *viewNode = [self subNodeWithViewId:viewId];
BOOL skipReload = NO;
if (viewNode) {
CGSize originSize = viewNode.view.frame.size;
[viewNode blend:subModel[@"props"]];
[viewNode.view.doricLayout apply:CGSizeMake(self.view.width, self.view.height)];
[viewNode requestLayout];
if (CGSizeEqualToSize(originSize, viewNode.view.frame.size)) {
skipReload = YES;
}
} else {
NSMutableDictionary *model = [[self subModelOf:viewId] mutableCopy];
[self recursiveMixin:subModel to:model];
[self setSubModel:model in:viewId];
}
if (skipReload) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.itemViewIds enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) {
if ([viewId isEqualToString:obj]) {
*stop = YES;
[UIView performWithoutAnimation:^{
NSUInteger itemCount = self.rowCount;
if (itemCount <= [key integerValue] || currentCount != itemCount) {
[self.view reloadData];
return;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[key integerValue] inSection:0];
@try {
[self.view reloadItemsAtIndexPaths:@[indexPath]];
}
@catch (id exception) {
[self.doricContext.driver.registry onException:exception inContext:self.doricContext];
}
}];
}
}];
});
}
- (void)callItem:(NSUInteger)position width:(CGFloat)width {
NSNumber *old = self.itemWidths[@(position)];
if (old && [old isEqualToNumber:@(width)]) {
return;
}
NSUInteger currentCount = self.rowCount;
self.itemWidths[@(position)] = @(width);
if (@available(iOS 12.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.view.doricLayout.heightSpec == DoricLayoutFit) {
DoricSuperNode *node = self.superNode;
while (node.superNode != nil) {
node = node.superNode;
}
[node requestLayout];
}
[UIView performWithoutAnimation:^{
NSUInteger itemCount = self.rowCount;
if (itemCount <= position || currentCount != itemCount) {
return;
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:position inSection:0];
@try {
[self.view reloadItemsAtIndexPaths:@[indexPath]];
}
@catch (id exception) {
[self.doricContext.driver.registry onException:exception inContext:self.doricContext];
}
}];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.view.doricLayout.heightSpec == DoricLayoutFit) {
DoricSuperNode *node = self.superNode;
while (node.superNode != nil) {
node = node.superNode;
}
[node requestLayout];
}
[self.view reloadData];
});
}
}
- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId {
__block DoricViewNode *ret = nil;
[self.doricContext.driver ensureSyncInMainQueue:^{
for (UICollectionViewCell *collectionViewCell in self.view.visibleCells) {
if ([collectionViewCell isKindOfClass:[DoricHorizontalTableViewCell class]]) {
DoricHorizontalListItemNode *node = ((DoricHorizontalTableViewCell *) collectionViewCell).doricHorizontalListItemNode;
if ([viewId isEqualToString:node.viewId]) {
ret = node;
break;
}
}
}
}];
return ret;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (DoricDidScrollBlock block in self.didScrollBlocks) {
block(scrollView);
}
if (self.onScrollFuncId) {
if (!self.jsDispatcher) {
self.jsDispatcher = [DoricJSDispatcher new];
}
__weak typeof(self) __self = self;
[self.jsDispatcher dispatch:^DoricAsyncResult * {
__strong typeof(__self) self = __self;
return [self callJSResponse:self.onScrollFuncId,
@{
@"x": @(self.view.contentOffset.x),
@"y": @(self.view.contentOffset.y),
},
nil];
}];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (self.onScrollEndFuncId) {
[self callJSResponse:self.onScrollEndFuncId,
@{
@"x": @(self.view.contentOffset.x),
@"y": @(self.view.contentOffset.y),
},
nil];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
if (self.onScrollEndFuncId) {
[self callJSResponse:self.onScrollEndFuncId,
@{
@"x": @(self.view.contentOffset.x),
@"y": @(self.view.contentOffset.y),
},
nil];
}
}
}
- (NSMutableSet<DoricDidScrollBlock> *)didScrollBlocks {
if (!_didScrollBlocks) {
_didScrollBlocks = [NSMutableSet new];
}
return _didScrollBlocks;
}
- (void)addDidScrollBlock:(__nonnull DoricDidScrollBlock)didScrollListener {
[self.didScrollBlocks addObject:didScrollListener];
}
- (void)removeDidScrollBlock:(__nonnull DoricDidScrollBlock)didScrollListener {
[self.didScrollBlocks removeObject:didScrollListener];
}
- (void)scrollToItem:(NSDictionary *)params {
BOOL animated = [params[@"animated"] boolValue];
NSUInteger scrolledPosition = [params[@"index"] unsignedIntegerValue];
if (scrolledPosition < self.rowCount && scrolledPosition >= 0) {
for (int i = 0; i <= scrolledPosition; i++) {
[self collectionView:self.view cellForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.view scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:scrolledPosition inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:animated];
});
} else {
[self.doricContext.driver.registry onLog:DoricLogTypeError
message:[NSString stringWithFormat:@"scrollToItem Error:%@", @"scrolledPosition range error"]];
}
}
- (NSArray *)findVisibleItems {
return [self.view.indexPathsForVisibleItems map:^id(NSIndexPath *obj) {
return @(obj.row);
}];
}
- (NSArray *)findCompletelyVisibleItems {
NSArray<__kindof UICollectionViewCell *> *items = [self.view.visibleCells filter:^BOOL(__kindof UICollectionViewCell *obj) {
return CGRectContainsRect(self.view.bounds, obj.frame);
}];
return [items map:^id(__kindof UICollectionViewCell *obj) {
return @([self.view indexPathForCell:obj].row);
}];
}
- (void)reset {
[super reset];
self.view.scrollEnabled = YES;
self.renderItemFuncId = nil;
self.onLoadMoreFuncId = nil;
self.loadMoreViewId = nil;
self.onScrollFuncId = nil;
self.onScrollEndFuncId = nil;
self.beforeDraggingFuncId = nil;
self.onDraggingFuncId = nil;
self.onDraggedFuncId = nil;
self.loadMore = NO;
}
- (void)subNodeContentChanged:(DoricViewNode *)subNode {
[subNode.view.doricLayout apply];
[super subNodeContentChanged:subNode];
}
- (void)reload {
self.loadAnchor = -1;
[self.itemViewIds.allValues forEach:^(NSString *obj) {
[self removeSubModel:obj];
}];
[self.itemViewIds removeAllObjects];
dispatch_async(dispatch_get_main_queue(), ^{
self.rowCount = self.itemCount + (self.loadMore ? 1 : 0);
[self.view reloadData];
});
}
@end