always trust yourself
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];
}