iOS开发:一个高仿美团的团购ipad客户端的设计和实现(功能:根据拼音进行检索并展示数据,离线缓存团购数据,浏览记录与收藏记录的批量删除等)

大致花了一个月时间,利用各种空闲时间,将这个客户端实现了,在这里主要是想记录下,设计的大体思路以及实现过程中遇到的坑......

这个项目的github地址:https://github.com/wzpziyi1/GroupPurchase

主要实现的功能,用UICollectionViewController展示团购数据,根据拼音进行检索并展示数据,离线缓存团购数据,浏览记录与收藏记录的批量删除,友盟分享的集成,利用UIView+AutoLayout写布局,实现地图定位、自定义大头针等

整个项目,按功能分,有六个大的模块,每个模块里面都有各自的MVC,在各个MVC里面,再按照功能的不同,新建一下文件夹,存放实现统一功能的文件。

除此之外,还有一个Main模块,里面的Lib意为Library,放一些实现此客户端所需要用到的第三方库。Category文件夹里面,放整个项目各个地方都有可能需要用到的分类;Controller里面,放上一些最主要功能的ViewController,比如,自定义的NavigationController和TabBarController,那么整个项目的Navigation或者TabBarController都是可以根据需要继承自自定义的NavigationController,这里,我放的是:

后续,Home(首页)界面的viewController和Search(搜索)界面的viewController都是继承自ZYDealViewController的,Collect(收藏的团购记录)和Browse(浏览的团购记录)界面的viewController都是继承自ZYOldDealViewController,整个项目多个地方用到,所以我放在了这里。

Other文件夹里面,我放的是一些杂七杂八的文件,比如说appDelegate等。Tool文件夹里面,我放的是一些工具类文件.

其中,Tool和Category里面的文件不要和他类耦合,因为这些东西,我们写好一次之后,基本上是可以拖到下个项目直接使用的。

Home界面

如图:

在联网情况下,默认进来就是广州的所有团购信息,支持上拉、下拉刷新,已经适配好横竖屏。除此之外,还设置了当前地区、分类条件下,所有的团购信息都已经加载完毕了,那么将不可以进行上拉刷新操作(因为所有团购信息加载完毕,已经没有数据可以加载,那么应该要隐藏footer),默认是一次刷新10条团购信息。

除此之外,如果在当前条件下,如果没有团购信息,那么我显示的是一张提示图片:

下面这两个弹框是一样的,当初第一次写的时候,是分别写了两个popViewController,后面在进行优化的时候,发现是可以写成一个通用的双表,这样就进行了一系列的优化,使得以后遇到类似的双表界面,直接将我写好的类拖过去,就可以继续使用了,图片如下:

这是ZYHomeDropdown类,代码如下:

#import <UIKit/UIKit.h>

@class ZYHomeDropdown;

@protocol ZYHomeDropdownDataSource <NSObject>
/**
 *  左边表格一共有多少行
 */
- (NSUInteger)numberOfRowsInMainTable:(ZYHomeDropdown *)homeDropdown;

/**
 *  左边表格每一行的标题
 *
 */
- (NSString *)homeDropdown:(ZYHomeDropdown *)homeDropdown titleForRowInMainTable:(NSUInteger)row;

/**
 *  左边表格每一行的子数据
 *
 */
- (NSArray *)homeDropdown:(ZYHomeDropdown *)homeDropdown subDataForRowInMainTable:(NSUInteger)row;

@optional
/**
 *  左边表格每一行的图标
 *
 */
- (NSString *)homeDropdown:(ZYHomeDropdown *)homeDropdown normalIconForRowInMainTable:(NSUInteger)row;

/**
 *  左边表格每一行的选中图标
 *
 */
- (NSString *)homeDropdown:(ZYHomeDropdown *)homeDropdown selectedIconForRowInMainTable:(NSUInteger)row;
@end

@protocol ZYHomeDropdownDelegate <NSObject>
- (void)homeDropdown:(ZYHomeDropdown *)homeDropdown didSelectedRowInMainTable:(int)row;

- (void)homeDropdown:(ZYHomeDropdown *)homeDropdown didSelectedRowInSubTable:(int)subRow mainRow:(int)mainRow;
@end

@interface ZYHomeDropdown : UIView
@property (nonatomic, weak) id<ZYHomeDropdownDataSource>dataSource;
@property (nonatomic, weak) id<ZYHomeDropdownDelegate>delegate;

+ (instancetype)homeDropdown;
@end

#import "ZYHomeDropdown.h"
#import "ZYHomeMainCell.h"
#import "ZYHomeSubCell.h"
@interface ZYHomeDropdown () <UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *mainTableView;
@property (weak, nonatomic) IBOutlet UITableView *subTableview;
//主表中被选的cell的row
@property (nonatomic, assign) int selectedMainRow;
@end

@implementation ZYHomeDropdown

+ (instancetype)homeDropdown
{
    return [[self alloc] init];
}

- (instancetype)init
{
    if (self = [super init]) {
        self = [[[NSBundle mainBundle] loadNibNamed:@"ZYHomeDropdown" owner:nil options:nil] lastObject];
        [self commitInit];
    }
    return self;
}

