首页 > 代码库 > AsyncDisplayKit 2.0 Objective-C 教程

AsyncDisplayKit 2.0 Objective-C 教程

集成 AsyncDisplayKit

AsyncDisplayKit 官方只支持 CocoaPods 和 Carthage 安装。Carthage 不用说了,它是 Swift 包管理器。以国内的网络状况,CocoaPods 也基本不用想了,于是手动安装是我们的唯一选择。

如果你想用 git submodule 的方式安装,请参考这里。 本文使用源代码方式安装,于是你需要从 github 以 zip 包方式下载 AyncDisplayKit。下载后解压缩出 AsyncDisplay-master 目录(你可以把这个目录拷贝到要集成的项目目录下)。将这个目录拖进你的项目的项目导航窗口。

在项目导航窗口,选中你的项目的 target,在 General/Embedded Binaries 下面,点击 + 按钮,将 AsyncDisplayKit.framerork 添加到列表中。

编译项目,看看是否有报错的地方。

以 submodule 方式集成

  1. 进入要安装的项目根目录,将 ASDK 作为一个 git submodule 添加到项目中:

    git submodule add https://github.com/facebook/AsyncDisplayKit.git

  2. 等 git clone 完成,进入 AsyncDisplayKit 目录,将 .xcodeproj 拖进你的项目的 Xcode 项目导航窗口,它会在你的项目图标下方显示,无论你拖到什么位置。
  3. 在项目导航窗口中选择 AsyncDisplayKit.xcodeproj,修改 deployment target 为你的 App target 一致。
  4. 在项目导航窗口中选中你的项目图标,在 target 设置中,选择 App 的 target。在 General 标签页,找到 Embedded Binaries,点击 + 按钮,添加 AsyncDisplayKit.framework。
  5. 完成。

导入头文件

要在你的 Object C 项目中使用 AsyncDisplayKit ,需要导入头文件:

#import <AsyncDisplayKit/AsyncDisplayKit.h>

用 ASTableNode 替换 UITableView

在你的 View Controller 中,将用到 UITableView 的地方全部注释,我们将使用 ASTableNode 替换它以获得顺滑的滚动加载体验。因此将原来的 tableView 对象、TableView 的 Delegate 和 DataSource 协议方法和调用代码全部删除。

在 View Controller 中,定义一个属性:

@property(strong,nonatomic)ASTableNode* tableNode;

在 viewDidLoad 方法中,用下列代码初始化 tableNode:

    // 1
    _tableNode = [[ASTableNode alloc]initWithStyle:UITableViewStylePlain];
    // 2
    self.tableNode.dataSource = self;
    self.tableNode.delegate = self;
   // 3 
    self.tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone;
   // 4
    self.tableNode.view.leadingScreensForBatching = 1.0;
   // 5
    [self.view addSubnode:self.tableNode];

代码解释如下:

  1. 和 UITableView 一样,这句创建了 ASTableNode 实例,同时指定类型为 plain,因为我们决定使用自定义的表格。
  2. 设置 delegate 和 dataSource。
  3. tableNode 实际上封装了一个 UITableView,可以通过它的 .view 属性引用这个 UITableView,因此这句清除了表格分隔线。
  4. leadingScreenFroBatching 指定了表格什么时候抓取新数据,单位为一个完整屏幕。这句表示当还有 1 屏 cell 数据缓存时就抓取新数据。
  5. addSubnode 是以 Category 形式添加的。其作用类似于 addSubview,不过它添加的是 ASNode 而不是 UIView。

实现一个viewWillLayoutSubviews方法:

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    self.tableNode.frame = CGRectMake(0, CGRectGetMaxY(_bar.frame), CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)-CGRectGetHeight(self.bar.frame));
}

没有什么,指定了 TabelNode 的 frame 而已。这里我们让它放在 _bar(一个 UIView)下面,并占据屏幕上除 _bar 以外的剩余空间。

在 dealloc 中释放 delegate 和 dataSource,据说这样可以避免闪退(实际有没有作用不得而知):

