iOS实现一个简单的页面切换控件
背景
项目需要开发一个游戏类型的壳版本,壳版本的UI已经出来了,剩下的事情就是复制粘贴了,重复的工作太没意思了,不如来点有挑战的,把项目重构一遍吧,顺便复制粘贴下,这样才不会那么枯燥无聊吧。好吧,说干就干,谁叫我姓张呢?项目重构的地方有很多,这次会讲到一个经常用到的页面切换控件的重构以及实现一个简单的Demo,后续会放出一些其他方面重构的实践经验。
结果
项目Demo:PTPageKit
老生常谈没图没真相,还是先上图:
Demo2
图片很挫,大概做的就是这么一个东西,上面的切换按钮,下面的是按钮对应的页面。点击按钮,下面的页面会随之变化。
分析
把系统分为三部分,Page 头部
,Page 内容
, Page 中间者
,每一部分对应的对输入和输出进行抽象,对各个部分进行组件化。
Header分析
Header的输入(公有接口部分):
- 选中Header Item
- 刷新Header视图
- 滚动到Header某个位置
Header的输出(Delegate和DataSource部分):
- 已经选中Header Item
- Header Item的数量
- Header Item展示的View
- Header Item的元数据
Body分析
Body的输入(公有接口部分):
- 选中Body页面
- 刷新Body视图
Body的输出(Delegate和DataSource部分):
- 已经滚动到某个Body
- Body移动到某个位置
- 有多少个Body
- Body对应的ViewController
中间者分析
中间者作为了Body和Header的Delegate和DataSource,为Body和Header提供数据并且连接了这两者。中间者隐藏了实现的细节,客户端(使用者)最终只要和中间者交互即可,传递需要的参数交给中间者去处理,中间者会做好子元素的UI渲染和UI交互。
组件图
下面是对这个系统的输入输出进行抽象的组件图。
组件图
Page 头部
,Page 内容
, Page 中间者
这三者依赖于抽象基类,没有依赖具体的实现类,所以他们是相互独立并且可替换的。对于某个具体的实现,客户端(使用者)只要和 Page 中间者
这个对象打交道,传递需要的参数:页面的Header元数据和页面的ViewContoller数组即可,
实现
抽象的基类
抽象的基类是针对组件图的交互实现了一个基本的框架,并不包含任何的UI实现,定义了组件交互的规范。
Body
//
// PTBasePageBodyView.h
// PlushGame
//
// Created by aron on 2017/11/23.
// Copyright © 2017年 aron. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol PTPageBodyViewDelegate, PTPageBodyViewDataSource;
@interface PTBasePageBodyView : UIView
@property (nonatomic, weak) id<PTPageBodyViewDelegate> delegate;
@property (nonatomic, weak) id<PTPageBodyViewDataSource> dataSource;
- (void)moveToIndex:(NSInteger)index;
- (void)reloadData;
@end
//______________________________________________________________________________________________________________
@protocol PTPageBodyViewDelegate <NSObject>
- (void)bodyView:(PTBasePageBodyView *)bodyView didSelectItemAtIndex:(NSInteger)index;
- (void)bodyView:(PTBasePageBodyView *)bodyView didScrollWithOffset:(CGFloat)offset;
@end
//_______________________________________________________________________________________________________________
@protocol PTPageBodyViewDataSource <NSObject>
@required
- (NSInteger)numberOfItemsInBodyView:(PTBasePageBodyView *)bodyView;
- (UIViewController *)bodyView:(PTBasePageBodyView *)bodyView viewControllerAtIndex:(NSInteger)index;
@end
Header
//
// PTBasePageHeaderView.h
// PlushGame
//
// Created by aron on 2017/11/23.
// Copyright © 2017年 aron. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, PTPageHeaderStyle) {
PTPageHeaderStyleDefault = 0,
};
@protocol PTPageHeaderViewDelegate, PTPageHeaderViewDataSource;
@interface PTBasePageHeaderView : UIView
@property (assign, nonatomic) PTPageHeaderStyle headerStyle;
@property (nonatomic, weak) id<PTPageHeaderViewDelegate> delegate;
@property (nonatomic, weak) id<PTPageHeaderViewDataSource> dataSource;
- (void)moveToIndex:(NSInteger)index;
- (void)animateMoveToOffset:(CGFloat)offset;
- (void)reloadData;
- (NSInteger)currentIndex;
@end
//______________________________________________________________________________________________________________
@protocol PTPageHeaderViewDelegate <NSObject>
- (void)headerView:(PTBasePageHeaderView *)headerView didSelectItemAtIndex:(NSInteger)index;
@end
//_______________________________________________________________________________________________________________
@protocol PTPageHeaderViewDataSource <NSObject>
@required
- (NSInteger)numberOfItemsInHeaderView:(PTBasePageHeaderView *)headerView;
@optional
- (UIView *)pageView:(PTBasePageHeaderView *)headerView headerItemViewAtIndex:(NSInteger)index;
- (id)pageView:(PTBasePageHeaderView *)headerView headerItemAtIndex:(NSInteger)index;
@end
中间者
Body.h
//
// PTBasePageViewController.h
// PlushGame
//
// Created by aron on 2017/11/23.
// Copyright © 2017年 aron. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "PTBasePageHeaderView.h"
#import "PTBasePageBodyView.h"
@protocol PTBasePageViewDataSource <NSObject>
// 需要添加到ViewController的View上,默认的是self.view
- (UIView*)pt_container;
@end
@interface PTBasePageViewController : UIViewController
<PTPageHeaderViewDelegate, PTPageHeaderViewDataSource,
PTPageBodyViewDelegate, PTPageBodyViewDataSource>
@property (nonatomic, weak) id<PTBasePageViewDataSource> dataSource;
@property (nonatomic, strong) PTBasePageHeaderView *headerView;
@property (strong, nonatomic) PTBasePageBodyView *bodyView;
@property (nonatomic, strong) NSArray<id>* pageHeaderTitles;
@property (nonatomic, strong) NSArray<UIViewController*>* pageBodyViewControllers;
@property (nonatomic, assign) NSInteger defaultIndex;
- (void)reloadData;
- (void)preViewDidLoadContainer;
- (UIView*)container;
@end
Body.m
//
// PTBasePageViewController.m
// PlushGame
//
// Created by aron on 2017/11/23.
// Copyright © 2017年 aron. All rights reserved.
//
#import "PTBasePageViewController.h"
@interface PTBasePageViewController ()
@end
@implementation PTBasePageViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self preViewDidLoadContainer];
[self.container addSubview:self.bodyView];
[self.container addSubview:self.headerView];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)reloadData {
[self.headerView reloadData];
[self.bodyView reloadData];
}
- (void)preViewDidLoadContainer {
}
- (UIView*)container {
UIView* container = nil;
if ([self.dataSource respondsToSelector:@selector(pt_container)]) {
container = [self.dataSource pt_container];
} else {
container = self.view;
}
return container;
}
#pragma mark - ......::::::: Lazy load :::::::......
- (PTBasePageHeaderView *)headerView {
if (nil == _headerView) {
_headerView = [[PTBasePageHeaderView alloc] init];
_headerView.dataSource = self;
_headerView.delegate = self;
}
return _headerView;
}
- (PTBasePageBodyView *)bodyView {
if (nil == _bodyView) {
_bodyView = [[PTBasePageBodyView alloc] init];
_bodyView.dataSource = self;
_bodyView.delegate = self;
}
return _bodyView;
}
#pragma mark - ......::::::: PTPageHeaderViewDelegate, PTPageHeaderViewDataSource :::::::......
- (void)headerView:(PTBasePageHeaderView *)headerView didSelectItemAtIndex:(NSInteger)index {
[self.bodyView moveToIndex:index];
}
- (NSInteger)numberOfItemsInHeaderView:(PTBasePageHeaderView *)headerView {
return self.pageHeaderTitles.count;
}
- (UIView *)pageView:(PTBasePageHeaderView *)headerView headerItemViewAtIndex:(NSInteger)index {
return nil;
}
- (id)pageView:(PTBasePageHeaderView *)headerView headerItemAtIndex:(NSInteger)index {
return self.pageHeaderTitles[index];
}
#pragma mark - ......::::::: PTPageBodyViewDelegate, PTPageBodyViewDataSource :::::::......
- (void)bodyView:(PTBasePageBodyView *)bodyView didSelectItemAtIndex:(NSInteger)index {
[self.headerView moveToIndex:index];
}
- (void)bodyView:(PTBasePageBodyView *)bodyView didScrollWithOffset:(CGFloat)offset {
[self.headerView animateMoveToOffset:offset];
}
- (NSInteger)numberOfItemsInBodyView:(PTBasePageBodyView *)bodyView {
return self.pageBodyViewControllers.count;
}
- (UIViewController *)bodyView:(PTBasePageBodyView *)bodyView viewControllerAtIndex:(NSInteger)index {
if (index >= 0 && index < self.pageBodyViewControllers.count) {
return self.pageBodyViewControllers[index];
}
return nil;
}
@end
具体的实现
具体的实现需要继承 PTBasePageBodyView
PTBasePageHeaderView
PTBasePageViewController
实现一些UI和交互的细节,比如Header中按钮的布局和样式等。可以参考项目中的一个子模块 PTSimplePageKitImpl
,打开 Example Demo项目既可以看到效果,具体的实现不在赘述。
后记
项目Demo:PTPageKit