- (void)commitInit
{
    self.mainTableView.delegate = self;
    self.mainTableView.dataSource = self;

    self.subTableview.delegate = self;
    self.subTableview.dataSource = self;
}

- (void)awakeFromNib
{
    self.autoresizingMask = UIViewAutoresizingNone;
}

#pragma mark ----UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (self.mainTableView == tableView) {
        return [self.dataSource numberOfRowsInMainTable:self];
    }
    else{
        return [self.dataSource homeDropdown:self subDataForRowInMainTable:self.selectedMainRow].count;
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (tableView == self.mainTableView) {
        ZYHomeMainCell *cell = [ZYHomeMainCell mainCellWithTableView:tableView];
        cell.textLabel.text = [self.dataSource homeDropdown:self titleForRowInMainTable:indexPath.row];

        if ([self.dataSource respondsToSelector:@selector(homeDropdown:normalIconForRowInMainTable:)]) {

            cell.imageView.image = [UIImage imageNamed:[self.dataSource homeDropdown:self normalIconForRowInMainTable:indexPath.row]];
        }

        if ([self.dataSource respondsToSelector:@selector(homeDropdown:selectedIconForRowInMainTable:)]) {

            cell.imageView.highlightedImage = [UIImage imageNamed:[self.dataSource homeDropdown:self selectedIconForRowInMainTable:indexPath.row]];
        }

        NSArray *subData = [self.dataSource homeDropdown:self subDataForRowInMainTable:indexPath.row];

        if (subData.count) {
            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        }
        else{
            cell.accessoryType = UITableViewCellAccessoryNone;
        }

        return cell;
    }
    else{
        ZYHomeSubCell *cell = [ZYHomeSubCell subCellWithTableView:tableView];

        NSArray *subData = [self.dataSource homeDropdown:self subDataForRowInMainTable:self.selectedMainRow];

        cell.textLabel.text = subData[indexPath.row];

        return cell;
    }
}

#pragma mark ----UITabelViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (tableView == self.mainTableView) {
        self.selectedMainRow = (int)indexPath.row;
        [self.subTableview reloadData];

        if ([self.delegate respondsToSelector:@selector(homeDropdown:didSelectedRowInMainTable:)]) {
            [self.delegate homeDropdown:self didSelectedRowInMainTable:(int)indexPath.row];
        }
    }
    else{
        if ([self.delegate respondsToSelector:@selector(homeDropdown:didSelectedRowInSubTable:mainRow:)]) {
            [self.delegate homeDropdown:self didSelectedRowInSubTable:(int)indexPath.row mainRow:self.selectedMainRow];
        }
    }
}
@end

上面代码是大概实现思路,在具体实现里面,还有一个xib文件,以及mainCell和subCell。

下面是搜索的图片,实际上,搜索实现很简单,按照拼音或者关键字来检索下,如果符合条件,就放到数组里面,最后刷新tableView即可:

另外,跨控制进行数据交换,最好是使用Notification

首页还存在这样一个界面:

也就是菜单界面,动态展示了一些模块,五角星代表这需要展示收藏模块,圆代表着展示浏览记录模块,另外两个并未实现具体功能。

实现这样一个动态的菜单,我使用的是AwesomeMenu这个第三方库,具体实现可以见它github上的示例,项目中实现菜单的具体代码:

- (void)setupAwesomeMenu
{
    //initWithImage放背景图片,normal和highlighted状态下的背景图片
    //contentImage放具体要显示的图片
    AwesomeMenuItem *midItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"icon_pathMenu_background_highlighted"] highlightedImage:nil ContentImage:[UIImage imageNamed:@"icon_pathMenu_mainMine_normal"] highlightedContentImage:nil];

    AwesomeMenuItem *firstItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg_pathMenu_black_normal"] highlightedImage:nil ContentImage:[UIImage imageNamed:@"icon_pathMenu_collect_normal"] highlightedContentImage:[UIImage imageNamed:@"icon_pathMenu_collect_highlighted"]];
    AwesomeMenuItem *secoendItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg_pathMenu_black_normal"] highlightedImage:nil ContentImage:[UIImage imageNamed:@"icon_pathMenu_scan_normal"] highlightedContentImage:[UIImage imageNamed:@"icon_pathMenu_scan_highlighted"]];
    AwesomeMenuItem *thirdItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg_pathMenu_black_normal"] highlightedImage:nil ContentImage:[UIImage imageNamed:@"icon_pathMenu_more_normal"] highlightedContentImage:[UIImage imageNamed:@"icon_pathMenu_more_highlighted"]];
    AwesomeMenuItem *fourthItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg_pathMenu_black_normal"] highlightedImage:nil ContentImage:[UIImage imageNamed:@"icon_pathMenu_collect_normal"] highlightedContentImage:[UIImage imageNamed:@"icon_pathMenu_collect_highlighted"]];

    NSArray *items = @[firstItem, secoendItem, thirdItem, fourthItem];
    AwesomeMenu *awesome = [[AwesomeMenu alloc] initWithFrame:CGRectZero startItem:midItem optionMenus:items];
    //开始点
    awesome.startPoint = CGPointMake(50, 150);
    //设置显示区域(也就是角度)
    awesome.menuWholeAngle = M_PI_2;

    awesome.delegate = self;
    //让中间按钮不旋转
    awesome.rotateAddButton = NO;
    awesome.alpha = 0.5;
    [self.view addSubview:awesome];

    [awesome autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:0];
    [awesome autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0];

    [awesome autoSetDimensionsToSize:CGSizeMake(200, 200)];
}

