美文网首页
iOS嵌套式游戏详情页

iOS嵌套式游戏详情页

作者: 123123dfgj656 | 来源:发表于2018-04-14 16:51 被阅读75次

    先上作品图

    image

    UI样式设计非原创,仅用于学习。

    主要的功能点

    • 头部视图会跟随移动,选择介绍和攻略页的segmentController又会一直保留在最顶部
    • 点击segmentController切换相应页面,滑动页面变更segmentController的index显示
    • tableViewCell中嵌套UICollectionView
    • tableViewCell中的实现类似朋友圈“全文”和“收起”的效果

    视图结构分析

    image

    最外层是scrollView和带有segmentController的头部视图并列(为什么scrollView的高度是这个值而不是减去头部视图的值,下文会补充)。
    scrollView的contentView添加了左右两个tableView,介绍页(左),攻略页(右)。介绍页tableViewCell又嵌套了一个UIcollectionView。大致上层级就是这样的。

    一步一步来完成

    头部视图和外层scrollView

    自定义一个HeaderView的类,HeadView内部实现就不累赘了。需要的先放个源码
    在ViewController中添加HeaderView和scrollView。

    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:self.scrollView];
        [self.view addSubview:self.headerView];
    }
    
    - (HeaderView *)headerView {
        if (!_headerView) {
            CGFloat headerHeight = SCREEN_HEIGHT / 6 + SEGMENT_HEIGHT;
            _headerView = [[HeaderView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, headerHeight)
                                                      title:@"王者荣耀"
                                                  downloads:@"n+ 次下载"
                                                   descripe:@"1.1 GB"];
            
            [_headerView.segmentedControl addObserver:self
                                           forKeyPath:@"selectedSegmentIndex"
                                              options:NSKeyValueObservingOptionNew
                                              context:nil];
        }
        return _headerView;
    }
    
    - (UIScrollView *)scrollView {
        if (!_scrollView) {
            self.scrollViewHeight = self.view.height - 64;
            _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, self.scrollViewHeight)];
            _scrollView.contentSize = CGSizeMake(self.view.width*2, 0);
            _scrollView.bounces = NO;
            _scrollView.showsHorizontalScrollIndicator = NO;
            _scrollView.pagingEnabled = YES;
            _scrollView.showsVerticalScrollIndicator = NO;
            _scrollView.delegate = self;
        }
        return _scrollView;
    }
    

    segmentController和scrollView相互作用

    上面代码已经设置了scrollView按页滚动,点击segmentController的介绍页,则让scrollView滚动到左边的页面。反之亦然。这里通过KVO来实现,也就是观察segmentController的index值的变化来更改scrollView的contentOffset。KVO的释放不能忘!

    #pragma mark - KVO
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
            // segmentController选中不同按钮切换scrollView的页面
            if ([keyPath isEqualToString:@"selectedSegmentIndex"]) {
            // 点击介绍
            if (self.headerView.segmentedControl.selectedSegmentIndex == 0) {
                    self.scrollView.contentOffset = CGPointZero;
                // 点击攻略
            } else {
                self.scrollView.contentOffset = CGPointMake(self.view.width, 0);
            }
        }
    }
    
    - (void)dealloc {
        [self.headerView.segmentedControl removeObserver:self forKeyPath:@"selectedSegmentIndex"];
    }
    
    

    而当scrollView滚动时,用代理方法来更改segmentController的index。这里当然也可以使用KVO的,为什么使用代理方法会更合适?下文会提到。

    #pragma mark - scrollView delegate
    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (scrollView == self.scrollView) {
            // 滑动到介绍页
            if (scrollView.contentOffset.x == 0) {
                if (self.headerView.segmentedControl.selectedSegmentIndex != 0) {
                    self.headerView.segmentedControl.selectedSegmentIndex = 0;
                }
            // 滑动到攻略页
            } else if (scrollView.contentOffset.x == self.view.width) {
                if (self.headerView.segmentedControl.selectedSegmentIndex != 1) {
                    self.headerView.segmentedControl.selectedSegmentIndex = 1;
                }
            }
        }
    }
    

    头部视图跟随移动

    实现思路:给表视图添加一个空的headerView,大小和我们上面定义的头部视图一样,如下图。滚动表视图的时候,根据表视图的偏移量来设置真正的头部视图的y值到达到滚动的假象,并且同步左右两个表视图的偏移量,而segmentController滚动到顶部的时候便令其y值保持不变就不再滚动了。

    还记得上文说的scrollView的高度为什么不减去headerView的高度了吗,看了这个图理解了吗。

    另外一点,这里采用KVO监听tableView的contentOffset的值,变化后更改头部视图的y。所以KVO的keyPath是”contentOffset”。这就是上文提到的为什么scrollView不使用KVO而使用代理方法。因为keyPath会冲突。

    image

    接上面ViewController代码,为了让代码更好地分离,这里使用childViewController。

    ViewController.m
    
    - (void)viewDidLoad {
        // ...
        self.introduceTVC = [[IntroduceTableViewController alloc] init];
        self.strategyTVC = [[StrategyTableViewController alloc] init];
        [self setupChildViewController:self.introduceTVC x:0];
        [self setupChildViewController:self.strategyTVC x:SCREEN_WIDTH];
    }
    
    - (void)setupChildViewController:(UITableViewController *)tableViewController x:(CGFloat)x {
        UITableViewController *tableVC = tableViewController;
        tableVC.view.frame = CGRectMake(x, 0, self.view.width, self.scrollViewHeight);
        [self addChildViewController:tableVC];
        [self.scrollView addSubview:tableVC.view];
        [tableVC.tableView addObserver:self
                            forKeyPath:@"contentOffset"
                               options:NSKeyValueObservingOptionInitial
                               context:nil];
    }
    
    #pragma mark - KVO
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        // 头部视图移动跟随滑动
        if ([keyPath isEqualToString:@"contentOffset"]) {
            CGFloat headerViewScrollStopY = SCREEN_HEIGHT/6 - 15;
            UITableView *tableView = object;
            
            CGFloat contentOffsetY = tableView.contentOffset.y;
    
            // 滑动没有超过停止点
            if (contentOffsetY < headerViewScrollStopY) {
                self.headerView.y = - tableView.contentOffset.y;
                // 同步tableView的contentOffset
                for (UITableViewController *vc in self.childViewControllers) {
                    if (vc.tableView.contentOffset.y != tableView.contentOffset.y) {
                        vc.tableView.contentOffset = tableView.contentOffset;
                    }
                }
            } else {
                self.headerView.y = - headerViewScrollStopY;
            }
        }
    

    两个childViewController中的方法

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.tableView.backgroundColor = DEFAULT_BACKGROUND_COLOR;
        self.tableView.showsVerticalScrollIndicator = NO;
        
        CGFloat headerHeight = SCREEN_HEIGHT / 6 + SEGMENT_HEIGHT;
        // 假的tableview,高度同GameDetailHeadView
        self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, headerHeight)];
    }
    
    image

    tableViewCell嵌套collectionView

    在介绍页的第一个section中需要显示一系列的介绍图片,图片有需要滚动,这时候嵌套UIcollectionView就很合适了。

    自定义tableViewCell,cell中添加collectionView,由于初始化时大小并未确定下来,所以需要在layoutSubviews方法中设置collectionView的大小,让其填充满cell。

    下面的...setCollectionViewDataSourceDelegate...方法可以帮助cell把相应的indexPath传递给collectionView,该项目并没有使用到tableViewCell的indexPath,没有也是可以的,只是为了增加其通用性,当有多个cell需要嵌套collectionView的时候,这时候就需要用indexPath来判断具体是哪个section哪个row了。

    这里还有个坑[self.collectionView setContentOffset:CGPointZero animated:NO];方法不能用[self.collectionView setContentOffset:CGPointZero];代替,这是因为滚动过程中,很可能还未停下来,如果用了后面的方法,那么设置contentOffset之后,collectionView还会持续把刚才未滚动完的继续完成,位置就会出现偏差。

    ImageTableViewCell.h
    
    #import <UIKit/UIKit.h>
    
    static NSString *CollectionViewCellID = @"CollectionViewCellID";
    
    @interface ImageCollectionView : UICollectionView
    
    // collectionView所在的tableViewCell的indexPath
    @property (nonatomic, strong) NSIndexPath *indexPath;
    
    @end
    
    @interface ImageTableViewCell : UITableViewCell
    
    @property (nonatomic, strong) ImageCollectionView *collectionView;
    
    - (void)setCollectionViewDataSourceDelegate:(id<UICollectionViewDataSource, UICollectionViewDelegate>)dataSourceDelegate indexPath:(NSIndexPath *)indexPath;
    
    @end
    
    
    ImageTableViewCell.m
    
    @implementation ImageCollectionView
    
    @end
    
    @implementation ImageTableViewCell
    
    - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
        if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
            UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
            layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
            layout.itemSize = CGSizeMake(SCREEN_WIDTH/3, SCREEN_HEIGHT/4);
            layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
            self.collectionView = [[ImageCollectionView alloc] initWithFrame:self.contentView.bounds collectionViewLayout:layout];
            
            [self.collectionView registerClass:[ImageCollectionViewCell class] forCellWithReuseIdentifier:CollectionViewCellID];
            self.collectionView.backgroundColor = [UIColor whiteColor];
            self.collectionView.showsHorizontalScrollIndicator = NO;
            [self.contentView addSubview:self.collectionView];
        }
        
        return self;
    }
    
    -(void)layoutSubviews {
        [super layoutSubviews];
        self.collectionView.frame = self.contentView.bounds;
    }
    
    /// 设置delegate,dataSource等
    - (void)setCollectionViewDataSourceDelegate:(id<UICollectionViewDataSource, UICollectionViewDelegate>)dataSourceDelegate indexPath:(NSIndexPath *)indexPath {
        self.collectionView.dataSource = dataSourceDelegate;
        self.collectionView.delegate = dataSourceDelegate;
        self.collectionView.indexPath = indexPath;
        [self.collectionView setContentOffset:CGPointZero animated:NO];
        [self.collectionView reloadData];
    }
    

    collectionViewCell的代码也不关键,不占地方了,需要的看源码

    之后在IntroduceTableViewController中设置相应的数据源和代理方法。这里有一个...willDisplayCell...方法,用来配置collectionView的,理论上里面的操作也可以在...cellForRowAtIndexPath...完成,只是数据源加载好当cell要显示的时候再去执行配置collectionView的方法更符合逻辑一些。

    IntroduceTableViewController.m
    
    static NSString *kCellID0 = @"cellID0";
    @interface IntroduceTableViewController () <UICollectionViewDelegate, UICollectionViewDataSource>
    @end
    
    #pragma mark - tableView dataSource
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID0];
        if (!cell) {
            cell = [[ImageTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellID0];
        }
        return cell;
    }
    
    #pragma mark - tableView delegate
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        return SCREEN_HEIGHT / 3;
    }
    
    - (void)tableView:(UITableView *)tableView willDisplayCell:(ImageTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
            [cell setCollectionViewDataSourceDelegate:self indexPath:indexPath];
    }
    
    #pragma mark - collection view deta source
    -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
        return 5;
    }
    
    -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        ImageCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CollectionViewCellID forIndexPath:indexPath];
        if (!cell) {
            cell = [[ImageCollectionViewCell alloc] initWithFrame:CGRectMake(0, 0, SCREEN_HEIGHT/4, SCREEN_HEIGHT/4)];
        }
        cell.imageView.image = [UIImage imageNamed:@"fake_game"];
        return cell;
    }
    
    

    tableViewCell中段落的“全文”和“收起”

    实现思路:
    这里的文字采用UILabel来展示,收起状态下,返回固定的cell高度,并且保存初始的UILabel和UIButton的frame值。展开状态下,根据需要显示的文字计算其文字高度,根据高度来更改cell高度,还有其他控件的frame。
    定义indexPath把自身所处的indexPath在控制器传进来,点击按钮后回调showMoreBlock根据indexPath刷新cell的内容和高度。所以这里的...layoutSubview...方法很关键。

    ContentTableViewCell.h
    
    typedef void (^ShowMoreBlock)(NSIndexPath *indexPath);
    
    @interface ContentTableViewCell : UITableViewCell
    
    @property (nonatomic, copy) NSString *gameIntroduce; // 游戏介绍内容
    @property (nonatomic, strong) NSIndexPath *indexPath; // 用于刷新指定cell
    @property (nonatomic, assign, getter=isShowMoreContent) BOOL showMoreContent; // 是否显示更多内容
    
    @property (nonatomic, copy) ShowMoreBlock showMoreBlock; // 点击更多按钮回调
    
    /// 默认高度(收起)
    + (CGFloat)cellDefaultHeight;
    
    /// 显示全文的高度
    + (CGFloat)cellMoreContentHeight:(NSString *)content;
    
    ContentTableViewCell.m
    
    static const CGFloat kBlankLength = 10;
    
    @interface ContentTableViewCell ()
    
    @property (nonatomic, strong) UILabel *contentLabel;
    @property (nonatomic, strong) UIButton *showMoreButton;
    // 记录button初始的frame
    @property (nonatomic, assign) CGRect btnOriFrame;
    
    @end
    
    @implementation ContentTableViewCell
    
    - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
        self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
        
        if (self) {
            self.contentLabel = [[UILabel alloc] initWithFrame:CGRectMake(kBlankLength, kBlankLength, SCREEN_WIDTH-kBlankLength*2, 100)];
            self.contentLabel.numberOfLines = 0;
            self.contentLabel.font = [UIFont systemFontOfSize:14];
            [self.contentView addSubview:self.contentLabel];
            
            self.showMoreButton = [UIButton buttonWithType:UIButtonTypeSystem];
            [self.showMoreButton setTitle:@"更多" forState:UIControlStateNormal];
            [self.showMoreButton addTarget:self
                                    action:@selector(showMoreOrLessContent)
                          forControlEvents:UIControlEventTouchUpInside];
            [self.showMoreButton sizeToFit];
            CGFloat unFoldButtonX = SCREEN_WIDTH - kBlankLength - self.showMoreButton.width;
            CGFloat unFoldButtonY = kBlankLength * 2 + self.contentLabel.height;
            CGRect buttonFrame = self.showMoreButton.frame;
            buttonFrame.origin.x = unFoldButtonX;
            buttonFrame.origin.y = unFoldButtonY;
            self.showMoreButton.frame = buttonFrame;
            self.btnOriFrame = buttonFrame;
            [self.contentView addSubview:self.showMoreButton];
        }    
        return self;
    }
    
    - (void)layoutSubviews {
        [super layoutSubviews];
        if (self.isShowMoreContent) {
            // 计算文本高度
            NSDictionary *attribute = @{NSFontAttributeName: [UIFont systemFontOfSize:14]};
            NSStringDrawingOptions option = (NSStringDrawingOptions)(NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading);
            CGSize size = [self.gameIntroduce boundingRectWithSize:CGSizeMake(SCREEN_WIDTH-kBlankLength*2, 1000) options:option attributes:attribute context:nil].size;
            
            self.contentLabel.frame = CGRectMake(kBlankLength, kBlankLength, SCREEN_WIDTH-kBlankLength*2, size.height+20);
            
            CGFloat buttonMoreContentY = kBlankLength * 2 + self.contentLabel.height;
            CGRect buttonMoreContentRect = self.btnOriFrame;
            buttonMoreContentRect.origin.y = buttonMoreContentY;
            self.showMoreButton.frame = buttonMoreContentRect;
            [self.showMoreButton setTitle:@"收起" forState:UIControlStateNormal];
        } else {
            self.contentLabel.frame = CGRectMake(kBlankLength, kBlankLength, SCREEN_WIDTH-kBlankLength*2, 100);
            self.showMoreButton.frame = self.btnOriFrame;
            [self.showMoreButton setTitle:@"全文" forState:UIControlStateNormal];
        }
    }
    
    - (void)showMoreOrLessContent {
        if (self.showMoreBlock) {
            self.showMoreBlock(self.indexPath);
        }
    }
    
    
    
    - (void)setGameIntroduce:(NSString *)gameIntroduce {
        self.contentLabel.text = gameIntroduce;
        _gameIntroduce = gameIntroduce;
    }
    
    
    + (CGFloat)cellDefaultHeight {
        return 160;
    }
    
    + (CGFloat)cellMoreContentHeight:(NSString *)content {
        // 计算文本高度
        NSDictionary *attribute = @{NSFontAttributeName: [UIFont systemFontOfSize:14]};
        NSStringDrawingOptions option = (NSStringDrawingOptions)(NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading);
        CGSize size = [content boundingRectWithSize:CGSizeMake(SCREEN_WIDTH-kBlankLength*2, 1000) options:option attributes:attribute context:nil].size;
        return size.height + 80;
    }
    

    PS:如果有留心观察的童鞋会发现这里的Label的内容在”全文”和“收起”状态下的高度并不对齐,这是因为UILabel默认内容是居中对齐的。
    尝试过使用UITextField,但是内容只能显示一行,弃用。
    尝试过使用UITextView,收起的内容末尾不会出现省略号,而且不是根据文字内容按行压缩,可能会出现文字的一半被压缩的情况,弃用。
    找到的一种相对满意的方法是自定义一个UILabel,重写其...drawRect...方法,给需要的同学提供个思路。这里不详细展开了,挖个坑,之后计划写一篇和文字排版有关的可能会提到。

    最后再把其他数据项填充一下,就是我们看到的这个样子了


    image

    是不是觉得少了点什么,内容怎么感觉都在一块了。对了,是section headerView 和 footerView。

    section header footer

    这里实现是不难,但是一样要把控细节。我们的第一个展示图片的cell是不需要headerView的,最后一个cell是不需要footerView的,这两个就像画蛇添足,有了反而不好看。这里采用了偷懒的方式,直接把footerView的部分添加到了headerView的头部。


    image

    图中两个剪头的位置分别是假的section footerView和section headerView

    IntroduceTableViewController.m
    
    - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
        UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 40)];
        backgroundView.backgroundColor = [UIColor clearColor];
        SectionHeaderView *view = [[SectionHeaderView alloc] initWithFrame:CGRectMake(0, 10, self.view.width, 30)];
        
        if (section == 0) {
            return nil;
        }
        
        switch (section) {
            case CellTypeContentText:
                view.labelText = @"内容摘要";
                break;
            case CellTypeRelatedList:
                view.labelText = @"游戏相关";
            default:
                break;
        }
        [backgroundView addSubview:view];
        return backgroundView;
    }
    

    iOS 11下的一个小问题

    image

    在介绍页点击“收起”和“全文”后移动到攻略页的时候会发现,内容向下偏移了。
    找了一下原因是点击按钮的时候会reload cell中的数据,导致tableView的偏移量发生的变化。下面引用来自腾讯Bugly

    Self-Sizing在iOS11下是默认开启的,Headers, footers, and cells都默认开启Self-Sizing,所有estimated 高度默认值从iOS11之前的 0 改变为UITableViewAutomaticDimension。

    如果目前项目中没有使用estimateRowHeight属性,在iOS11的环境下就要注意了,因为开启Self-Sizing之后,tableView是使用estimateRowHeight属性的,这样就会造成contentSize和contentOffset值的变化,如果是有动画是观察这两个属性的变化进行的,就会造成动画的异常,因为在估算行高机制下,contentSize的值是一点点地变化更新的,所有cell显示完后才是最终的contentSize值。因为不会缓存正确的行高,tableView reloadData的时候,会重新计算contentSize,就有可能会引起contentOffset的变化。

    这时候需要在tableViewController中添加如下即可解决。

        self.tableView.estimatedRowHeight = 0;
        self.tableView.estimatedSectionFooterHeight = 0;
        self.tableView.estimatedSectionHeaderHeight = 0;
    

    就当认为已经解决的时候,问题再次出现。

    又一个问题诞生

    对tableView进行快速向下滚动操作,会出现和上图一样的情况,而且灰色区域每次都不一致。于是快速滚动一次,打印出了数据。 image

    这里可以很明显看出精度很不精确,keyPath无法获取准确的停止点,因此同步两个页面的tableView的contentOffset会不准确。这里需要的精度至少是1。
    原本以为...scrollViewDidScroll...可以获取到想要的精度,尝试之后发现也不可行。
    后来想到一种不太优雅却能解决问题的方法,既然还未显示的tableView的contentOffset会偏下,那么如果小于头部视图的y值,就直接设置成这个值就好了。修改方法为

    if ([keyPath isEqualToString:@"contentOffset"]) {
            CGFloat headerViewScrollStopY = (int)SCREEN_HEIGHT/6 - 15.0;
            UITableView *tableView = object;
            CGFloat contentOffsetY = tableView.contentOffset.y;
            // 滑动没有超过停止点,头部视图跟随移动
            if (contentOffsetY < headerViewScrollStopY) {
                self.headerView.y = - tableView.contentOffset.y;
                // 同步tableView的contentOffset
                for (UITableViewController *vc in self.childViewControllers) {
                    if (vc.tableView.contentOffset.y != tableView.contentOffset.y) {
                        vc.tableView.contentOffset = tableView.contentOffset;
                    }
                }
            // 头部视图固定位置
            } else {
                self.headerView.y = - headerViewScrollStopY;
                // 解决高速滑动下tableView偏移量错误的问题
                if (self.headerView.segmentedControl.selectedSegmentIndex == 0) {
                    UITableViewController *vc = self.childViewControllers[1];
                    if (vc.tableView.contentOffset.y < headerViewScrollStopY) {
                        CGPoint contentOffset = vc.tableView.contentOffset;
                        contentOffset.y = headerViewScrollStopY;
                        vc.tableView.contentOffset = contentOffset;
                    }
                } else {
                    UITableViewController *vc = self.childViewControllers[1];
                    if (vc.tableView.contentOffset.y < headerViewScrollStopY) {
                        CGPoint contentOffset = vc.tableView.contentOffset;
                        contentOffset.y = headerViewScrollStopY;
                        vc.tableView.contentOffset = contentOffset;
                    }
                }
            }
        }
    

    源码和博客相辅相成,博客帮助理解,源码更具有结构性。推荐下载源码来看一下帮助理解。觉得有帮助的希望来个star。
    源码地址:https://github.com/HasjOH/NestedPage

    参考:
    https://ashfurrow.com/blog/putting-a-uicollectionview-in-a-uitableviewcell/
    https://mp.weixin.qq.com/s/AZFrqL9dnlgA6Vt2sVhxIw
    https://stackoverflow.com/questions/1054558/vertically-align-text-to-top-within-a-uilabel

    相关文章

      网友评论

          本文标题:iOS嵌套式游戏详情页

          本文链接:https://www.haomeiwen.com/subject/damlkftx.html