2025-10-29 20:57:45 +08:00
//
// KBNetworkManager . m
// CustomKeyboard
//
# import "KBNetworkManager.h"
# import "AFNetworking.h"
2025-10-31 16:06:54 +08:00
# import "KBAuthManager.h"
2025-10-29 20:57:45 +08:00
NSErrorDomain const KBNetworkErrorDomain = @ "com.company.keyboard.network" ;
@ interface KBNetworkManager ( )
@ property ( nonatomic , strong ) AFHTTPSessionManager * manager ; // AFN 管 理 器 ( ephemeral 配 置 )
// 私 有 错 误 派 发
- ( void ) fail : ( KBNetworkError ) code completion : ( KBNetworkCompletion ) completion ;
@ end
2025-11-13 15:34:56 +08:00
# if DEBUG
// 在 使 用 到 Debug 辅 助 方 法 之 前 做 前 置 声 明 , 避 免 出 现
// “ No visible @ interface declares the selector … ” 的 编 译 错 误 。
@ interface KBNetworkManager ( Debug )
- ( NSString * ) kb_prettyJSONStringFromObject : ( id ) obj ;
- ( NSString * ) kb_textFromData : ( NSData * ) data ;
- ( NSString * ) kb_trimmedString : ( NSString * ) s maxLength : ( NSUInteger ) maxLen ;
@ end
# endif
2025-10-29 20:57:45 +08:00
@ 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-29 20:57:45 +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 ;
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-29 20:57:45 +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-29 20:57:45 +08:00
[ self applyHeaders : headers toMutableRequest : req contentType : nil ] ;
2025-11-13 15:34:56 +08:00
# if DEBUG
// 打 印 请 求 信 息 ( GET )
NSString * paramStr = [ self kb_prettyJSONStringFromObject : parameters ] ? : @ "(null)" ;
KBLOG ( @ "HTTP GET\nURL: %@\nHeaders: %@\n参数: %@" ,
req . URL . absoluteString ,
req . allHTTPHeaderFields ? : @ { } ,
paramStr ) ;
# endif
2025-10-29 20:57:45 +08:00
return [ self startAFTaskWithRequest : req completion : completion ] ;
}
- ( NSURLSessionDataTask * ) POST : ( NSString * ) path
jsonBody : ( id ) jsonBody
headers : ( NSDictionary < NSString * , NSString * > * ) headers
completion : ( KBNetworkCompletion ) completion {
if ( ! [ self ensureEnabled : completion ] ) return nil ;
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 ] ;
2025-11-13 15:34:56 +08:00
# if DEBUG
// 打 印 请 求 信 息 ( POST JSON )
NSString * bodyStr = [ self kb_prettyJSONStringFromObject : jsonBody ] ? : @ "(null)" ;
KBLOG ( @ "HTTP POST\nURL: %@\nHeaders: %@\nJSON: %@" ,
req . URL . absoluteString ,
req . allHTTPHeaderFields ? : @ { } ,
bodyStr ) ;
# endif
2025-10-29 20:57:45 +08:00
return [ self startAFTaskWithRequest : req completion : completion ] ;
}
# 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-29 20:57:45 +08:00
if ( completion ) completion ( nil , nil , e ) ;
return NO ;
}
return YES ;
}
- ( NSString * ) buildURLStringWithPath : ( NSString * ) path {
if ( path . length = = 0 ) return nil ;
if ( [ path hasPrefix : @ "http://" ] || [ path hasPrefix : @ "https://" ] ) {
return path ;
}
if ( self . baseURL ) {
2025-11-13 16:23:46 +08:00
// 1 ) 统 一 为 目 录 型 base ( 确 保 以 / 结 尾 ) , 否 则 相 对 路 径 会 把 最 后 一 段 当 文 件 替 换
NSString * base = self . baseURL . absoluteString ? : @ "" ;
if ( ! [ base hasSuffix : @ "/" ] ) { base = [ base stringByAppendingString : @ "/" ] ; }
NSURL * dirBase = [ NSURL URLWithString : base ] ;
// 2 ) 防 呆 : 调 用 方 若 传 了 以 “ / ” 开 头 的 path , 会 导 致 相 对 路 径 从 根 覆 盖 , 丢 失 / api
NSString * relative = ( [ path hasPrefix : @ "/" ] ) ? [ path substringFromIndex : 1 ] : path ;
return [ NSURL URLWithString : relative relativeToURL : dirBase ] . absoluteURL . absoluteString ;
2025-10-29 20:57:45 +08:00
}
return path ; // 当 无 baseURL 且 path 不 是 完 整 URL 时 , 让 AFN 处 理 ( 可 能 失 败 )
}
- ( void ) applyHeaders : ( NSDictionary < NSString * , NSString * > * ) headers toMutableRequest : ( NSMutableURLRequest * ) req contentType : ( NSString * ) contentType {
2025-10-31 16:06:54 +08:00
// 合 并 默 认 头 与 局 部 头 , 并 注 入 授 权 头 ( 若 可 用 ) 。 局 部 覆 盖 优 先 。
2025-10-29 20:57:45 +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-29 20:57:45 +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 ] ; } ] ;
}
- ( 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 DEBUG
NSInteger status = [ response isKindOfClass : NSHTTPURLResponse . class ] ? ( ( NSHTTPURLResponse * ) response ) . statusCode : 0 ;
KBLOG ( @ "请求失败\nURL: %@\n状态: %ld\n错误: %@\nUserInfo: %@" ,
req . URL . absoluteString ,
( long ) status ,
error . localizedDescription ,
error . userInfo ? : @ { } ) ;
# endif
if ( completion ) completion ( nil , response , error ) ;
return ;
}
2025-10-29 20:57:45 +08:00
NSData * data = ( NSData * ) responseObject ;
if ( ! [ data isKindOfClass : [ NSData class ] ] ) {
2025-11-17 20:07:39 +08:00
# if DEBUG
KBLOG ( @ "无效响应\nURL: %@\n说明: %@" , req . URL . absoluteString , KBLocalized ( @ "未获取到数据" ) ) ;
# endif
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorInvalidResponse userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "No data" ) } ] ) ;
return ;
}
2025-10-29 20:57:45 +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-29 20:57:45 +08:00
if ( looksJSON ) {
NSError * jsonErr = nil ;
id json = [ NSJSONSerialization JSONObjectWithData : data options : 0 error : & jsonErr ] ;
2025-11-13 15:34:56 +08:00
if ( jsonErr ) {
2025-11-17 20:07:39 +08:00
# if DEBUG
2025-11-13 15:34:56 +08:00
KBLOG ( @ "响应解析失败(JSON)\nURL: %@\n错误: %@" ,
req . URL . absoluteString ,
jsonErr . localizedDescription ) ;
2025-11-17 20:07:39 +08:00
# endif
if ( completion ) completion ( nil , response , [ NSError errorWithDomain : KBNetworkErrorDomain code : KBNetworkErrorDecodeFailed userInfo : @ { NSLocalizedDescriptionKey : KBLocalized ( @ "Failed to parse JSON" ) } ] ) ;
2025-11-13 15:34:56 +08:00
return ;
}
# if DEBUG
NSString * pretty = [ self kb_prettyJSONStringFromObject : json ] ? : @ "(null)" ;
pretty = [ self kb_trimmedString : pretty maxLength : 4096 ] ;
NSInteger status = [ response isKindOfClass : NSHTTPURLResponse . class ] ? ( ( NSHTTPURLResponse * ) response ) . statusCode : 0 ;
KBLOG ( @ "响应成功(JSON)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@" ,
req . URL . absoluteString ,
( long ) status ,
ct ? : @ "" ,
pretty ) ;
# endif
2025-10-29 20:57:45 +08:00
if ( completion ) completion ( json , response , nil ) ;
} else {
2025-11-13 15:34:56 +08:00
# if DEBUG
NSString * text = [ self kb_textFromData : data ] ;
text = [ self kb_trimmedString : text maxLength : 4096 ] ;
NSInteger status = [ response isKindOfClass : NSHTTPURLResponse . class ] ? ( ( NSHTTPURLResponse * ) response ) . statusCode : 0 ;
KBLOG ( @ "响应成功(Data)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@" ,
req . URL . absoluteString ,
( long ) status ,
ct ? : @ "" ,
text ) ;
# endif
2025-10-29 20:57:45 +08:00
if ( completion ) completion ( data , response , nil ) ;
}
} ] ;
[ task resume ] ;
return task ;
}
# pragma mark - AFHTTPSessionManager
- ( AFHTTPSessionManager * ) manager {
if ( ! _manager ) {
NSURLSessionConfiguration * cfg = [ NSURLSessionConfiguration ephemeralSessionConfiguration ] ;
cfg . requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData ;
2025-11-13 15:34:56 +08:00
// 不 在 会 话 级 别 设 置 超 时 , 避 免 与 per - request 的 serializer . timeoutInterval 产 生 不 一 致
2025-10-29 20:57:45 +08:00
if ( @ available ( iOS 11.0 , * ) ) { cfg . waitsForConnectivity = YES ; }
_manager = [ [ AFHTTPSessionManager alloc ] initWithBaseURL : nil sessionConfiguration : cfg ] ;
// 默 认 不 使 用 JSON 解 析 器 , 保 持 原 生 数 据 , 再 按 需 解 析
_manager . responseSerializer = [ AFHTTPResponseSerializer serializer ] ;
}
return _manager ;
}
# pragma mark - Private helpers
2025-11-17 20:07:39 +08:00
- ( void ) fail : ( KBNetworkError ) code completion : ( KBNetworkCompletion ) completion {
NSString * msg = KBLocalized ( @ "Network error" ) ;
switch ( code ) {
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 ;
default : break ;
}
NSError * e = [ NSError errorWithDomain : KBNetworkErrorDomain
code : code
userInfo : @ { NSLocalizedDescriptionKey : msg } ] ;
if ( completion ) completion ( nil , nil , e ) ;
2025-10-29 20:57:45 +08:00
}
@ end
2025-11-13 15:34:56 +08:00
# pragma mark - Debug helpers
# if DEBUG
@ implementation KBNetworkManager ( Debug )
// 将 对 象 转 为 漂 亮 的 JSON 字 符 串 ; 否 则 返 回 description
- ( NSString * ) kb_prettyJSONStringFromObject : ( id ) obj {
if ( ! obj || obj = = ( id ) kCFNull ) return nil ;
if ( ! [ NSJSONSerialization isValidJSONObject : obj ] ) {
// 非 标 准 JSON 对 象 , 直 接 description
return [ obj description ] ;
}
NSData * data = [ NSJSONSerialization dataWithJSONObject : obj options : NSJSONWritingPrettyPrinted error : NULL ] ;
if ( ! data ) return [ obj description ] ;
return [ [ NSString alloc ] initWithData : data encoding : NSUTF8StringEncoding ] ? : [ obj description ] ;
}
// 尝 试 把 二 进 制 数 据 转 为 可 读 文 本 ; 失 败 则 返 回 占 位 带 长 度
- ( NSString * ) kb_textFromData : ( NSData * ) data {
if ( ! data ) return @ "(null)" ;
if ( data . length = = 0 ) return @ "" ;
// 优 先 UTF -8
NSString * text = [ [ NSString alloc ] initWithData : data encoding : NSUTF8StringEncoding ] ;
if ( text . length > 0 ) return text ;
// 再 尝 试 常 见 编 码
NSArray * encs = @ [ @ ( NSASCIIStringEncoding ) , @ ( NSISOLatin1StringEncoding ) , @ ( NSUnicodeStringEncoding ) ] ;
for ( NSNumber * n in encs ) {
text = [ [ NSString alloc ] initWithData : data encoding : n . unsignedIntegerValue ] ;
if ( text . length > 0 ) return text ;
}
return [ NSString stringWithFormat : @ "<binary %lu bytes>" , ( unsigned long ) data . length ] ;
}
// 过 长 时 裁 剪 , 避 免 刷 屏
- ( NSString * ) kb_trimmedString : ( NSString * ) s maxLength : ( NSUInteger ) maxLen {
if ( ! s ) return @ "" ;
if ( s . length <= maxLen ) return s ;
NSString * head = [ s substringToIndex : maxLen ] ;
return [ head stringByAppendingString : @ "\n...<trimmed>..." ] ;
}
@ end
# endif