-(void)dealloc{
    self.tableNode.delegate = nil;
    self.tableNode.dataSource = nil;
}

接下来实现 dataSource:

- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section {
    // 1
    return self.models.count;
}

- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 2
    AlbumModel* model = _models[indexPath.row];

    // 3
    ASCellNode *(^ASCellNodeBlock)() = ^ASCellNode *() {
        AlbumCellNode *cellNode = [[AlbumCellNode alloc] initWithAlbum:model];
        return cellNode;
    };

    return ASCellNodeBlock;
}
- (NSInteger)numberOfSectionsInTableNode:(ASTableNode *)tableNode{
    // 4
    return 1;
}

ASTableNode 的数据源方法和 UITableView 的数据源方法基本上是可以对应的,比如这 3 个方法,返回了表格的行数、节数和 cell。
代码解释如下:

  1. models 是一个数组,是用远程抓取到的模型对象来填充的。我们需要显示这些模型数据,因此简单返回它的个数以作为表格的行数。
  2. nodeBlockForRowAtIndexPath: 方法签名有点怪(它的返回结果是一个块),不用管它,我们把它当成 cellForRowAtIndexPath: 方法用就行,除了返回结果奇怪点外,它们基本上没区别。这句从 models 数组中检索出 cell 将要展现的模型对象。
  3. ASCellNode 是 UITableViewCell 的对应封装。本来我们可以直接返回 ASCellNode(ASDK 有一个返回这个类型的数据源方法),但是官方推荐我们使用返回块的版本。因此这里定义了一个 ASCellNodeBlock 对象,这是一个特殊的块,会返回一个 ASCellNode,不需要提供任何参数。在这个块中,我们使用了一个自定义的 ASCellNode,名为 AlbumCellNode(后面我们会实现它),并调用它的初始化方法把模型传递进去,然后在块的最后返回这个 ASCellNode。
  4. 我们的表格不分组,所以返回 1 就行。

接下来是 delegate 方法实现:

// 1
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode {
    return YES;
}
//2
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
{
    [context beginBatchFetching];
    [self loadPageWithContext:context];
}
// 3
- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [tableNode deselectRowAtIndexPath:indexPath animated:YES];

    // 你自己的代码
    ......

}

这两个全新的 delegate 方法是 ASTableDelegate 中独有的,UITableViewDelegate 中没有对应的方法。这两个方法的作用是让表格实现异步加载,也就是以往我们需要用 MJRefresh 实现的功能。代码解释如下:

  1. 这个方法用于告诉 ASTableNode,用户的一次下拉动作是否需要触发异步抓取,这里我们返回了 YES,也就是不管什么情况都进行异步抓取。我们这样做的原因,是现在的后台服务从来不告诉前端什么时候数据才会”完”,反正有数据的话服务器会返回数据,没数据的话则返回错误(比如“ 404 没有数据” 之类)或者返回空结果集。所以我们根本无法事先知道数据什么时候数据已经加载完。所以不管数据有没有完,我们都当做没有完来进行抓取,并通过服务器返回的结果来判断。这样这个方法就没有必要进行任何计算了,直接返回 YES。
  2. 这个方法用于进行一次抓取。loadPageWithContext: 方法是我们自定义的,它会加载一页数据,同时页数会累加,这样每次都会加载“下一页”,除非服务器没有数据返回。context 参数是必须的,用于抓取完后通知 ASTableNode 抓取完成(见后面的loadPageWithContext 方法实现)。
  3. 和 UITableView 的 cell 点击事件方法一模一样,你自己实现吧。

然后,新增两个方法如下:

// 1
- (void)loadPageWithContext:(ASBatchContext *)context
{
    NSString* radioId = [[AccountAdditionalModel currentAccount] radioId];
    // 2
    [self loadAlbums:radioId pageNum:pageNum+1 pageSize:pageSize].thenOn(dispatch_get_main_queue(),^(NSArray<AlbumModel>* array){

        if(array!=nil){
            // 3
            pageNum = pageNum+1;
            [_models addObjectsFromArray: array];
            // 4
             [self insertNewRowsInTableNode:array];
        }
        // 5
        if (context) {
                [context completeBatchFetching:YES];
            }

    }).catch(^(NSError* error){
        [self showHint:error.localizedDescription];
        // 6
        if (context) {
            [context completeBatchFetching:YES];
        }
    });

}