前面提到,Home模块的vc和Search模块的vc(viewController)是继承自ZYDealViewController,在ZYDealViewController我处理了home模块和search模块的相同功能,它们各自的特殊功能是放到各自的vc里面实现的,ZYDealViewController的代码如下:

#import <UIKit/UIKit.h>

@interface ZYDealViewController : UICollectionViewController
/**  设置请求参数:交给子类去实现  */
- (void)setParams:(NSMutableDictionary *)params;
@end

#import "ZYDealViewController.h"
#import "ZYConst.h"
#import "UIBarButtonItem+ZYExtension.h"
#import "UIView+Extension.h"
#import "ZYHomeTopItem.h"
#import "ZYCategoryViewController.h"
#import "ZYDistrictViewController.h"
#import "ZYSort.h"
#import "ZYCity.h"
#import "ZYMetaTool.h"
#import "ZYSortViewController.h"
#import "ZYRegion.h"
#import "ZYCategory.h"
#import "DPAPI.h"
#import "ZYDeal.h"
#import "MJExtension.h"
#import "ZYDealCell.h"
#import "MJRefresh.h"
#import "MBProgressHUD+MJ.h"
#import "UIView+AutoLayout.h"
#import "ZYDetailViewController.h"

@interface ZYDealViewController () <DPRequestDelegate>

@property (nonatomic, strong) NSMutableArray *deals;

@property (nonatomic, strong) DPRequest *lastRequest;

@property (nonatomic, assign) int currentPage;

@property (nonatomic, assign) int totalCount;

/** 当没有团购数据时,显示一张没有数据的背景 */
@property (nonatomic, strong) UIImageView *backgroundImageView;

@end

@implementation ZYDealViewController

static NSString * const reuseIdentifier = @"ZYDealViewControllerCell";

- (UIImageView *)backgroundImageView
{
    if (!_backgroundImageView) {
        _backgroundImageView = [[UIImageView alloc] init];
        _backgroundImageView.image = [UIImage imageNamed:@"icon_deals_empty"];
        [self.view addSubview:_backgroundImageView];
        [_backgroundImageView autoCenterInSuperview];

    }
    return _backgroundImageView;
}

- (NSMutableArray *)deals
{
    if (!_deals) {
        _deals = [NSMutableArray array];
    }
    return _deals;
}

- (instancetype)init
{
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];

    //设置cell的大小
    layout.itemSize = CGSizeMake(305, 305);
    return [self initWithCollectionViewLayout:layout];
}

/**
 当屏幕旋转,控制器view的尺寸发生改变调用
 */
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    // 根据屏幕宽度决定列数
    int cols = (size.width == 1024) ? 3 : 2;
    // 根据列数计算内边距
    UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout;
    CGFloat inset = (size.width - cols * layout.itemSize.width) / (cols + 1);
    layout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
    // 设置每一行之间的间距
    layout.minimumLineSpacing = inset;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupCollection];

}

#pragma mark ----setup系列

- (void)setupCollection
{
    [self.collectionView registerNib:[UINib nibWithNibName:@"ZYDealCell" bundle:nil] forCellWithReuseIdentifier:reuseIdentifier];

    self.collectionView.backgroundColor = ZYGlobalBg;
    self.collectionView.alwaysBounceVertical = YES;
    self.collectionView.header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(heaerRefresh)];
    self.collectionView.footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(footerRefresh)];
}

#pragma mark ----与服务器进行交互
- (void)loadNewDeals
{
    DPAPI *api = [[DPAPI alloc] init];
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    //让子类自行设置响应的参数
    [self setParams:params];
    params[@"page"] = @(self.currentPage);
    // 每页的条数
    params[@"limit"] = @10;
    self.lastRequest = [api requestWithURL:@"v1/deal/find_deals" params:params delegate:self];

    //    NSLog(@"请求参数:%@", params);
}

- (void)loadDeals
{
    self.currentPage = 1;
    [self loadNewDeals];
}

- (void)loadMoreDeals
{
    self.currentPage++;
    [self loadNewDeals];
}

- (void)request:(DPRequest *)request didFinishLoadingWithResult:(id)result
{
    if (request != self.lastRequest) {  //如果不是同一个请求,是短时间内发了两次请求,那么只要最近的一次请求
        return;
    }
    self.totalCount = [result[@"total_count"] intValue];

    NSArray *newDeals = [ZYDeal objectArrayWithKeyValuesArray:result[@"deals"]];
    if (self.currentPage == 1) {
        [self.deals removeAllObjects];
    }

    [self.deals addObjectsFromArray:newDeals];

    [self.collectionView reloadData];

    [self.collectionView.footer endRefreshing];
    [self.collectionView.header endRefreshing];
}

