// // KBNetworkManager.m // CustomKeyboard // #import "KBNetworkManager.h" #import "AFNetworking.h" NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; @interface KBNetworkManager () @property (nonatomic, strong) AFHTTPSessionManager *manager; // AFN 管理器(ephemeral 配置) // 私有错误派发 - (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; _defaultHeaders = @{ @"Accept": @"application/json" }; // 设置基础域名,路径可相对该地址拼接 _baseURL = [NSURL URLWithString:KB_BASE_URL]; } return self; } #pragma mark - Public - (NSURLSessionDataTask *)GET:(NSString *)path parameters:(NSDictionary *)parameters headers:(NSDictionary *)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; NSMutableURLRequest *req = [serializer requestWithMethod:@"GET" URLString:urlString parameters:parameters error:NULL]; [self applyHeaders:headers toMutableRequest:req contentType:nil]; return [self startAFTaskWithRequest:req completion:completion]; } - (NSURLSessionDataTask *)POST:(NSString *)path jsonBody:(id)jsonBody headers:(NSDictionary *)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]; return [self startAFTaskWithRequest:req completion:completion]; } #pragma mark - Core - (BOOL)ensureEnabled:(KBNetworkCompletion)completion { if (!self.isEnabled) { NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDisabled userInfo:@{NSLocalizedDescriptionKey: @"网络未启用(可能未开启完全访问)"}]; 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) { return [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteURL.absoluteString; } return path; // 当无 baseURL 且 path 不是完整 URL 时,让 AFN 处理(可能失败) } - (void)applyHeaders:(NSDictionary *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType { // 合并默认头与局部头,局部覆盖 NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new]; 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) { if (error) { if (completion) completion(nil, response, error); return; } NSData *data = (NSData *)responseObject; if (![data isKindOfClass:[NSData class]]) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"无数据"}]); return; } NSString *ct = nil; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"]; } BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]); if (looksJSON) { NSError *jsonErr = nil; id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr]; if (jsonErr) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]); return; } if (completion) completion(json, response, nil); } else { if (completion) completion(data, response, nil); } }]; [task resume]; return task; } #pragma mark - AFHTTPSessionManager - (AFHTTPSessionManager *)manager { if (!_manager) { NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration]; cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; cfg.timeoutIntervalForRequest = self.timeout; cfg.timeoutIntervalForResource = MAX(self.timeout, 30.0); 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 - (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion { NSString *msg = @"网络错误"; switch (code) { case KBNetworkErrorDisabled: msg = @"网络未启用(可能未开启完全访问)"; break; case KBNetworkErrorInvalidURL: msg = @"无效的URL"; break; case KBNetworkErrorInvalidResponse: msg = @"无效的响应"; break; case KBNetworkErrorDecodeFailed: msg = @"解析失败"; break; default: break; } NSError *e = [NSError errorWithDomain:KBNetworkErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: msg}]; if (completion) completion(nil, nil, e); } @end