IOS开发 - 引导页的两种实现 - UICollectionView和UIScrollView

本文介绍两种iOS引导页实现方法:使用UICollectionViewController和UIScrollView。通过模型SCGuide包装cell的图片名称,实现视图的更新。文章还介绍了如何判断是否显示引导页及处理状态栏覆盖的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介:

本文中两种方法实现了IOS开发中引导页面的基本功能,在使用上差别不大。UICollecitonView比UIScrollView多了一个cell重用,以及每个Cell都是通过数据源方法设置。而UIScrollView则是一次性完成整个scrollView的初始化,以下通过两种方法的对比,回顾一下整个引导页面实现的过程。


判断是否有新版本(准备工作)

首先通过[NSUserDefaults standardUserDefaults]来存储当前的版本号。NSUserDefaults用于存储数据量小的数据,例如用户配置。

这里我给存储NSString的方法写了一个工具,分别是把版本号写入系统的文件中,从系统的文件中读取。

+ (id)objectForKey:(NSString *)defaultName {
    return [[NSUserDefaults standardUserDefaults] objectForKey:defaultName];
}
+ (void)setObject:(id)value forKey:(NSString *)defaultName {
    [[NSUserDefaults standardUserDefaults] setObject:value forKey:defaultName];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

注意:通过[NSUserDefaults standardUserDefaults]写入数据,需要用[SCUserDefaultssynchronize]做同步处理,否则会延迟写入。


有了读写版本数据的工具代码,接下来就可以分析是否需要进入引导页面。从CFBundleVersion字典中取出版本号,然后判断,如果和当前版本不相等,就执行进入引导页面代码。

+ (void)setRootViewController:(UIWindow *)window {
    NSDictionary *dict = [NSBundle mainBundle].infoDictionary;
    NSString *curVersion = dict[@"CFBundleVersion"];
    NSString *lastVersion = [SCSaveTool objectForKey:@"version"];
    JLTabBarController *tabBar = [[JLTabBarController alloc] init];
    window.rootViewController = tabBar;
    if (![curVersion isEqualToString:lastVersion]) { // 有新版本,加引导页
        [SCSaveTool setObject:curVersion forKey:@"version"];
        // 进入引导页面
        SCGuideController *gv = [[SCGuideController alloc] init];
        [tabBar addChildViewController:gv];
        [tabBar.view addSubview:gv.view];
    }
}
注意:引导页面控制器必须设置为根控制器的子控制器,否则UICollectionViewCell会无法显示。


两种引导页代码的实现

这里分别通过创建UICollectionViewController和UIViewController来实现两种引导页的整个逻辑。

在UICollectionViewController中,每个引导页是一个单独的UICollectionViewCell,我用一个模型Guide来包装cell的图片名字,完成每个对应的Cell的图片更新。

在UIViewController中,每个引导页是自定义的view对象,继承至UIView,同样用模型Guide来包装每个cell的图片名字,通过一次性的实例化,把整个scrollView添加至控制器视图。

以下目录1为模型代码,2为视图代码,3为控制器代码。

1、两种方法模型代码是一样的:

.h文件:

@interface SCGuide : NSObject

@property (nonatomic, copy) NSString *imageName;

+ (instancetype)guideWithImageName:(NSString *)imageName;

@end

.m文件:

#import "SCGuide.h"

@implementation SCGuide

+ (instancetype)guideWithImageName:(NSString *)imageName {
    SCGuide *guide = [[self alloc] init];
    guide.imageName = imageName;
    return guide;
}

@end

该模型中,只有一个主图,也就是每个显示到cell的主背景图的图片名,这里虽然只给模型加了一个属性,但是为了代码的扩展性(也许会给每个cell添加一些其他的控件),这里的代码还是遵循MVC设计模式来写。


2.1、collectionViewCell代码:

.h文件:

#import <UIKit/UIKit.h>
@class SCGuide;

@interface SCGuideCell : UICollectionViewCell

@property (nonatomic, strong) SCGuide *guide;

@end

.m文件:

#import "SCGuideCell.h"
#import "SCGuide.h"

@interface SCGuideCell()

@property (nonatomic, weak) UIImageView *imageView;

@end

@implementation SCGuideCell

- (UIImageView *)imageView {
    if (!_imageView) {
        UIImageView *imageView = [[UIImageView alloc] init];
        [self addSubview:imageView];
        _imageView = imageView;
    }
    return _imageView;
}

- (void)setGuide:(SCGuide *)guide {
    self.imageView.image = [UIImage imageNamed:guide.imageName];
    _guide = guide;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    self.imageView.frame = self.bounds;
}

@end

该cell类中,通过重写cell的guide属性set方法,来完成cell内部的属性初始化,包括为所有的子控件布局,这样很好地遵循了开闭原则。视图只对外暴露了一个模型属性,通过设置模型,就可以对模型对应的视图完成所有的封装完的操作。


2.2 UIView代码(用来描述scrollView中每个cell)

.h文件:

#import <UIKit/UIKit.h>
@class SCGuide;

@interface SCGuideCell : UIView

@property (nonatomic, strong) SCGuide *guide;

- (instancetype)initWithIndex:(int)index;

@end

.m文件:

#import "SCGuideCell.h"
#import "SCGuide.h"

@interface SCGuideCell()

@property(nonatomic, weak) UIImageView *imageView;

@end

@implementation SCGuideCell

- (instancetype)initWithIndex:(int)index {
    if (self = [super init]) {
        CGRect frame = [UIScreen mainScreen].bounds;
        CGFloat cellW = frame.size.width;
        CGFloat cellH = frame.size.height;
        CGFloat cellX = cellW * index;
        CGFloat cellY = 0;
        self.frame = CGRectMake(cellX, cellY, cellW, cellH);
    }
    return self;
}

- (UIImageView *)imageView {
    if (!_imageView) {
        UIImageView *imageView = [[UIImageView alloc] init];
        [self addSubview:imageView];
        _imageView = imageView;
    }
    return _imageView;
}

- (void)setGuide:(SCGuide *)guide {
    self.imageView.image = [UIImage imageNamed:guide.imageName];
    _guide = guide;
}

- (void)layoutSubviews {
    self.imageView.frame = self.bounds;
}
@end

该代码中,头文件除了提供了对应模型的接口外,还提供了一个便利初始化方法,该方法主要是为了需要得到每个cell的下标,来完成cell的frame的不同初值(因为所有cell的初始化是是一次性完成,而不是像collectionView会自动布局)

3.1UICollectionViewController的实现

#import "SCGuideController.h"
#import "SCGuide.h"
#import "SCGuideCell.h"

#define SCGuidePageCount 4
@interface SCGuideController ()

@property (nonatomic, strong) NSMutableArray *guides;

@end

@implementation SCGuideController

static NSString * const reuseIdentifier = @"GuideCell";

- (instancetype)init {
    if (self = [super init]) {
        UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
        layout.itemSize = SCScreenSize;
        layout.minimumLineSpacing = 0;
        layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        
        self = [self initWithCollectionViewLayout:layout];
        self.view.frame = SCScreenBounds;
        self.collectionView.pagingEnabled = YES;
        self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, SCScreenWidth);
        self.collectionView.showsHorizontalScrollIndicator = NO;
        self.collectionView.bounces = NO;
        self.collectionView.backgroundColor = [UIColor clearColor];
        NSLog(@"%@", self.collectionView);
    }
    return self;
}