- (void)request:(DPRequest *)request didFailWithError:(NSError *)error
{
    if (self.lastRequest != request) {
        return;
    }
    //在ipad开发中,如果要显示HUD,特别需要注意,显示到self.view上
    //    [MBProgressHUD showError:@"加载失败,请检查您的网络..."];

    [MBProgressHUD showError:@"加载失败,请检查您的网络..." toView:self.view];

    [self.collectionView.footer endRefreshing];

    //当不是请求第一页的时候,如果请求失败,那么应当减去这次请求的
    self.currentPage--;

    [self.collectionView.footer endRefreshing];
    [self.collectionView.header endRefreshing];
}

#pragma mark ----刷新方法

- (void)heaerRefresh
{
    [self loadDeals];
}

- (void)footerRefresh
{
    [self loadMoreDeals];
}
#pragma mark <UICollectionViewDataSource>

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {

    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    //需要在请求发送就设置以此cell的布局
    [self viewWillTransitionToSize:CGSizeMake(self.collectionView.width, self.collectionView.height) withTransitionCoordinator:nil];
    self.backgroundImageView.hidden = (self.deals.count != 0);
    return self.deals.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    ZYDealCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    cell.deal = self.deals[indexPath.row];

    //第一次进入界面,self.totalCount会被初始化为0
    self.collectionView.footer.hidden = (self.totalCount == self.deals.count);
    return cell;
}

#pragma mark <UICollectionViewDelegate>

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    ZYDetailViewController *detailVc = [[ZYDetailViewController alloc] initWithNibName:@"ZYDetailViewController" bundle:nil];
    detailVc.deal = self.deals[indexPath.row];
    [self presentViewController:detailVc animated:YES completion:nil];
}

@end

其次就是Collect模块和Browse模块,这两个模块很相似,都是需要实现离线缓存,都是需要支持批量删除的,如此,就完全可以将对象相似的功能提取出来,放到基类里面,而这两个类继承自基类,从而可以大大减少代码量,在代码层次、结构上来看,也会好上很多。

我采用的是Sqlite数据库来进行的离线缓存,是这样设计的,有一个自增长的主键,一个团购(deal)model,团购的id,取出数据时,按照主键的倒序排序。

数据库设计代码如下:

#import <Foundation/Foundation.h>

@class ZYDeal;

@interface ZYDealTool : NSObject
+ (void)addCollectionDeal:(ZYDeal *)deal;
+ (void)removeCollectionDeal:(ZYDeal *)deal;

+ (NSArray *)collectDeals:(int)page;
+ (int)collectDealsCount;

+ (BOOL)isCollected:(ZYDeal *)deal;

+ (void)addBrowseDeal:(ZYDeal *)deal;
+ (void)removeBrowseDeal:(ZYDeal *)deal;

+ (NSArray *)browseDeals:(int)page;
+ (int)browseDealsCount;

+ (BOOL)isBrowsed:(ZYDeal *)deal;
@end

#import "ZYDealTool.h"
#import "FMDB.h"
#import "ZYDeal.h"
@implementation ZYDealTool

static FMDatabase *_database;

+ (void)initialize
{
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *path = [doc stringByAppendingPathComponent:@"deal.sqlite"];
    _database = [FMDatabase databaseWithPath:path];

    if (![_database open]) return;

    [_database executeUpdateWithFormat:@"CREATE TABLE IF NOT EXISTS t_collect_deal(id integer PRIMARY KEY, deal blob NOT NULL, deal_id text NOT NULL);"];

    [_database executeUpdateWithFormat:@"CREATE TABLE IF NOT EXISTS t_browse_deal(id integer PRIMARY KEY, deal blob NOT NULL, deal_id text NOT NULL);"];
}

+ (void)addCollectionDeal:(ZYDeal *)deal
{
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:deal];
    [_database executeUpdateWithFormat:@"INSERT INTO t_collect_deal(deal, deal_id) VALUES (%@, %@);",data, deal.deal_id];
}
+ (void)removeCollectionDeal:(ZYDeal *)deal
{
    [_database executeUpdateWithFormat:@"DELETE FROM t_collect_deal WHERE deal_id = %@;", deal.deal_id];
}

+ (NSArray *)collectDeals:(int)page
{
    int size = 10;
    int pos = (page - 1) * size;

    NSMutableArray *deals = [NSMutableArray array];
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT * FROM t_collect_deal ORDER BY id DESC LIMIT %d,%d;",pos,size];

    while (resultSet.next) {
        ZYDeal *deal = [NSKeyedUnarchiver unarchiveObjectWithData:[resultSet objectForColumnName:@"deal"]];
        [deals addObject:deal];
    }
    return deals;
}
+ (int)collectDealsCount
{
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT count(*) AS deal_count FROM t_collect_deal;"];
    [resultSet next];

    return [resultSet intForColumn:@"deal_count"];
}