// 7
- (void)insertNewRowsInTableNode:(NSArray<AlbumModel>*)array
{
    NSInteger section = 0;
    NSMutableArray *indexPaths = [NSMutableArray array];

    for (NSUInteger row = _models.count-array.count; row < _models.count; row++) {
        NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:section];
        [indexPaths addObject:path];
    }
    [_tableNode insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
}

代码解释如下:

  1. 这个方法用于加载一页数据,如果后台服务器也支持分页查询的话。这里各人的实现会不一样,这个实现仅供参考。
  2. 在这个方法中,调用你自己的分页抓取实现,具体的实现就不介绍了,无非就是向服务器进行异步请求之类的。注意,这里的分页抓取实现使用了 Promise 设计模型来解决异步调用的问题,即 thenOn/catch 形式。
  3. 当所请求的数据返回,如果结果集不为空,我们累加当前页数和数据源数组。
  4. 调用 insertNewRowsInTableNode 方法,这个方法向 tableNode 中插入新的单元格(见后面的实现)。
  5. 数据源更新完毕,一定要通知 context(ASBatchContext 实例)告诉它批抓取结束,即调用它的 completeBatchFetching 方法。
  6. 出现错误时,也要调用 completeBatchFetching。
  7. insertNewRowsInTableNode 方法实现就很简单了,一次向 tableNode 中插入多个 cell。

批抓取什么时候进行?

ASTableNode 也有“页”的概念,但和我们向服务器进行分页请求时的“页”不同,这里的“页”是一屏的概念。也就是说,我们在 viewDidLoad 中设置 self.tableNode.view.leadingScreensForBatching = 1.0; 这一句时,表示当屏幕中还剩下一屏(页)的数据就要显示完的时候,ASTableNode 会自动进行抓取。

但是我们一次向服务器能够请求的页大小并不一定能够填满一屏。比如分页查询的页大小是 4,然而 4 条数据并不足以填满一个屏幕,因此 ASTableNode 还会再请求一次分页查询,然后检查(会进行一个预布局,计算数据显示时的尺寸)是否填满一屏,如果不够,会再次请求,直至填满一屏。

也就是说,数据在真正得到显示之前就已经进行了布局(异步的)。当需要显示的时候,仅仅是一个绘制而已,这样绘制的速度就会非常快,滚动体验会无比顺滑。

自定义 cell

ASTableNode 的真正难点,在于自定义 cell 的使用,因为 ASDK 使用了自己的布局系统 ASLayoutSpec,这个布局系统不同于你曾经使用过的任何一个自动布局系统,没有使用过它的人无疑会很头疼。但没有关系,当你使用过它几次之后,你就会觉得它和安卓的布局系统很像,还是较容易掌握的。

在前面的 tableNode: nodeBlockForRowAtIndexPath: 数据源方法中,我们使用了一个自定义的 ASCellNode 类 AlbumCellNode,它就是一个 ASLayoutSpec 的极好的例子。我们来看看它是怎样实现的。

@implementation AlbumCellNode{
    AlbumModel* _album;
    ASNetworkImageNode  *_photoImageNode;
    ASTextNode          *_titleLabel;
    ASTextNode          *_contentLabel;
    ASTextNode          *_albumCountLabel;
}

这个 cell 中包含了几个对象:

  1. _album:一个自定义模型对象,我们将在 cell 上展示的数据。
  2. _photoImageNode:一个 ASNetwokImageNode(能够异步加载图片的 UIImageView),用于在 cell 上显示一张图片。
  3. _titleLabel:一个 ASTextNode,用于显示标题文本。ASTextNode 相当于 UILabel,不同的是能够在 ASCellNode 上使用。通常情况下,在 ASCellNode 中你不能使用 UIKit 中的UI 组件,而只能使用它们的 ASDK 的封装类。
  4. _contentLabel:用于显示一些文字描述。
  5. _albumCountLabel:用于显示专辑中的曲目数。