#pragma mark - 懒加载方法
- (NSMutableArray *)guides {
    if (!_guides) {
        _guides = [NSMutableArray array];
        for (int i = 0; i < SCGuidePageCount; i++) {
            NSString *imageName = [NSString stringWithFormat:@"guide%dBackground", i+1];
            SCGuide *guide = [SCGuide guideWithImageName:imageName];
            [_guides addObject:guide];
        }
    }
    return _guides;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.collectionView registerClass:[SCGuideCell class] forCellWithReuseIdentifier:reuseIdentifier];
    
}

#pragma mark <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return SCGuidePageCount;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    SCGuideCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    
    cell.guide = self.guides[indexPath.row];
    
    return cell;
}

#pragma mark <UIScrollViewDelegate>
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (self.collectionView.contentOffset.x == self.collectionView.contentSize.width) {
        [self.view removeFromSuperview];
    }
}

@end

控制器只有.m文件,因为.h文件不需要对外提供接口。该实例代码中,提供了4张主背景图,所以在顶部定义了一个cell数量的宏,方便后续修改。

既然有了前面模型和视图类的铺垫,控制器中的业务逻辑就非常清晰了。

1、拿到数据,用一个数组来保存它(guides)

2、重写控制器的初始化方法,给它指定一个layout(流水布局)

