简介:
本文中两种方法实现了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]做同步处理,否则会延迟写入。
+ (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;
}