+ (BOOL)isCollected:(ZYDeal *)deal
{
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT count(*) AS deal_count FROM t_collect_deal WHERE deal_id = %@;", deal.deal_id];

    [resultSet next];
    #warning 索引从1开始
    return [resultSet intForColumn:@"deal_count"] == 1;
}

+ (void)addBrowseDeal:(ZYDeal *)deal
{
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:deal];
    [_database executeUpdateWithFormat:@"INSERT INTO t_browse_deal(deal, deal_id) VALUES (%@, %@);",data, deal.deal_id];
}

+ (void)removeBrowseDeal:(ZYDeal *)deal
{
    [_database executeUpdateWithFormat:@"DELETE FROM t_browse_deal WHERE deal_id = %@;", deal.deal_id];
}

+ (NSArray *)browseDeals:(int)page
{
    int size = 10;
    int pos = (page - 1) * size;

    NSMutableArray *deals = [NSMutableArray array];
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT * FROM t_browse_deal ORDER BY id DESC LIMIT %d,%d;",pos,size];

    while (resultSet.next) {
        ZYDeal *deal = [NSKeyedUnarchiver unarchiveObjectWithData:[resultSet objectForColumnName:@"deal"]];
        [deals addObject:deal];
    }
    return deals;
}

+ (int)browseDealsCount
{
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT count(*) AS deal_count FROM t_browse_deal;"];
    [resultSet next];

    return [resultSet intForColumn:@"deal_count"];
}

+ (BOOL)isBrowsed:(ZYDeal *)deal
{
    FMResultSet *resultSet = [_database executeQueryWithFormat:@"SELECT count(*) AS deal_count FROM t_browse_deal WHERE deal_id = %@;", deal.deal_id];

    [resultSet next];
#warning 索引从1开始
    return [resultSet intForColumn:@"deal_count"] == 1;
}
@end

删除单个、批量删除功能:

这里需要注意的一点就是cell的循环利用功能,你不能只是修改界面展示,而是需要深入去修改导致界面更改的model,这样才可以做到完美的循环利用,不然会导致bug,请注意,在mvc模式下,界面如何展示都是由model决定的,如果要更改,请更改model。还有就是,当点击进去,会是一条团购详情的界面,在这个界面,当我取消收藏之后,回到收藏界面,也是应当同步删除掉取消了收藏的团购数据的,这里需要注意,用Notification发回来的deal和正在展示的那条deal并不会指向同一个内存地址,所以单纯的判断这两条团购数据相等与否是不行的,应当是重写deal的模型的isEqual方法。

下面是代码:

ZYOldDealViewController

#import <UIKit/UIKit.h>

@class ZYDeal;

@interface ZYOldDealViewController : UICollectionViewController
/**
 *  这个方法交给子类去实现,从而得到不同子类的具体背景图片
 *
 */
- (NSString *)bgImageName;

/**
 *  这个方法交给子类去实现,从而得到不同子类数据库里的具体数据
 *
 */
- (NSArray *)arrayWithCurretnPage:(int)currentPage;

/**
 *  这个方法教给子类去调用,移除deals内所有数据,重新从数据库里加载
 */
- (void)removeDealsAllObjects;

/**
 *  让子类实现这个方法,返回数据库中还有多少条团购数据
 *
 */
- (int)countForDeals;

/**
 *  让子类实现这个方法,返回navigationBar的标题
 *
 */
- (NSString *)titleForNavBar;

/**
 *  让子类实现这个方法,删除掉自身数据库内拥有的deal
 *
 */
- (void)deletedSqliteDeal:(ZYDeal *)deal;
@end

#import "ZYOldDealViewController.h"
#import "ZYDeal.h"
#import "UIView+AutoLayout.h"
#import "ZYConst.h"
#import "MJRefresh.h"
#import "UIView+Extension.h"
#import "ZYDealCell.h"
#import "ZYDetailViewController.h"
#import "ZYDealTool.h"
#import "UIBarButtonItem+ZYExtension.h"

@interface ZYOldDealViewController ()
/** 当没有团购数据时,显示一张没有数据的背景 */
@property (nonatomic, strong) UIImageView *backgroundImageView;

@property (nonatomic, strong) NSMutableArray *deals;

@property (nonatomic, assign) int currentPage;

@property (nonatomic, strong) UIBarButtonItem *backItem;

@property (nonatomic, strong) UIBarButtonItem *selectedAllItem;

@property (nonatomic, strong) UIBarButtonItem *unselectedAllItem;

@property (nonatomic, strong) UIBarButtonItem *delectedItem;
@end

@implementation ZYOldDealViewController

static NSString * const reuseIdentifier = @"ZYDealViewControllerCell";

- (UIImageView *)backgroundImageView
{
    if (!_backgroundImageView) {
        _backgroundImageView = [[UIImageView alloc] init];
        NSString *imageName = [self bgImageName];
        _backgroundImageView.image = [UIImage imageNamed:imageName];
        [self.view addSubview:_backgroundImageView];
        [_backgroundImageView autoCenterInSuperview];
    }
    return _backgroundImageView;
}

- (NSMutableArray *)deals
{
    if (!_deals) {
        _deals = [NSMutableArray array];
    }
    return _deals;
}