然后是初始化方法:

- (instancetype)initWithAlbum:(AlbumModel *)album{
    self = [super init];

    if (self) {
        _album = album;
        // 1
        _photoImageNode          = [[ASNetworkImageNode alloc] init];
        _photoImageNode.URL      = [NSURL URLWithString:[kDownloadUrl add: album.icon]];

        [self addSubnode:_photoImageNode];

        // 2
        _titleLabel = [[ASTextNode alloc]init];
        _titleLabel.attributedText = [[NSAttributedString alloc]initWithString:album.title attributes:@{NSForegroundColorAttributeName: [UIColor blackColor],
NSFontAttributeName: [UIFont systemFontOfSize:15]}];
        [self addSubnode:_titleLabel];

        _contentLabel = [[ASTextNode alloc]init];

        NSString* newStr = album.content;//[album.content stringByPaddingToLength:album.content.length*10 withString:album.content startingAtIndex:0];
        _contentLabel.attributedText = [[NSAttributedString alloc]initWithString:newStr attributes:@{NSForegroundColorAttributeName: [UIColor grayColor],
                                                                                                        NSFontAttributeName: [UIFont systemFontOfSize:12]}];

        [self addSubnode:_contentLabel];

        _albumCountLabel = [[ASTextNode alloc]init];
        _albumCountLabel.backgroundColor = [UIColor redColor];
        _albumCountLabel.cornerRadius = 3;
        _albumCountLabel.clipsToBounds = YES;
        _albumCountLabel.attributedText = [[NSAttributedString alloc]initWithString:[album.programCount add:@"辑"] attributes:@{NSForegroundColorAttributeName: [UIColor whiteColor],
                                                                                                            NSFontAttributeName: [UIFont systemFontOfSize:9]}];


        [self addSubnode:_albumCountLabel];
    }
    return self;
}

这个初始化方法需要用一个模型数据作为参数,根据模型中已有的数据来初始化 UI 组件。

  1. 构建一个 ASNetworkImageNode,将它通过 addSubnode 添加到 cell 中。这个组件的使用非常简单,实例化、设置 URL、addSubnode。
  2. 剩下的是构建 3 个 ASTextNode 并 addSubnode,它们的使用都是一样的,所以只看一个就够了。关键点在于 attributedText 属性的使用,也就是这个组件通过 NSAttributedString 来一次性指定文字的字体、大小、颜色、换行、段落格式、对齐方式、文本内容。当然这个对象暴露了一些 UILabel 和 CALayer 的属性,比如 backgroundColor、cornerRadius 等。

注意到一个细节没有?不管是 ASNetworkImageNode 还是 ASTextNode,它们都不需要设置框架(frame)。这是因为它们的构建和布局是分开进行的(这就是框架名字中 Async 异步的由来了),在初始化方法中,你只管构建好了,布局在另一个方法中进行:

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{

    // 1
    NSMutableArray *mainStackContent = [[NSMutableArray alloc] init];
    [mainStackContent addObject:_titleLabel];
    [mainStackContent addObject:_contentLabel];

    // 2
    _albumCountLabel.textContainerInset = UIEdgeInsetsMake(3, 5, 3, 5);

    // 3
    ASRelativeLayoutSpec *albumCountSpec = [ASRelativeLayoutSpec
                                            relativePositionLayoutSpecWithHorizontalPosition:ASRelativeLayoutSpecPositionStart
                                            verticalPosition:ASRelativeLayoutSpecPositionCenter
                                            sizingOption:ASRelativeLayoutSpecSizingOptionMinimumWidth
                                            child:_albumCountLabel];
    // 4
    [mainStackContent addObject:albumCountSpec];


    // 5
    ASStackLayoutSpec *contentSpec =
    [ASStackLayoutSpec
     stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
     spacing:8.0
     justifyContent:ASStackLayoutJustifyContentStart
     alignItems:ASStackLayoutAlignItemsStretch
     children:mainStackContent];
    contentSpec.style.flexShrink = 1.0;

    // 6
    _photoImageNode.style.preferredSize = CGSizeMake(90, 90);
    // 7
    ASStackLayoutSpec *avatarContentSpec =
    [ASStackLayoutSpec
     stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
     spacing:16.0
     justifyContent:ASStackLayoutJustifyContentStart
     alignItems:ASStackLayoutAlignItemsStart
     children:@[_photoImageNode, contentSpec]];
    // 8
    return [ASInsetLayoutSpec
            insetLayoutSpecWithInsets:UIEdgeInsetsMake(8, 16, 8, 16)
            child:avatarContentSpec];
}

