最终效果图:
控制器继承关系图:
说明:
点击主控制器左侧的Dock上的按钮,
比如【团购】、【收藏】、【地图】时,
实现的功能有许多相同之处。
具体说明如下:
点击【团购】,以九宫格的形式显示一个个团购,
并且,点击一个Cell时,展示该Cell对应的团购详情
点击【收藏】,以九宫格的形式显示一个个已经归档的团购模型,
并且,点击一个Cell时,展示该Cell对应的团购详情
点击【地图】,以MapView上一个个大头针的形式显示团购模型(通过城市名+经度+纬度+半径作参数发送请求)
并且,点击一个大头针时,展示该Cell对应的团购详情
上述各控制器的主要功能和方法声明如下:
抽取的父类如下:
父类ShowDealDetailController
负责创建并展示和隐藏封装的【团购详情控制器】
私有成员:Cover
// // ShowDealDetailController.h // 帅哥_团购 // // Created by beyond on 14-8-20. // Copyright (c) 2014年 com.beyond. All rights reserved. // 基类,父类,展示 订单详情控制器的控制器,当点击XXX时,需要展示团购详情的时候,就继承这个控制器即可,内部实现了创建并显示订单订单详情控制器,以及隐藏订单详情控制器 #import <UIKit/UIKit.h> @class Deal; @interface ShowDealDetailController : UIViewController // 显示订单详情的控制器所依赖的数据模型(数据源) - (void)showDetail:(Deal *)deal; @end
// // ShowDealDetailController.m // 帅哥_团购 // // Created by beyond on 14-8-20. // Copyright (c) 2014年 com.beyond. All rights reserved. // 基类,父类,展示 订单详情控制器的控制器,当点击XXX时,需要展示团购详情的时候,就继承这个控制器即可,内部实现了创建并显示订单订单详情控制器,以及隐藏订单详情控制器 #import "ShowDealDetailController.h" #import "Cover.h" // 真正的用xib封装的详情控制器,创建时,要传入deal数据源 #import "DealDetailController.h" // 自己封装的全局统一样式的导航控制器 #import "BeyondNavigationController.h" // 真正的用xib封装的详情控制器 其高度可变,但是宽度一般是要固定的 #define kDealDetailVCWidth 600 @interface ShowDealDetailController () { // 遮盖 Cover *_cover; } @end @implementation ShowDealDetailController #pragma mark 显示详情控制器 - (void)showDetail:(Deal *)deal { // 1.显示遮盖 if (_cover == nil) { _cover = [Cover coverWithTarget:self action:@selector(hideDetail)]; } // self只是子(根)控制器,self的上方是导航控制器的导航栏,因此self.navigationController是拿到整个导航控制器的view(包括导航栏和它的根(子)控制器) // ????导航控制器的view的宽高(包括导航栏)????因为self本控制器,每次出场,都是先经过导航控制器包装过的,因为self的顶部还有导航栏 _cover.frame = self.navigationController.view.bounds; // 创建时透明,之后动画变黑 _cover.alpha = 0; [UIView animateWithDuration:kDefaultAnimDuration animations:^{ [_cover alphaReset]; }]; // ???? 添加到导航控制器的view最上面,(盖住导航栏和其根控制器) [self.navigationController.view addSubview:_cover]; // 2.创建并展示团购详情控制器 DealDetailController *detailVC = [[DealDetailController alloc] init]; // 重要~~~其导航条左边是关闭,点击后调用本控制器的方法执行动画关闭,创建出来的DealDetailController,之所以不到DealDetailController内部去设置这个按钮,是因为在内部无法方便地调用外部的这个隐藏DealDetailController的方法 detailVC.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithIcon:@"btn_nav_close.png" highlightedIcon:@"btn_nav_close_hl.png" target:self action:@selector(hideDetail)]; // 需提供数据源,供其内部的从xib生成的子控件显示数据 detailVC.deal = deal; // 用导航控制器包装 真正的团购详情控制器 BeyondNavigationController *nav = [[BeyondNavigationController alloc] initWithRootViewController:detailVC]; // 为实现详情控制器的抽屉效果,监听pan手势拖拽 [nav.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(drag:)]]; // 详情控制器始终靠右边,所以左边距伸缩,高度也伸缩 nav.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin; // ????在遮罩的右边,意思是先完全看不见,然后动画慢慢向左移动出场??? // 真正的用xib封装的详情控制器 其高度可变,但是宽度一般是要固定的 nav.view.frame = CGRectMake(_cover.frame.size.width, 0, kDealDetailVCWidth, _cover.frame.size.height); // 当2个控制器互为父子关系时,它们的view也是互为父子关系,因为如果只加view,不加控制器,那么控制器在本方法调用完毕之后就会被销毁,因为其为局部变量 [self.navigationController.view addSubview:nav.view]; [self.navigationController addChildViewController:nav]; // 动画向左慢慢移动出场,显示出详情控制器 [UIView animateWithDuration:kDefaultAnimDuration animations:^{ CGRect f = nav.view.frame; f.origin.x -= kDealDetailVCWidth; nav.view.frame = f; }]; } #pragma mark 隐藏详情控制器 - (void)hideDetail { // 取得包装了详情控制器的导航控制器,动画隐藏其view UIViewController *nav = [self.navigationController.childViewControllers lastObject]; [UIView animateWithDuration:0.3 animations:^{ // 1.隐藏遮盖 _cover.alpha = 0; // 2.隐藏控制器 CGRect f = nav.view.frame; f.origin.x += kDealDetailVCWidth; nav.view.frame = f; } completion:^(BOOL finished) { [_cover removeFromSuperview]; [nav.view removeFromSuperview]; [nav removeFromParentViewController]; }]; } #pragma mark - 让详情控制器有抽屉效果,外加弹簧效果 - (void)drag:(UIPanGestureRecognizer *)pan { // 向左走为负 CGFloat tx = [pan translationInView:pan.view].x; // 手势结束时,即松手 if (pan.state == UIGestureRecognizerStateEnded) { CGFloat halfW = pan.view.frame.size.width * 0.5; if (tx >= halfW) { // 已经往右边挪动超过一半了 [self hideDetail]; } else { [UIView animateWithDuration:kDefaultAnimDuration animations:^{ pan.view.transform = CGAffineTransformIdentity; }]; } } else { // 移动控制器的view if (tx < 0) { // 向左边拽 tx *= 0.4; } pan.view.transform = CGAffineTransformMakeTranslation(tx, 0); } } @end
父类BaseDealListController
负责创建并维护一个CollectionView,并且向子类要数据源totalDealsArr
// // BaseDealListController.h // 帅哥_团购 // // Created by beyond on 14-8-21. // Copyright (c) 2014年 com.beyond. All rights reserved. // 位于中间层的父类,继承自ShowtDealDetailController,自动拥有了点击了XXX,只要供给数据源,就动画展示团购详情的功能,并自动拥有了点击遮盖,动画隐藏掉团购详情控制器的功能....自己只负责建立维护一个CollectionView(九宫格),并且九宫格的数据源(团购对象数组)由子类 自己提供 #import "ShowDealDetailController.h" @interface BaseDealListController : ShowDealDetailController { UICollectionView *_collectionView; } // 九宫格的数据源(团购对象数组)由子类 自己提供 - (NSArray *)totalDeals; // 所有的团购数据 @end
// // BaseDealListController.m // 帅哥_团购 // // Created by beyond on 14-8-21. // Copyright (c) 2014年 com.beyond. All rights reserved. // 位于中间层的父类,继承自ShowtDealDetailController,自动拥有了点击了XXX,只要供给数据源,就动画展示团购详情的功能,并自动拥有了点击遮盖,动画隐藏掉团购详情控制器的功能....自己只负责建立维护一个CollectionView(九宫格),并且九宫格的数据源(团购对象数组)由子类 自己提供 #import "BaseDealListController.h" #import "Deal.h" #import "DealCell.h" // 每一个格子的宽和高 #define kItemW 250 #define kItemH 250 @interface BaseDealListController ()<UICollectionViewDataSource, UICollectionViewDelegate> // 自己创建并维护一个九宫格,数据源(团购对象数组)由子类提供 @property (nonatomic, strong) UICollectionView *collectionView; @end @implementation BaseDealListController #pragma mark - 生命周期方法 - (void)viewDidLoad { [super viewDidLoad]; // 1.创建自己的collectionView [self addCollectionView]; // 2.注册cell格子要用到的xib文件 [self.collectionView registerNib:[UINib nibWithNibName:@"DealCell" bundle:nil] forCellWithReuseIdentifier:@"DealCell"]; // 3.设置collectionView永远支持垂直滚动,为下拉刷新准备(弹簧) self.collectionView.alwaysBounceVertical = YES; // 4.设置collectionView的背景色 self.collectionView.backgroundColor = kGlobalBg; } // 1.创建自己的collectionView - (void)addCollectionView { // 创建一个流布局,必须指定 UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; // 设置流布局里面的每一个格子宽和高,即每一个网格的尺寸 layout.itemSize = CGSizeMake(kItemW, kItemH); // 每一行之间的间距 layout.minimumLineSpacing = 20; // 指定的流布局创建一个collectionView,并且用成员变量记住 self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; // 高度和宽度自动伸缩 self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; self.collectionView.delegate = self; self.collectionView.dataSource = self; [self.view addSubview:self.collectionView]; } #pragma mark 在viewWillAppear和viewDidAppear中可以取得view最准确的宽高(width和height) // 重要~~~因为在控制器创建时,宽默认是768,高默认是1024,不管横竖屏 // 只有在viewWillAppear和viewDidAppear方法中,可以取得view最准确的(即实际的)宽和高(width和height) - (void)viewWillAppear:(BOOL)animated { // 默认计算layout [self didRotateFromInterfaceOrientation:0]; } #pragma mark - 父类方法 // 拦截,屏幕即将旋转的时候调用(控制器监控屏幕旋转) - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { //log(@"屏幕即将旋转"); } #pragma mark 屏幕旋转完毕的时候调用 // 拦截,屏幕旋转完毕的时候调用 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { // 1.取出创建CollectionViewController时传入的的UICollectionViewFlowLayout UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout; // 2.计算间距 CGFloat v = 0; CGFloat h = 0; CGFloat height = self.view.frame.size.height -44; CGFloat width = self.view.frame.size.width; if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation) ) { // 横屏的间距 v = (height - 2 * kItemH) / 3; h = (width - 3 * kItemW) / 4; } else { // 竖屏的间距 v = (height - 3 * kItemH) / 4; h = (width - 2 * kItemW) / 3; } // 3.动画调整格子之间的距离 [UIView animateWithDuration:4.0 animations:^{ // 上 左 下 右 四个方向的margin layout.sectionInset = UIEdgeInsetsMake(h, h, v, h); // 每一行之间的间距 layout.minimumLineSpacing = h; }]; } #pragma mark - collectionView代理方法 // 共有多少个Item(就是格子Cube),询问子类 - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.totalDeals.count; } #pragma mark 刷新数据的时候会调用(reloadData) #pragma mark 每当有一个cell重新进入屏幕视野范围内就会调用 // 生成每一个独一无二的格子,询问子类 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *ID = @"DealCell"; DealCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath]; cell.deal = self.totalDeals[indexPath.row]; return cell; } // 点击了一个格子时,showDealDetailVC - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 再次调用父类的方法,展示dealDetail控制器 [self showDetail:self.totalDeals[indexPath.row]]; } #pragma mark - 生命周期方法 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end
子类 DealListController
只需继承即可,并向父类提供数据源totalDealsArr
// // DealListController.h // 帅哥_团购 // // Created by beyond on 14-8-14. // Copyright (c) 2014年 com.beyond. All rights reserved. // 点击dock上面的【团购】按钮对应的控制器,上面是导航栏,导航栏右边是searchBar,导航栏左边是一个大按钮(TopMenu)(内部由三个小按钮组成<TopMenuItem>) // 本控制器继承自BaseDealListController,便自动拥有了九宫格,只需要为其提供数据即可,而BaseDealListController又继承自ShowDealDetailVc,拥有展示闲情的功能 #import "BaseDealListController.h" @interface DealListController : BaseDealListController @end
// // DealListController.m // 帅哥_团购 // // Created by beyond on 14-8-14. // Copyright (c) 2014年 com.beyond. All rights reserved. // 点击dock上面的【团购】按钮对应的控制器,上面是导航栏,导航栏右边是searchBar,导航栏左边是一个大按钮(TopMenu)(内部由三个小按钮组成<TopMenuItem>) #import "DealListController.h" // 导航栏左边是一个大按钮(顶部菜单) #import "TopMenu.h" // 封装的自定义cell #import "DealCell.h" // 点评提供的封装发送请求的类 #import "DPAPI.h" // 工具类 #import "MetaDataTool.h" // 封装请求的工具类 #import "DealRequestTool.h" // 模型类 #import "City.h" #import "Deal.h" // 二次封装的图片下载工具类 #import "ImgDownloadTool.h" #define kItemW 250 #define kItemH 250 @interface DealListController()<DPRequestDelegate,MJRefreshBaseViewDelegate> { // 用于接收服务器返回的字典数组----转化成的对象数组,供格子们显示 NSMutableArray *_deals; // 下拉刷新 MJRefreshHeaderView *_header; // 上拉加载下一页 MJRefreshFooterView *_footer; // 每次加载的页码数 int _pageNo; } @end @implementation DealListController // 继承BaseDealListController控制器,必须实现的方法,目的是为collectionView提供数据源 - (NSArray *)totalDeals { return _deals; } - (void)viewDidLoad { [super viewDidLoad]; _deals = [NSMutableArray array]; // 1.顶部导航栏的基本设置 [self setNavigationBar]; // 2.添加刷新控件 [self addRefresher]; // 0.监听到城市改变的通知了,就下拉刷新 kAddAllNotes(dataChange) } // 0.监听到城市改变的通知了,就下拉刷新 - (void)dataChange { [_header beginRefreshing]; } // 1.顶部导航栏的基本设置 - (void)setNavigationBar { // 1.右边的搜索框 UISearchBar *s = [[UISearchBar alloc] init]; s.frame = CGRectMake(0, 0, 210, 35); s.placeholder = @"请输入商品名、地址等"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:s]; // 2.左边的菜单栏 TopMenu *top = [[TopMenu alloc] init]; // 重要,TopMenu里面的item点击后,创建的PopMenu将要添加到哪儿去???就是本控制器的view top.controllerView = self.view; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:top]; } // 3.添加刷新控件 - (void)addRefresher { _header = [MJRefreshHeaderView header]; _header.scrollView = _collectionView; _header.delegate = self; _footer = [MJRefreshFooterView footer]; _footer.scrollView = _collectionView; _footer.delegate = self; } #pragma mark 刷新控件的代理方法 - (void)refreshViewBeginRefreshing:(MJRefreshBaseView *)refreshView { // 标记一下,是下拉 还是上拉 BOOL isHeader = [refreshView isKindOfClass:[MJRefreshHeaderView class]]; if (isHeader) { // 下拉刷新 // 建议先,清除前面下载的图片内存缓存 [ImgDownloadTool clear]; // 每次下拉,都是加载第一页 _pageNo = 1; } else { // 上拉加载更多,就是加载下一页 _pageNo++; } // 加载第第_pageNo页的数据 [[DealRequestTool sharedDealRequestTool] dealRequestWithPageNo:_pageNo success:^(NSArray *deals,int total_count) { if (isHeader) { // 如果是下拉加载第一页数据,则先移除旧的 [_deals removeAllObjects]; } // 1.添加新的返回的对象数组 [_deals addObjectsFromArray:deals]; // 2.刷新表格 [_collectionView reloadData]; // 3.恢复刷新状态 [refreshView endRefreshing]; // 4.根据总数量简单判断是否需要隐藏上拉控件 _footer.hidden = _deals.count >= total_count; } fail:^(NSError *error) { log(@"请求失败--%@",error); // 也要隐藏 [refreshView endRefreshing]; }]; } @end