2025-10-28 15:59:09 +08:00
//
// KBNetworkManager . m
// CustomKeyboard
//
# import "KBNetworkManager.h"
2025-10-28 18:02:10 +08:00
# import "AFNetworking.h"
2025-10-31 16:06:54 +08:00
# import "KBAuthManager.h"
2025-10-28 15:59:09 +08:00
NSErrorDomain const KBNetworkErrorDomain = @ "com.company.keyboard.network" ;
@ interface KBNetworkManager ( )
2025-10-28 16:11:35 +08:00
@ property ( nonatomic , strong ) AFHTTPSessionManager * manager ; // AFN 管 理 器 ( ephemeral 配 置 )
2025-10-28 15:59:09 +08:00
// 私 有 错 误 派 发
- ( void ) fail : ( KBNetworkError ) code completion : ( KBNetworkCompletion ) completion ;
@ end
@ implementation KBNetworkManager
+ ( instancetype ) shared {
static KBNetworkManager * m ; static dispatch_once _t onceToken ; dispatch_once ( & onceToken , ^ { m = [ KBNetworkManager new ] ; } ) ;
return m ;
}
- ( instancetype ) init {
if ( self = [ super init ] ) {
_enabled = NO ; // 键 盘 扩 展 默 认 无 网 络 能 力 , 需 外 部 显 式 开 启
_timeout = 10.0 ;
2025-11-13 15:34:56 +08:00
// 默 认 接 受 任 意 类 型 , 避 免 下 载 图 片 / 二 进 制 被 服 务 端 基 于 Accept 拒 绝
_defaultHeaders = @ { @ "Accept" : @ "*/*" } ;
2025-10-30 13:10:33 +08:00
// 设 置 基 础 域 名 , 路 径 可 相 对 该 地 址 拼 接
_baseURL = [ NSURL URLWithString : KB_BASE _URL ] ;
2025-10-28 15:59:09 +08:00
}
return self ;
}
# pragma mark - Public
- ( NSURLSessionDataTask * ) GET : ( NSString * ) path
parameters : ( NSDictionary * ) parameters
headers : ( NSDictionary < NSString * , NSString * > * ) headers
completion : ( KBNetworkCompletion ) completion {
if ( ! [ self ensureEnabled : completion ] ) return nil ;
2025-10-28 16:11:35 +08:00
NSString * urlString = [ self buildURLStringWithPath : path ] ;
if ( ! urlString ) { [ self fail : KBNetworkErrorInvalidURL completion : completion ] ; return nil ; }
// 使 用 AFHTTPRequestSerializer 生 成 带 参 数 与 头 的 请 求
AFHTTPRequestSerializer * serializer = [ AFHTTPRequestSerializer serializer ] ;
serializer . timeoutInterval = self . timeout ;
2025-11-13 15:34:56 +08:00
NSError * serror = nil ;
2025-10-28 16:11:35 +08:00
NSMutableURLRequest * req = [ serializer requestWithMethod : @ "GET"
URLString : urlString
parameters : parameters
2025-11-13 15:34:56 +08:00
error : & serror ] ;
if ( serror || ! req ) {
2025-11-17 20:07:39 +08:00
if ( completion ) completion ( nil , nil , serror ? : [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidURL userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Invalid URL" ) } ] ) ;
2025-11-13 15:34:56 +08:00
return nil ;
}
2025-10-28 16:11:35 +08:00
[ self applyHeaders : headers toMutableRequest : req contentType : nil ] ;
return [ self startAFTaskWithRequest : req completion : completion ] ;
2025-10-28 15:59:09 +08:00
}
- ( NSURLSessionDataTask * ) POST : ( NSString * ) path
jsonBody : ( id ) jsonBody
headers : ( NSDictionary < NSString * , NSString * > * ) headers
completion : ( KBNetworkCompletion ) completion {
if ( ! [ self ensureEnabled : completion ] ) return nil ;
2025-10-28 16:11:35 +08:00
NSString * urlString = [ self buildURLStringWithPath : path ] ;
if ( ! urlString ) { [ self fail : KBNetworkErrorInvalidURL completion : completion ] ; return nil ; }
// 用 JSON 序 列 化 器 生 成 JSON Body 的 请 求
AFJSONRequestSerializer * serializer = [ AFJSONRequestSerializer serializer ] ;
serializer . timeoutInterval = self . timeout ;
NSError * error = nil ;
NSMutableURLRequest * req = [ serializer requestWithMethod : @ "POST"
URLString : urlString
parameters : jsonBody
error : & error ] ;
if ( error ) { if ( completion ) completion ( nil , nil , error ) ; return nil ; }
[ self applyHeaders : headers toMutableRequest : req contentType : nil ] ;
return [ self startAFTaskWithRequest : req completion : completion ] ;
2025-10-28 15:59:09 +08:00
}
# pragma mark - Core
- ( BOOL ) ensureEnabled : ( KBNetworkCompletion ) completion {
if ( ! self . isEnabled ) {
2025-11-17 20:07:39 +08:00
NSError * e = [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorDisabled userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Network disabled (Full Access may be off)" ) } ] ;
2025-10-28 15:59:09 +08:00
if ( completion ) completion ( nil , nil , e ) ;
return NO ;
}
return YES ;
}
2025-10-28 16:11:35 +08:00
- ( NSString * ) buildURLStringWithPath : ( NSString * ) path {
2025-10-28 15:59:09 +08:00
if ( path . length = = 0 ) return nil ;
if ( [ path hasPrefix : @ "http://" ] || [ path hasPrefix : @ "https://" ] ) {
2025-10-28 16:11:35 +08:00
return path ;
2025-10-28 15:59:09 +08:00
}
2025-10-28 16:11:35 +08:00
if ( self . baseURL ) {
2025-11-13 16:23:46 +08:00
// 统 一 为 目 录 型 base ( 以 / 结 尾 ) , 并 剥 掉 path 的 前 导 / , 避 免 覆 盖 base 路 径
NSString * base = self . baseURL . absoluteString ? : @ "" ;
if ( ! [ base hasSuffix : @ "/" ] ) { base = [ base stringByAppendingString : @ "/" ] ; }
NSURL * dirBase = [ NSURL URLWithString : base ] ;
NSString * relative = ( [ path hasPrefix : @ "/" ] ) ? [ path substringFromIndex : 1 ] : path ;
return [ NSURL URLWithString : relative relativeToURL : dirBase ] . absoluteURL . absoluteString ;
2025-10-28 16:11:35 +08:00
}
return path ; // 当 无 baseURL 且 path 不 是 完 整 URL 时 , 让 AFN 处 理 ( 可 能 失 败 )
2025-10-28 15:59:09 +08:00
}
2025-10-28 16:11:35 +08:00
- ( void ) applyHeaders : ( NSDictionary < NSString * , NSString * > * ) headers toMutableRequest : ( NSMutableURLRequest * ) req contentType : ( NSString * ) contentType {
2025-10-31 16:06:54 +08:00
// 合 并 默 认 头 与 局 部 头 , 并 注 入 授 权 头 ( 若 可 用 ) 。 局 部 覆 盖 优 先 。
2025-10-28 15:59:09 +08:00
NSMutableDictionary * all = [ self . defaultHeaders mutableCopy ] ? : [ NSMutableDictionary new ] ;
2025-10-31 16:06:54 +08:00
NSDictionary * auth = [ [ KBAuthManager shared ] authorizationHeader ] ;
[ auth enumerateKeysAndObjectsUsingBlock : ^ ( NSString * key , NSString * obj , BOOL * stop ) { all [ key ] = obj ; } ] ;
2025-10-28 15:59:09 +08:00
if ( contentType ) all [ @ "Content-Type" ] = contentType ;
[ headers enumerateKeysAndObjectsUsingBlock : ^ ( NSString * key , NSString * obj , BOOL * stop ) { all [ key ] = obj ; } ] ;
[ all enumerateKeysAndObjectsUsingBlock : ^ ( NSString * key , NSString * obj , BOOL * stop ) { [ req setValue : obj forHTTPHeaderField : key ] ; } ] ;
}
2025-10-28 16:11:35 +08:00
- ( NSURLSessionDataTask * ) startAFTaskWithRequest : ( NSURLRequest * ) req completion : ( KBNetworkCompletion ) completion {
// 响 应 先 用 原 始 数 据 返 回 , 再 按 Content - Type 解 析 JSON ( 与 原 实 现 一 致 )
self . manager . responseSerializer = [ AFHTTPResponseSerializer serializer ] ;
NSURLSessionDataTask * task = [ self . manager dataTaskWithRequest : req uploadProgress : nil downloadProgress : nil completionHandler : ^ ( NSURLResponse * response , id responseObject , NSError * error ) {
2025-11-13 15:34:56 +08:00
// AFN 默 认 对 非 2 xx 的 状 态 码 返 回 error ; 这 里 直 接 回 调 上 层
if ( error ) {
if ( completion ) completion ( nil , response , error ) ;
return ;
}
2025-10-28 16:11:35 +08:00
NSData * data = ( NSData * ) responseObject ;
if ( ! [ data isKindOfClass : [ NSData class ] ] ) {
2025-11-17 20:07:39 +08:00
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidResponse userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "No data" ) } ] ) ;
2025-10-28 16:11:35 +08:00
return ;
}
2025-10-28 15:59:09 +08:00
NSString * ct = nil ;
if ( [ response isKindOfClass : [ NSHTTPURLResponse class ] ] ) {
ct = ( ( NSHTTPURLResponse * ) response ) . allHeaderFields [ @ "Content-Type" ] ;
}
2025-11-13 15:34:56 +08:00
// 更 宽 松 的 JSON 判 定 : Content - Type 里 包 含 json ; 或 首 字 符 是 { / [
BOOL looksJSON = ( ct && [ [ ct lowercaseString ] containsString : @ "json" ] ) ;
if ( ! looksJSON ) {
// 内 容 嗅 探 ( 不 依 赖 服 务 端 声 明 )
const unsigned char * bytes = data . bytes ;
NSUInteger len = data . length ;
for ( NSUInteger i = 0 ; ! looksJSON && i < len ; i + + ) {
unsigned char c = bytes [ i ] ;
if ( c = = ' ' || c = = ' \ n ' || c = = ' \ r ' || c = = ' \ t ' ) continue ;
looksJSON = ( c = = ' { ' || c = = ' [ ' ) ;
break ;
}
}
2025-10-28 15:59:09 +08:00
if ( looksJSON ) {
2025-10-28 16:11:35 +08:00
NSError * jsonErr = nil ;
id json = [ NSJSONSerialization JSONObjectWithData : data options : 0 error : & jsonErr ] ;
2025-11-17 20:07:39 +08:00
if ( jsonErr ) { if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorDecodeFailed userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Failed to parse JSON" ) } ] ) ; return ; }
2025-10-28 15:59:09 +08:00
if ( completion ) completion ( json , response , nil ) ;
} else {
if ( completion ) completion ( data , response , nil ) ;
}
} ] ;
[ task resume ] ;
return task ;
}
2025-10-28 16:11:35 +08:00
# pragma mark - AFHTTPSessionManager
2025-10-28 15:59:09 +08:00
2025-10-28 16:11:35 +08:00
- ( AFHTTPSessionManager * ) manager {
if ( ! _manager ) {
2025-10-28 15:59:09 +08:00
NSURLSessionConfiguration * cfg = [ NSURLSessionConfiguration ephemeralSessionConfiguration ] ;
cfg . requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData ;
2025-11-13 15:34:56 +08:00
// 不 在 会 话 级 别 设 置 超 时 , 避 免 与 per - request 的 serializer . timeoutInterval 产 生 不 一 致
2025-10-28 15:59:09 +08:00
if ( @ available ( iOS 11.0 , * ) ) { cfg . waitsForConnectivity = YES ; }
2025-10-28 16:11:35 +08:00
_manager = [ [ AFHTTPSessionManager alloc ] initWithBaseURL : nil sessionConfiguration : cfg ] ;
// 默 认 不 使 用 JSON 解 析 器 , 保 持 原 生 数 据 , 再 按 需 解 析
_manager . responseSerializer = [ AFHTTPResponseSerializer serializer ] ;
2025-10-28 15:59:09 +08:00
}
2025-10-28 16:11:35 +08:00
return _manager ;
2025-10-28 15:59:09 +08:00
}
# pragma mark - Private helpers
- ( void ) fail : ( KBNetworkError ) code completion : ( KBNetworkCompletion ) completion {
2025-11-17 20:07:39 +08:00
NSString * msg = KBLocalized ( @ "Network error" ) ;
2025-10-28 15:59:09 +08:00
switch ( code ) {
2025-11-17 20:07:39 +08:00
case KBNetworkErrorDisabled : msg = KBLocalized ( @ "Network disabled (Full Access may be off)" ) ; break ;
case KBNetworkErrorInvalidURL : msg = KBLocalized ( @ "Invalid URL" ) ; break ;
case KBNetworkErrorInvalidResponse : msg = KBLocalized ( @ "Invalid response" ) ; break ;
case KBNetworkErrorDecodeFailed : msg = KBLocalized ( @ "Parse failed" ) ; break ;
2025-10-28 15:59:09 +08:00
default : break ;
}
NSError * e = [ NSError errorWithDomain : KBNetworkErrorDomain
code : code
userInfo : @ { NSLocalizedDescriptionKey : msg } ] ;
if ( completion ) completion ( nil , nil , e ) ;
}
@ end