3、给控制器的collectionView设置一些属性,注意这里还需要把控制器的view的frame改成屏幕的bounds,因为默认系统的view高度为减去状态栏的20个点。

4、设置数据源,从数据数组中调到模型,赋值给每个cell,完成UI的更新。

5、设置scrollView的代理方法,实现当把最后一个cell划出屏幕时,移除引导页面,从而显示根控制器的页面。


3.2 UIViewController的实现

.m文件:

#import "SCGuideController.h"
#import "SCGuide.h"
#import "SCGuideCell.h"

@interface SCGuideController ()<UIScrollViewDelegate>

@property (nonatomic, weak) UIScrollView *scrollView;
@property (nonatomic, strong) NSMutableArray *guides;

@end

@implementation SCGuideController

#define SCGuidePageCount 4

#pragma mark - 懒加载方法
- (NSMutableArray *)guides {
    if (!_guides) {
        _guides = [NSMutableArray array];
        for (int i = 0; i < SCGuidePageCount; i++) {
            NSString *imageName = [NSString stringWithFormat:@"guide%dBackground", i+1];
            SCGuide *guide = [SCGuide guideWithImageName:imageName];
            [_guides addObject:guide];
        }
    }
    return _guides;
}

- (UIScrollView *)scrollView {
    if (!_scrollView) {
        CGRect frame = [UIScreen mainScreen].bounds;
        CGFloat imgW = frame.size.width;
        CGFloat imgH = frame.size.height;

        UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:frame];
        scrollView.contentSize = CGSizeMake(imgW * SCGuidePageCount, imgH);
        scrollView.showsHorizontalScrollIndicator = NO;
        scrollView.pagingEnabled = YES;
        scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, imgW);
        scrollView.bounces = NO;
        scrollView.delegate = self;
        
        [self.view addSubview:scrollView];
        _scrollView = scrollView;
    }
    return _scrollView;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self setGuideCell];
}

- (void)setGuideCell {
    for (int i = 0; i < SCGuidePageCount; i++) {
        SCGuideCell *cell = [[SCGuideCell alloc] initWithIndex:i];
        cell.guide = self.guides[i];
        [self.scrollView addSubview:cell];
    }

}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (self.scrollView.contentOffset.x == self.scrollView.contentSize.width) {
        [self.view removeFromSuperview];
    }
}
@end
通过scrollView来实现引导页和collection有一些区别。类扩展中除了需要一个包含模型的数组属性外,还需要一个UIScrollView属性。因为控制器的view默认颜色是透明的,所以我们不需要对view的属性进行修改,而只需要把初始化好的scrollVIew添加至控制器的视图即可。

业务逻辑因为少了流水布局和数据源方法,所以和collectionView的实现也有一些差别。

1、拿到数据。

2、初始化scrollView,并设置相应的属性。

3、设置cell,每次都手动初始化一个我们前面自定义好的cell,并通过模型赋值,把所有cell添加至scrollView。

4、设置监听滚动方法,实现最后一个cell划出屏幕时,移除当前view。


总结

两种方法没有什么太大的区别,在实际开发中,可任选一种实现。

昨天发现这段代码有个bug,状态栏无法被引导页面所覆盖。因为状态栏是比较特殊的视图,所以想要让引导页视图覆盖至状态栏上,我想到的方法是单独创建一个UIWindow,然后把引导页面添加至该window上,注意UIWIndow需要将设置windowLevelAlert,让窗口的level高于状态栏即可。

在AppDelegate中添加代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    self.window = [[UIWindow alloc] initWithFrame:SCScreenBounds];
    JLTabBarController *tabBar = [[JLTabBarController alloc] init];
    self.window.rootViewController = tabBar;
    [self.window makeKeyAndVisible];
    
    self.windowGuide = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.windowGuide.windowLevel = UIWindowLevelAlert;
    SCGuideController *gv = [[SCGuideController alloc] init];
    self.windowGuide.rootViewController = gv;
    [self.windowGuide makeKeyAndVisible];
    
    return YES;
}


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值