MyZone
题记
原定于双休日打包上线的硬生生的拖到了今天上午,心里的石头总算是落下了.
爱油我去
是我的第五个独立开发的app,自己也算得上是彻彻底底的一位老司机了.原以为随着自己的经验不断丰富,开发起来的难度会随之减少,但是结果却是恰恰相反.因为我突然发现要考虑的事情太多了,无论是项目的框架构建,还是本地化的一些考量等都需要经过深思熟虑,并不是走来就开撸.本篇文章不会涉及到技术方面的问题,就当是一篇小日记吧,记录下这个项目的开发流程以及不足之处和相应的解决方案.
notice
今天上午在打包提交审核的过程中遇到了一个问题,这里也一并记录下.因为我使用的是xcode8,提交的版本在
itunes connect
中总是不显示.后来在查看苹果的邮件中找到了问题所在.因为访问了用户的隐私数据但是并没有显示的添加描述,所以构建版本失败.
解决的方法也很简单,只需要在
info.plist
添加相应的描述文件即可.如下所示,全部添加上即可
项目的框架
在开始项目之前,首先需要对该款
app
的功能有一个大致的了解,最好列一个list
,这样成竹在胸,写起代码来自然会是得心应手.目录如下
为了很好的解决传统
MVC
的controller
代码臃肿问题,我在每一个controller
中引入了ViewModel
的类,让它全权负责网络请求,以及相应的模型解析等事件,将控制器
解放出来.每个控制器
的代码量控制在500
行之内.当然,自我感觉该项目的模式并不是真正意义上的MVVM
.因为它的体量并不算的上是一个很大的程序,功能并非很复杂.如果要是全部采用MVVM
的形式,不免有点儿大材小用,得不偿失的感觉.
-loveOil //项目名称
--AppManager //manager
--AppManager // 与app相关的类。首次登录相关,设置window的根控制器
--PermissonManager //授权相关 访问相册,定位
--AppEntry //设置程序的入口
--Features //模块。包含各个模块的Model,View,Controller,Manager
--BaseViewController //基ViewController
--IntroModule //引导页模块
--EntryNavModule //主程序入口
--GasStationPort //加油站
--UserPort //用户端
--MainContainer //主要的容器
--MainVC // 基控制器 MainTableVIewController
--SubVIewController //用户端下的分控制器 包括 买油 卖油 以及我
--BaseModule //基模块 用于其他几个控制器继承
--LoginAndRegisterAbout(注册登录相关)
--BuyOil //买油模块
--SubVC // 订单模块
--Profile //我的模块
--VC // 主控制器
-- RemainingBalance 现金余额
-- MyOilCard 我的油卡
-- AddOil 我要加油
-- ReapacketOilCard 红包油卡
-- SignInRewardVC 签到奖励
-- UnfilledOrderVC 未完成订单
--SellOil //卖油模块
--categories //类目。包含各种类的分类
--Frameworks //系统框架。包含导入的系统的框架
--Helpers //帮助类。包含网络,数据库,归档,定位等操作类的封装和实现
--CacheManager //缓存管理类
--BottomAlertView //弹框相关
--NetworkTool // 网络请求相关---包含整个项目的网络请求
--Utilites //工具类,一些非对象的,而是类方法调用的类
--Vendors //第三方库。部分需要修改或者不支持cocoapod的第三方的框架引入
--Config //配置。包含宏定义文件,全局配置文件,全局常量文件,颜色配置文件
--PrefixHeader //pch文件
--macro //常用的宏定义
--firstLaunchAbout //首次登录相关
--category //分类相关
--Resources // 资源。包含plist,image,html,bundle,Localizable.strings等
--AppEntry // 程序入口。包含AppDelegate,main.c,info.plist
--RemoteNotificationAbout // 通知相关(视图控制器)
-Products // 系统自动生成的.app所在文件夹
-Pods // 采用 CocoaPods 管理的第三方库。
关于网络请求
该项目采用的网络请求是基于
猿题库
的YTKNetwork
,它是对AFNetWorking
的再次封装,功能更加强大,使用起来也更加方便.这里不讨论YTKNetwork
这个网络库,如果有需要,可以去这里下载 YTKNetworkg.
- 这里要记录下我在使用这个网络框架时所遇到的一个坑.
需要修改下
AFNetWorking的配置信息
,在AFURLResponseSerialization.m
中,替换成如下即可
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/json",@"text/html",@"text/css", nil];
//修改下可以接受的类型,这样就不会报解析的错误.
return self;
}
我新建了一个网络请求类,命名为
BaseYtkRequest
,让它继承YTKRequest
,然后改项目的所有网络请求都继承自BaseYtkRequest
,这样做的好处是将第三方库对项目的影响降低到最小,低侵入,低耦合,这样,即使在那一天需要替换网络请求的库,也只需要修改BaseYtkRequest
接口,其他的网络请求接口都不需要修改.
- 请求所带的参数大致如下
#import "YTKRequest.h"
@interface BaseYtkRequest : YTKRequest
/**
* 当前版本号----每一个请求都有版本号
*/
@property (nonatomic, strong) NSString * ver;
/**
* 当前请求时间戳
*/
@property (nonatomic, assign) NSUInteger timestamp;
/**
* 签名
*/
@property (nonatomic, strong) NSString * sign;
/**
* 随机数
*/
@property (nonatomic, strong) NSString * random;
/**
* 签名
*/
@property (nonatomic, strong) NSString * token;
/**
* 用户id
*/
@property (nonatomic, assign) NSInteger users_id;
/**
*
*/
-(NSString *)staffRequestUrl;
关于应用内支付(该项目使用的有微信支付,以及支付宝支付)
这里不详细讨论如何接入上述的两种支付方式,仅仅说说一些注意点(微信为例).
总之,生成签名等关键步骤,由服务器端来做,app端仅仅负责拿到数据并且吊起支付.代码大致如下
//1.吊起微信支付
[[OpenShareManager manager] wechatpay:[[[request.responseJSONObject valueForKey:kData] valueForKey:kResult] valueForKey:kToken]];
//方法实现:
#pragma mark - 微信支付
-(void)wechatpay:(NSDictionary *)orderDetail
{
wechatPayResultModel * model = [wechatPayResultModel modelWithDictionary:orderDetail];
PayReq *request = [[PayReq alloc] init];
request.partnerId = model.partnerid;
request.prepayId = model.prepayid;
request.package = model.package;
request.nonceStr = model.noncestr;
request.timeStamp = model.timestamp;
request.sign = model.sign;
[WXApi sendReq:request];
}
关于即时通讯(腾讯IM)
因为用户需要实时的接收到订单信息,例如扫描二维码输入金额后,需要立即接收到确认订单的信息.那么只能使用长连接来完成上述功能.这里使用的是
腾讯IM
. 其中最主要的两个功能是单点登录以及新信息的接收.
- 如何接入
在app启动时,初始化
腾讯IM
.注意在初始化之前,必须首先设置在线状态监听者
,以及新消息通知监听者
,这样才能接收到用户状态变更以及新消息通知
/**
* 初始化IMSdk
*/
-(void)initIMSDKConfig
{
//0.禁用日志打印
[[TIMManager sharedInstance] setLogLevel:TIM_LOG_NONE];
//1.设置用户在线状态通知
[self setUserStatusListener];
//2.设置新消息通知
[self setMessageListener];
//3.初始化sdk
[[TIMManager sharedInstance] initSdk:[appidAt3rdFormal intValue] accountType:accountType];
}
/**
* 用户在线状态通知
*/
-(void)setUserStatusListener
{
TIMUserStatusListenerVC * listener = [[TIMUserStatusListenerVC alloc] init];
[[TIMManager sharedInstance] setUserStatusListener:listener];
}
- 用户状态变更
需要遵循
TIMUserStatusListener
协议,并且实现协议的方法
/**
* 被踢下线
*/
-(void)onForceOffline
{
//1.弹框提示用户
kDISPATCH_MAIN_THREAD(^{
DQAlertView * alertView = [[DQAlertView alloc] initWithTitle:@"下线通知" message:@"您的账号在另一台设备登录,如非本人操作,则密码可能已经泄露,建议前往修改密码" cancelButtonTitle:@"取消" otherButtonTitle:@"重新登录"];
alertView.otherButtonAction = ^{
//2.此处让用户重新登录,对用户透明
__block MBProgressHUD * hud = nil;
kDISPATCH_MAIN_THREAD(^{
hud = [MBProgressHUD showHudAddedTo:KEY_WINDOW mode:MBProgressHUDModeText text:WHENWAIT];
});
UserLogInRequest * request = [[UserLogInRequest alloc] initWithUserAccount:valueForKey(kUserAccount) password:valueForKey(kUserPwd)];
//2.1 登录类型
request.login_type = [valueForKey(kUserLoginType) integerValue];
[request startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest *request) {
kDISPATCH_MAIN_THREAD(^{
[hud hideAnimated:YES];
});
//3.登录成功
if (ValueForRespones(request.responseJSONObject) == StatusCodeSuccess) {
//4.缓存数据
[UserModelManager saveProfileModelToLocal:[[request.responseJSONObject valueForKey:kData] valueForKey:kResult]];
//5.IM重新登录
[IMManager userLogin];
//6.播放音效
[SoundManager playSystemSound];
}
} failure:^(__kindof YTKBaseRequest *request) {
kDISPATCH_MAIN_THREAD(^{
[hud hideAnimated:YES];
});
}];
};
alertView.cancelButtonAction = ^{
//3.让用户重新登录
app_window.rootViewController = [[BaseNavigationController alloc] initWithRootViewController:[[MainInterfaceEntryViewController alloc] init]];
};
[alertView show];
});
}
/**
* 票据过期
*/
-(void)onUserSigExpired
{
//1.发送请求
UserSignHasExpiredRequestTool * requestTool = [[UserSignHasExpiredRequestTool alloc] init];
[requestTool startWithCompletionBlockWithSuccess:^(__kindof YTKBaseRequest *request) {
if (ValueForRespones(request.responseJSONObject) == StatusCodeSuccess) {
//2.用户重新登录im
//2.1防止异常奔溃
if ([[[requestTool.responseJSONObject valueForKey:kData] valueForKey:kResult] valueForKey:KIMToken]) {
[IMManager userLoginWithIMToken:[[[requestTool.responseJSONObject valueForKey:kData] valueForKey:kResult] valueForKey:KIMToken]];
}
}
} failure:^(__kindof YTKBaseRequest *request) {
}];
}
- 设置新消息通知,并且遵循
TIMMessageListener
协议
/**
* 设置新消息通知
*/
-(void)setMessageListener
{
TIMUserMessangeListenerVC * messangeListener = [[TIMUserMessangeListenerVC alloc] init];
[[TIMManager sharedInstance] setMessageListener:messangeListener];
}