#pragma mark ----barButtonItem的懒加载
- (UIBarButtonItem *)backItem
{
    if (!_backItem) {
        _backItem = [UIBarButtonItem barButtonItemWithTarget:self action:@selector(clickbackItem) normalImage:@"icon_back" highImage:@"icon_back_highlighted"];
    }
    return _backItem;
}

- (UIBarButtonItem *)selectedAllItem
{
    if (!_selectedAllItem) {
        _selectedAllItem = [[UIBarButtonItem alloc] initWithTitle:@"  全选  " style:UIBarButtonItemStyleDone target:self action:@selector(clickSelectedAllItem)];
    }
    return _selectedAllItem;
}

- (UIBarButtonItem *)unselectedAllItem
{
    if (!_unselectedAllItem) {
        _unselectedAllItem = [[UIBarButtonItem alloc] initWithTitle:@"  全不选  " style:UIBarButtonItemStyleDone target:self action:@selector(clickUnselectedAllItem)];
    }
    return _unselectedAllItem;
}

- (UIBarButtonItem *)delectedItem
{
    if (!_delectedItem) {
        _delectedItem = [[UIBarButtonItem alloc] initWithTitle:@"  删除  " style:UIBarButtonItemStyleDone target:self action:@selector(clickDelectedItem)];
    }
    return _delectedItem;
}
- (instancetype)init
{
    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];

    //设置cell的大小
    layout.itemSize = CGSizeMake(305, 305);
    return [self initWithCollectionViewLayout:layout];
}

/**
 当屏幕旋转,控制器view的尺寸发生改变调用
 */
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    // 根据屏幕宽度决定列数
    int cols = (size.width == 1024) ? 3 : 2;
    // 根据列数计算内边距
    UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout;
    CGFloat inset = (size.width - cols * layout.itemSize.width) / (cols + 1);
    layout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset);
    // 设置每一行之间的间距
    layout.minimumLineSpacing = inset;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupNav];

    [self setupNavItem];

    [self setupCollection];

    [self loadMoreDeals];
}

#pragma mark ----setup系列
- (void)setupNav
{
    UINavigationBar *appearance = [UINavigationBar appearance];

    // 设置文字属性
    NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary];
    textAttrs[UITextAttributeTextColor] = [UIColor blackColor];

    textAttrs[UITextAttributeFont] = [UIFont boldSystemFontOfSize:20];//粗体,
    // UIOffsetZero是结构体, 只要包装成NSValue对象, 才能放进字典\数组中
    textAttrs[UITextAttributeTextShadowOffset] = [NSValue valueWithUIOffset:UIOffsetZero];
    [appearance setTitleTextAttributes:textAttrs];

    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"编辑" style:UIBarButtonItemStyleDone target:self action:@selector(clickEditBarButton:)];

    self.navigationItem.leftBarButtonItems = @[self.backItem];

    self.navigationItem.title = [self titleForNavBar];
}

- (void)setupNavItem
{
    //通过设置这个属性,可是设置整个导航栏的UIBarButtonItem的属性
    UIBarButtonItem *appearance = [UIBarButtonItem appearance];
    //设置普通状态下得文字属性
    NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary];
    //文字颜色
    textAttrs[NSForegroundColorAttributeName] = ZYColor(47,188,173);
    //文字字体
    textAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:18.0];
    [appearance setTitleTextAttributes:textAttrs forState:UIControlStateNormal];

    // 设置不可用状态(disable)的文字属性
    NSMutableDictionary *disableTextAttrs = [NSMutableDictionary dictionary];
    disableTextAttrs[NSForegroundColorAttributeName] = [UIColor lightGrayColor];
    disableTextAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:15];
    [appearance setTitleTextAttributes:disableTextAttrs forState:UIControlStateDisabled];
}

- (void)setupCollection
{
    [self.collectionView registerNib:[UINib nibWithNibName:@"ZYDealCell" bundle:nil] forCellWithReuseIdentifier:reuseIdentifier];

    self.collectionView.backgroundColor = ZYGlobalBg;
    self.collectionView.alwaysBounceVertical = YES;
    self.collectionView.footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(footerRefresh)];

}

#pragma mark ----与数据库进行交互
- (void)loadMoreDeals
{
    self.currentPage++;

    NSArray *tempArray = [self arrayWithCurretnPage:self.currentPage];

    [self.deals addObjectsFromArray:tempArray];

    [self.collectionView reloadData];

    [self.collectionView.footer endRefreshing];
}

#pragma mark ----刷新方法
- (void)footerRefresh
{
    [self loadMoreDeals];
}

#pragma mark ----click事件

- (void)clickbackItem
{
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)clickEditBarButton:(UIBarButtonItem *)item
{
    if ([item.title isEqualToString:@"完成"]) {
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"编辑" style:UIBarButtonItemStyleDone target:self action:@selector(clickEditBarButton:)];
        self.navigationItem.leftBarButtonItems = @[self.backItem];

        [self.deals enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
            obj.editing = NO;
            obj.checking = NO;
        }];

    }
    else{
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"完成" style:UIBarButtonItemStyleDone target:self action:@selector(clickEditBarButton:)];
        self.navigationItem.leftBarButtonItems = self.navigationItem.leftBarButtonItems = @[self.backItem, self.selectedAllItem, self.unselectedAllItem, self.delectedItem];

        [self.deals enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
            obj.editing = YES;
        }];
    }

    [self.collectionView reloadData];
}

