一、项目搭建
框架:对于app没有特别复杂的业务逻辑,核心为界面展示、数据交互,因此采用两层结构 + MVC的模式实现,对于实体的分类,可分为resq、resp,分别对应请求、响应;
项目结构如下:
1,library:主要是一些第三方的库,如支付宝、微信、sharesdk、Reachability… 第三方库建议优先使用CocoaPods
2,resource:资源文件,例如html、css、音频等
3,util:工具类,如http、json、MD5、sqlite、base64、3des、rsa等…
4,config:配置文件,例如全局的常量配置、url配置、通用宏定义…
5,application:主要是AppDelegate
6,core:应用的核心部分,下面代表跟业务相关的各个独立模块,core说明:此模块为应用的核心,包含了所有的业务相关模块,主要包括
- controller:控制器,负责接收界面事件、完成交互、处理逻辑,并实现Model与View的分离
- model:数据实体,分为resq、resp,分别对应请求、响应
- view:视图
- datalayer:数据层,又分为接口层、本地数据层
二、UI层设计
BaseViewController的基类封装:
1,viewDidLoad:通常会在视图加载时,完成三个操作,即初始化成员变量、初始化视图、加载数据,因此可在基类定义函数,交由子类实现
/**
* 所有视图控制器的基类
*/
@interface BaseViewController : UIViewController
/*
* 初始化变量
*/
-(void) initVariable;
/*
* 初始化视图
*/
-(void) initViews;
/*
* 加载数据
*/
-(void) loadData;
/*
* 返回处理
*/
-(void) back;
/*
* 返回到指定指定界面
*/
-(void) backToAssignedViewController:(Class) clazz;
@end
#import "BaseViewController.h"
@implementation BaseViewController
#pragma mark - 初始化操作
/*
* 视图加载
*/
- (void)viewDidLoad {
[super viewDidLoad];
//开启ios右滑返回
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.delegate = nil;
}
}
/*
* 初始化变量
*/
-(void) initVariable{
}
/*
* 初始化视图
*/
-(void) initViews{
}
/*
* 加载数据
*/
-(void) loadData{
}
/*
* 返回函数,可由子类重写
*/
-(void) back{
[self.navigationController popViewControllerAnimated:YES];
}
/*
* 跳转到指定界面
*/
-(void) backToAssignedViewController:(Class) clazz{
for(UIViewController *controller in self.navigationController.viewControllers){
if([controller isKindOfClass:clazz]){
[self.navigationController popToViewController:controller animated:YES];
break;
}
}
}
@end
2,导航栏可通过Category实现
#import <UIKit/UIKit.h>
@interface UIViewController (Navigation)
/*
* 设置导航栏样式 - 透明
*/
-(void) setNavigationBarTransparent;
/*
* 设置导航栏样式 - 默认
*/
-(void) setNavigationBarDefaultStyle;
/*
* 初始化导航栏返回按钮
*/
-(void) initNavigationBarBackButton:(NSString *) backStr;
@end
#import "UIViewController+Navigation.h"
#import "UIColor+Convert.h"
#import "TextSizeUtil.h"
@implementation UIViewController (Navigation)
/*
* 设置导航栏样式 - 透明
*/
-(void) setNavigationBarTransparent{
//1,状态栏高亮(相当于透明,跟随导航栏)
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
//2,导航栏透明
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"BgTransparent"] forBarMetrics:UIBarMetricsDefault];
self.navigationController.navigationBar.barStyle = UIBarStyleBlackTranslucent;
self.navigationController.navigationBar.translucent = YES;
[self.navigationController.navigationBar setShadowImage:[[UIImage alloc] init]]; //去除分割线
}
/*
* 设置导航栏样式 - 非透明
*/
-(void) setNavigationBarDefaultStyle{
//1,状态栏高亮(相当于透明,跟随导航栏)
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
//2,导航栏颜色
self.navigationController.navigationBar.barTintColor = [UIColor colorWithHexString:@"3A3A3F"];
self.navigationController.navigationBar.translucent = NO;
[self.navigationController.navigationBar setShadowImage:[[UIImage alloc] init]];
[self.navigationController.navigationBar setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIColor whiteColor], NSForegroundColorAttributeName, nil]];
}
/*
* 初始化导航栏返回按钮
*/
-(void) initNavigationBarBackButton:(NSString *) backStr{
//自定义navigation controller的返回按钮
UIButton *btnBack = [UIButton buttonWithType:UIButtonTypeCustom];
btnBack.titleLabel.textAlignment = NSTextAlignmentLeft;
[btnBack setTitle:backStr forState:UIControlStateNormal];
btnBack.titleLabel.font = [UIFont systemFontOfSize: 16.0];
[btnBack setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[btnBack setTitleColor:[UIColor colorWithHexString:@"898788"] forState:UIControlStateHighlighted];
[btnBack addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
//根据文字计算按钮大小
CGRect rect = [TextSizeUtil getRectByStr:backStr:16.0 :80 :44];
[btnBack setFrame:CGRectMake(0, 0, rect.size.width + 20, 44)]; //根据文字计算按钮宽度
[btnBack setImage:[UIImage imageNamed:@"IconBack"] forState:UIControlStateNormal];
[btnBack setImageEdgeInsets:UIEdgeInsetsMake(0.0, -10, 0.0, 0.0)]; //文字、图片之间间隔10
//返回按钮边距
UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithCustomView:btnBack];
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
target:nil action:nil];
negativeSpacer.width = -5;
self.navigationItem.leftBarButtonItems = @[negativeSpacer, backItem];
}
@end
3,UIVIew+FrameAdjust:便于改变UIView的大小、位置
/**
* 提供简便的方法,设置frame
*/
@interface UIView (FrameAdjust)
- (CGFloat)x;
- (void)setX:(CGFloat)x;
- (CGFloat)y;
- (void)setY:(CGFloat)y;
- (CGFloat)width;
- (void)setWidth:(CGFloat)width;
- (CGFloat)height;
- (void)setHeight:(CGFloat)height;
@end
#import "UIView+FrameAdjust.h"
@implementation UIView (FrameAdjust)
- (CGFloat)x{
return self.frame.origin.x;
}
- (void)setX:(CGFloat)x{
self.frame = CGRectMake(x, self.y, self.width, self.height);
}
- (CGFloat)y{
return self.frame.origin.y;
}
- (void)setY:(CGFloat)y{
self.frame = CGRectMake(self.x, y, self.width, self.height);
}
- (CGFloat)width{
return self.frame.size.width;
}
- (void)setWidth:(CGFloat)width{
}
- (CGFloat)height{
return self.frame.size.height;
}
- (void)setHeight:(CGFloat)height{
self.frame = CGRectMake(self.x, self.y, self.width, height);
}
@end
4,适配部分:采用autolayout,即相对布局,并结合Masonry改变约束即可
5,UIView+Loading;加载框,主要分为加载对话框、加载动画、无网络提示重新加载,此处暂仿美团实现
//用于在无网络时,回调到UIViewController
typedef void (^OnClickReload) (void);
/**
* 为UIView扩展加载框的功能,通常在UIViewController的rootView中开启,加载框分为两种
* 1, 加载对话框:主要用于登录、注册等操作
* 2, 加载动画:主要用于获取数据操作,
*/
@interface UIView (Loading)
/**
* 显示加载对话框
*/
-(void) showLoadingDialog;
/**
* 关闭加载对话框
*/
-(void) closeLoadingDialog;
/**
* 显示自定义GIF加载动画(仿美团)
*/
-(void) showLoadingGIFAnimation;
/**
* 关闭自定义GIF加载动画(仿美团)
*/
-(void) closeLoadingGIFAnimation;
/**
* 显示无网络视图
*/
-(void) showNotNetworkView:(NSError *) error :(UIViewController *) controller;
/**
* 关闭无网络视图
*/
-(void) closeNotNetworkView;
/**
* 弹出toast提示
*/
-(void) toast:(NSString *) text;
@end
@implementation UIView (Loading)
/**
* 显示加载对话框
*/
-(void) showLoadingDialog{
[MBProgressHUD showHUDAddedTo:self animated:YES];
}
/**
* 关闭加载对话框
*/
-(void) closeLoadingDialog{
[MBProgressHUD hideHUDForView:self animated:YES];
}
/**
* 显示自定义GIF加载动画(仿美团)
*/
-(void) showLoadingGIFAnimation{
NSArray *nibs = [[NSBundle mainBundle] loadNibNamed:@"LoadingGIFAnimationView" owner:nil options:nil];
LoadingGIFAnimationView *loadingView = [nibs lastObject];
loadingView.center = self.center;
[self addSubview:loadingView];
[loadingView startLoadingAnimation];
}
/**
* 关闭自定义GIF加载动画(仿美团)
*/
-(void) closeLoadingGIFAnimation{
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[LoadingGIFAnimationView class]]) {
[subview removeFromSuperview];
}
}
}
/**
* 显示无网络视图(仿美团)
*/
-(void) showNotNetworkView:(NSError *) error :(UIViewController *) controller{
NSArray *nibs = [[NSBundle mainBundle] loadNibNamed:@"NotNetworkView" owner:nil options:nil];
NotNetworkView *loadingView = [nibs lastObject];
loadingView.center = self.center;
//无网络
if(NSURLErrorNotConnectedToInternet == error.code){
loadingView.labelReason.text = @"您的网络好像不太给力,请稍后再试";
}
//请求超时
else if(NSURLErrorTimedOut == error.code){
loadingView.labelReason.text = @"请求超时,请稍后再试";
}
//连接不到服务器
else if(NSURLErrorCannotConnectToHost == error.code){
loadingView.labelReason.text = @"无法连接到服务器,请稍后再试";
}
//未知错误
else if(NSURLErrorUnknown == error.code){
loadingView.labelReason.text = @"未知错误";
}
[loadingView.btnReload addTarget:controller action:@selector(notNetworkClickRefresh) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:loadingView];
}
/**
* 关闭无网络视图(仿美团)
*/
-(void) closeNotNetworkView{
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[NotNetworkView class]]) {
[subview removeFromSuperview];
}
}
}
/**
* 弹出toast提示
*/
-(void) toast:(NSString *) text{
[self makeToast:text duration:1.0f position:CSToastPositionCenter];
}
@end
6,引导页、登录页、首页的管理(在didFinishLaunchingWithOptions中判断)
三、数据层设计
数据层:分为接口层、本地数据层,此处仅对接口层进行说明
首先是HttpUtil,提供发起http请求的工具类:对于回调的方式,可采用block、也可采用统一回调管理
#import <Foundation/Foundation.h>
#import "AFHTTPSessionManager.h"
#pragma mark http请求回调函数
/**
* 成功的回调
*
* @param responseBody 响应数据
*/
typedef void(^OnSuccess) (id responseBody);
/**
* 失败回调
*
* @param error 错误信息
*/
typedef void(^OnFailure) (NSError *error);
#pragma mark http请求的工具类
@interface HttpUtil : NSObject
@property (strong,nonatomic) AFHTTPSessionManager *httpSessionManager;
/**
* 单例
*/
+(HttpUtil *) sharedManager;
/*
* 判断网络连接状况
*/
+(BOOL) isConnectionAvailable;
/**
* http get请求 - block回调
*
* @param url 请求url
* @param params 请求参数
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) doGet:(NSString *) url params :(id) params
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure;
/**
* http post请求 - block回调
*
* @param url 请求url
* @param params 请求参数
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) doPost:(NSString *) url params :(id) params
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure;
/**
* http 多图片上传 - block回调
*
* @param url 请求url
* @param formName 表单的name,服务端根据此获取数组
* @param imageArray 图片数组,可为NSData
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) multiUploadImage:(NSString *) url
formName: (NSString *) formName
imageArray: (NSMutableArray *) imageArray
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure;
@end
@implementation HttpUtil
/**
* 避免重复创建HttpSessionManager
*/
-(id) httpSessionManager{
if(_httpSessionManager == nil){
_httpSessionManager = [AFHTTPSessionManager manager];
_httpSessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
_httpSessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
_httpSessionManager.requestSerializer.timeoutInterval = 10;
}
return _httpSessionManager;
}
/**
* 单例
*/
+(HttpUtil *) sharedManager{
static HttpUtil *sharedAccountManagerInstance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
sharedAccountManagerInstance = [[self alloc] init];
});
return sharedAccountManagerInstance;
}
/*
* 判断网络连接
*/
+(BOOL) isConnectionAvailable{
BOOL connected = YES;
// 1.检测wifi状态
Reachability *wifi = [Reachability reachabilityForLocalWiFi];
// 2.检测手机是否能上网络(WIFI\3G\2.5G)
Reachability *conn = [Reachability reachabilityForInternetConnection];
// 3.判断网络状态,wifi
if ([wifi currentReachabilityStatus] != NotReachable) {
connected = YES;
}
else if ([conn currentReachabilityStatus] != NotReachable) { //使用手机自带网络进行上网
connected = YES;
}
else { // 没有网络
connected = NO;
}
return connected;
}
/**
* http get请求 - block回调
*
* @param url 请求url
* @param params 请求参数
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) doGet:(NSString *) url params :(id) params
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure{
if([HttpUtil isConnectionAvailable]){
//通过AFHttpSessionManager发送请求
[self.httpSessionManager GET:url parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSData *data = responseObject;
NSString *jsonStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
onSuccess(jsonStr); //成功回调
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
onFailure(error); //失败回调
}];
}
else{
NSError *error = [NSError errorWithDomain:@"" code:NSURLErrorNotConnectedToInternet userInfo:nil]; //无网络的回调
onFailure(error);
}
}
/**
* http post请求 - block回调
*
* @param url 请求url
* @param params 请求参数
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) doPost:(NSString *) url params :(id) params
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure{
if([HttpUtil isConnectionAvailable]){
//通过AFHttpSessionManager发送请求
[self.httpSessionManager POST:url parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSData *data = responseObject;
NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
onSuccess(jsonStr); //成功回调
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
onFailure(error); //失败回调
}];
}
else{
NSError *error = [NSError errorWithDomain:@"" code:NSURLErrorNotConnectedToInternet userInfo:nil]; //无网络的回调
onFailure(error);
}
}
/**
* http 多图片上传 - block回调
*
* @param url 请求url
* @param formName 表单的name,服务端根据此获取数组
* @param imageArray 图片数组,可为NSData
* @param onSuccess 请求成功回调
* @param onFailure 请求失败回调
*/
-(void) multiUploadImage:(NSString *) url
formName: (NSString *) formName
imageArray: (NSMutableArray *) imageArray
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure{
if([HttpUtil isConnectionAvailable]){
//创建NSMutableURLRequest,设置multipart-formdata参数,并设置content-Type
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
for(int i = 0 ; i < imageArray.count; i++){
UIImage *image = [imageArray objectAtIndex:i];
NSData *data = UIImageJPEGRepresentation(image, 0.5f);
if(data != nil){
[formData appendPartWithFileData:data name:formName fileName:[NSString stringWithFormat:@"file_%d.jpg",i] mimeType:@"image/jpeg" ];
}
}
} error:nil];
[request setTimeoutInterval:30];
//创建上传任务
NSURLSessionUploadTask *uploadTask = [self.httpSessionManager uploadTaskWithStreamedRequest:request progress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if(error){
onFailure(error);
}
else{
NSData *data = responseObject;
NSString *jsonStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
onSuccess(jsonStr);
}
}];
//开始上传
[uploadTask resume];
}
else{
NSError *error = [NSError errorWithDomain:@"" code:NSURLErrorNotConnectedToInternet userInfo:nil]; //无网络的回调
onFailure(error);
}
}
@end
接口层负责所有数据的获取:
#import "BaseApiManager.h"
/**
* 用户相关 - api管理,主要是登录、注册
*/
@interface UserApiManager : BaseApiManager
/**
* 密码登录
*
* @param phone 手机号
* @param pwd 密码
*/
-(void) loginForPassword:(NSString *) phone :(NSString *) pasword
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure;
@end
#import "UserApiManager.h"
#import "Md5Util.h"
@implementation UserApiManager
/**
* 登录操作
*
* @param phone 手机号
* @param pwd 密码
*/
-(void) loginForPassword:(NSString *) phone :(NSString *) pasword
onSuccess: (OnSuccess) onSuccess
onFailure: (OnFailure) onFailure{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
[params setObject:phone forKey:@"phone"];
[params setObject:[Md5Util md5_32:pasword] forKey:@"password"];
[[HttpUtil sharedManager] doPost:@"http://120.76.102.13:8080/poli/mobile/user/loginForPassword.remote" params:params onSuccess:onSuccess onFailure:onFailure];
}
@end
对于http调用,应在离开当前界面时,取消请求
-(void) viewWillDisappear:(BOOL)animated{
//取消所有请求
[[HttpUtil sharedManager].httpSessionManager.operationQueue cancelAllOperations];
//取消单个请求
NSURLSessionDataTask *task = [HttpUtil doPost:.,…….];
[task cancel];
}