所有的 ASNode 都必须在这个方法中进行布局,这也是这一节重点要介绍的地方:

  1. 一个数组,我们放入了两个对象:一个标题标签,一个内容标签。这个后面会用到,因为我们打算将这两个标签放在一起,上下排列。
  2. 设置曲目数标签的文字补白,这样会在这个标签的文字四周补上一些边距,不至于让文字紧紧地被边框包裹在一起,因为默认的 Inset 值是 0。
  3. 一个相对布局,这个和 Android 中的相对布局是一样的。这个相对布局我们指定它的水平对齐方式为左对齐,垂直对齐方式为中对齐,在宽度上进行紧凑布局(相当于 Android 的 layout_width=”wrap_content”),然后将曲目数标签放入布局。这样会导致这个标签居于这个布局中的左边垂直居中,同时标签宽度不会占满整行,而是内容有多长就多宽。
  4. 将这个对布局也放到前面的数组中,这样,三个标签就放在一块了,上下排列。
  5. 要将 3个标签放到一块,需要将 3 者(即前面的数组,我们已经将 3 者放到一个数组了)添加到一个 stack 布局中。stack 布局类似自动布局中的 UIStackView,专门作为其他节点的容器,并且可以方便地指定这些节点的排列方式。这里我们在构建 stack 布局时指定三者为垂直排列,行间距 8,主轴方向(y轴)顶对齐,交叉轴方向(x轴)占据行宽。
  6. 现在来布局图片的大小,一般来说图片大小是固定的,这里我们通过设置它的 style.preferredSize 来指定它的固定大小为 90*90。
  7. 创建另一个 stack 布局,将图片和另一个 stack 布局(即 3 个标签)放到一起,指定:让两者水平排列、水平间距 16、主轴方向左对齐,交叉轴方向顶对齐。
  8. 最后在 stack 布局的基础上补白。并返回补白后的 Inset 布局。

ASDK 有 4 中布局,这 4 种布局分别是:

  • ASStackLayoutSpec: 允许你定义一个水平或垂直的子节点栈。它的 justifyContent 属性决定栈在相应方向上的子节点之间的间距。alignItems 属性决定了它们在另一个坐标轴上的间距。这种 layout specs 有点像 UIKit 的 UIStackView。
  • ASOverlayLayoutSpec: 允许你拉伸一个元素横跨到另一个元素。被覆盖的对象必须要有一个固定的 content size,否则无法工作。
  • ASRelativeLayoutSpec: 一种相对布局,允许将一个东西以相对位置放置在它的有效空间内。参考一下“9-切片图”的 9 个切片。你可以让一个东西放在这 9 个切片中的某个上。
  • ASInsetLayoutSpec: 一个 inset 布局,允许你在一个已有的对象的基础上添加某些间距。你想在你的 cell 四周加上经典的 iOS 16 像素的边距吗?用这个就对了。

在这个例子中我们用到了 3 种。Overlay 布局不太常用,它类似于 css 中的 position : absolute 属性,允许将一个布局重叠到另一个布局上。

结束

好了,本文就此结束。本文偏向于 ASDK 的应用和实作,对 ASDK 的基础概念介绍得较少。如果你需要这方面的介绍,可以阅读这几篇文章:

  • AsyncDisplayKit2.0教程(上)
  • AsyncDisplayKit2.0教程(下)
  • 用 AsyncDisplayKit 開發響應式 iOS App
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    AsyncDisplayKit 2.0 Objective-C 教程