- (void)clickSelectedAllItem
{
    [self.deals enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
        obj.checking = YES;
    }];
    [self.collectionView reloadData];
}

- (void)clickUnselectedAllItem
{
    [self.deals enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
        obj.checking = NO;
    }];
    [self.collectionView reloadData];
}

- (void)clickDelectedItem
{
    NSMutableArray *deletedArray = [NSMutableArray array];

    //需要注意的是,在遍历数组的时候,是不可以删除数组内元素的
    [self.deals enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
        if (obj.isChecking) {
            [deletedArray addObject:obj];
        }
    }];

    [deletedArray enumerateObjectsUsingBlock:^(ZYDeal *obj, NSUInteger idx, BOOL *stop) {
        [self.deals removeObject:obj];
        [self deletedSqliteDeal:obj];
    }];

    [self.collectionView reloadData];
}

#pragma mark ----其他
- (void)removeDealsAllObjects
{
    [self.deals removeAllObjects];
    self.currentPage = 0;
}

#pragma mark <UICollectionViewDataSource>

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {

    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    //需要在请求发送就设置以此cell的布局
    [self viewWillTransitionToSize:CGSizeMake(self.collectionView.width, self.collectionView.height) withTransitionCoordinator:nil];

    if (self.deals.count == 0 && [self.navigationItem.rightBarButtonItem.title isEqualToString:@"完成"]) {
        [self clickEditBarButton:self.navigationItem.rightBarButtonItem];
    }
    self.backgroundImageView.hidden = (self.deals.count != 0);
    self.navigationItem.rightBarButtonItem.enabled = (self.deals.count != 0);
    return self.deals.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    ZYDealCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    cell.deal = self.deals[indexPath.row];

    self.collectionView.footer.hidden = (self.deals.count == [self countForDeals]);
    return cell;
}

#pragma mark <UICollectionViewDelegate>

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    ZYDetailViewController *detailVc = [[ZYDetailViewController alloc] initWithNibName:@"ZYDetailViewController" bundle:nil];
    detailVc.deal = self.deals[indexPath.row];
    [self presentViewController:detailVc animated:YES completion:nil];
}

@end

ZYCollectViewController是继承自ZYOldDealViewController的,所以相应代码比较少:

#import <UIKit/UIKit.h>
#import "ZYOldDealViewController.h"
@interface ZYCollectViewController : ZYOldDealViewController

@end

#import "ZYCollectViewController.h"
#import "ZYDeal.h"
#import "ZYConst.h"
#import "MJRefresh.h"
#import "UIView+Extension.h"
#import "ZYDealCell.h"
#import "ZYDetailViewController.h"
#import "ZYDealTool.h"
#import "UIBarButtonItem+ZYExtension.h"

@interface ZYCollectViewController ()
@end

@implementation ZYCollectViewController

- (NSString *)bgImageName
{
    return @"icon_collects_empty";
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupNotification];

    [self loadDeals];
}

#pragma mark ----setup系列

- (void)setupNotification
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(collectStateDidChange:) name:ZYCollectStateDidChangeNotification object:nil];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark ----与数据库进行交互
- (void)loadDeals
{
    [self.collectionView.footer beginRefreshing];
}

- (void)deletedSqliteDeal:(ZYDeal *)deal
{
    [ZYDealTool removeCollectionDeal:deal];
}
#pragma mark ----notification系列
- (void)collectStateDidChange:(NSNotification *)note
{
    [self removeDealsAllObjects];
    [self loadDeals];
}

#pragma mark ----其他
- (NSArray *)arrayWithCurretnPage:(int)currentPage
{
    return [ZYDealTool collectDeals:currentPage];
}

- (int)countForDeals
{
    return [ZYDealTool collectDealsCount];
}

- (NSString *)titleForNavBar
{
    return @"收藏";
}

@end

同理,ZYBrowseViewController也是继承自ZYOldDealViewController的

#import "ZYBrowseViewController.h"
#import "MJRefresh.h"
#import "ZYConst.h"
#import "ZYDealTool.h"
@interface ZYBrowseViewController ()

@end

@implementation ZYBrowseViewController

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

    [self loadDeals];
}

- (NSString *)bgImageName
{
    return @"icon_latestBrowse_empty";
}

- (NSArray *)arrayWithCurretnPage:(int)currentPage
{
    return [ZYDealTool browseDeals:currentPage];
}

- (int)countForDeals
{
    return [ZYDealTool browseDealsCount];
}

- (NSString *)titleForNavBar
{
    return @"浏览记录";
}
#pragma mark ----与数据库进行交互
- (void)loadDeals
{
    [self.collectionView.footer beginRefreshing];
}

- (void)deletedSqliteDeal:(ZYDeal *)deal
{
    [ZYDealTool removeBrowseDeal:deal];
}

@end

团购详情界面,如图:

这是未被收藏的团购:

这是被收藏之后的图片:

当点击分享,会跳出关于分享的界面,只是最简单的分享......

下面,这是地图界面的相关图片:

MapKit框架中,反地理编码、自定义大头针等技术。

以上,就是这个项目的所有实现内容了,没有做缓存限制,这一点也很简单,直接使用SDWebImage框架提供的,在接受到内存警告时,清理缓存即可。

这个项目的github地址:https://github.com/wzpziyi1/GroupPurchase

时间: 2024-10-12 09:25:52

iOS开发:一个高仿美团的团购ipad客户端的设计和实现(功能:根据拼音进行检索并展示数据,离线缓存团购数据,浏览记录与收藏记录的批量删除等)的相关文章

高仿美团iOS版,版本号5.7

高仿美团iOS版,版本号:5.7 github链接:https://github.com/lookingstars/meituan 如果你觉得不错,欢迎star 哦 1.团购首页: 1.1  团购-->猜你喜欢->右上角分享 到微信朋友圈,新浪微博等 2.商家 3.名店抢购 4.推荐: 5.热门排队 6.团购详情 7.上门服务 8.上门洗车 9.地图:附近美食 10.商家分类显示 11.启动页广告: 12.我的 13.更多: 版权声明:本文为博主原创文章,未经博主允许不得转载.

微信小程序开发日记——高仿知乎日报(上)

本人对知乎日报是情有独钟,看我的博客和github就知道了,写了几个不同技术类型的知乎日报APP 要做微信小程序首先要对html,css,js有一定的基础,还有对微信小程序的API也要非常熟悉 我将该教程分为以下三篇 微信小程序开发日记--高仿知乎日报(上) 微信小程序开发日记--高仿知乎日报(中) 微信小程序开发日记--高仿知乎日报(下) 三篇分别讲不同的组件和功能块 这篇要讲 API分析 启动页 轮播图 日报列表 浮动按钮 侧滑菜单 API分析 以下是使用到的具体API,更加详细参数和返回结

高仿美团应用客户端项目源码

源码Tuan,这个案例是模仿MJ老师ipad版美团(swift版),高仿美团iOS版,版本号:5.7, 已更新到Swift 2.0 基于Xcode 7 源码下载: http://code.662p.com/view/11383.html <ignore_js_op> <ignore_js_op> <ignore_js_op> <ignore_js_op> <ignore_js_op> <ignore_js_op> <ignore

高仿美团应用客户端布局源码

高仿美团框架基本已搭好.代码简单易懂,适合新人.适合新人. 源码下载:http://code.662p.com/list/12_1.html新人. <ignore_js_op> <ignore_js_op> 详细说明:http://ios.662p.com/thread-2774-1-1.html

MeiTuanRefreshListView高仿美团下拉刷新《IT蓝豹》

MeiTuanRefreshListView高仿美团下拉刷新 MeiTuanRefreshListView高仿美团下拉刷新,本项目来自:https://github.com/nugongshou110/MeiTuanRefreshListView项目主要构成部分:自定义MeiTuanRefreshFirstStepView,MeiTuanRefreshSecondStepView,MeiTuanRefreshThirdStepView,其中自定义MeiTuanListView继承了ListVie

高仿美团&lt;二&gt;

由于在简书上已经把项目的思路和个个模块写的很清楚了. 点击以下链接高仿美团<2> 版权声明:本文为博主原创文章,未经博主允许不得转载.

Androidstudio如何制作一个高仿小米计算器小demo

Androidstudio如何制作一个高仿小米计算器小demo ————安德风 一.最终成品效果图: 二.界面设计布局源代码:文件名activity_main.xml (存放在jsj(我的模块名为jsj)/res/layout/activity_main.xml) 1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.a

奔五的人学IOS:一个好的应用最终还是要由服务端来支撑其功能,兼谈几个免费云空间

学习ios-swift有一段时间了,一些基本控件的使用应该是没有问题了.但一个好的应用最终还是要由服务端来支撑其功能,为了练习各种控件的使用,想找网站上现有的api并且内容又是你想要的,可能性很小,如果是已经有了该api,那一定是已经有了相应的app了. 基于以上原因,想要练习app,那就先弄api吧. 首先就从csdn入手了,考虑弄一个csdn的资讯app,直接抓取csdn的页面吧?这个思路有考虑过,但这个需要由swift来解析页面内容,感觉需要一定的功能才行,查了一下swift还没有类似的解

iOS开发-- 一个苹果证书如何多次使用

苹果的开发者账号限制开发者证书只能有5个,我们开发过程中遇到超过5个人需要真机调试的情况,如何解决这个问题呢? 有两种方式可以解决问题: 1. Revoke原来的证书----不推荐 将以前的证书“revoke”掉,然后重新生成一个新的证书. 这种方法是可以的,但是会造成相应的Provisioning Profiles失效,这是小问题.但是又要重新申请证书甚至描述文件很浪费时间,所以不提倡这种做法. 2. p12----推荐 我们的每一个证书都可以生成一个.p12文件,这个文件是一个加密的文件,只