// // KBNetworkManager.m // CustomKeyboard // #import "KBNetworkManager.h" NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; @interface KBNetworkManager () @property (nonatomic, strong) NSURLSession *session; // 懒创建,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" }; } return self; } #pragma mark - Public - (NSURLSessionDataTask *)GET:(NSString *)path parameters:(NSDictionary *)parameters headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { if (![self ensureEnabled:completion]) return nil; NSURL *url = [self buildURLWithPath:path params:parameters]; if (!url) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; } NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:self.timeout]; req.HTTPMethod = @"GET"; [self applyHeaders:headers toRequest:req contentType:nil]; return [self startTaskWithRequest:req completion:completion]; } - (NSURLSessionDataTask *)POST:(NSString *)path jsonBody:(id)jsonBody headers:(NSDictionary *)headers completion:(KBNetworkCompletion)completion { if (![self ensureEnabled:completion]) return nil; NSURL *url = [self buildURLWithPath:path params:nil]; if (!url) { [self fail:KBNetworkErrorInvalidURL completion:completion]; return nil; } NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:self.timeout]; req.HTTPMethod = @"POST"; NSData *body = nil; if (jsonBody) { if ([NSJSONSerialization isValidJSONObject:jsonBody]) { body = [NSJSONSerialization dataWithJSONObject:jsonBody options:0 error:NULL]; } else if ([jsonBody isKindOfClass:[NSData class]]) { body = (NSData *)jsonBody; // 允许直接传入 NSData } } if (body) req.HTTPBody = body; [self applyHeaders:headers toRequest:req contentType:(body ? @"application/json" : nil)]; return [self startTaskWithRequest: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; } - (NSURL *)buildURLWithPath:(NSString *)path params:(NSDictionary *)params { NSURL *url = nil; if (path.length == 0) return nil; if ([path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) { url = [NSURL URLWithString:path]; } else if (self.baseURL) { url = [NSURL URLWithString:path relativeToURL:self.baseURL].absoluteURL; } if (!url) return nil; if (params.count == 0) return url; NSURLComponents *c = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; NSMutableArray *items = c.queryItems.mutableCopy ?: [NSMutableArray array]; [params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { NSString *k = [key description]; NSString *v = [obj isKindOfClass:[NSString class]] ? obj : [obj description]; [items addObject:[NSURLQueryItem queryItemWithName:k value:v]]; }]; c.queryItems = items; return c.URL; } - (void)applyHeaders:(NSDictionary *)headers toRequest:(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 *)startTaskWithRequest:(NSURLRequest *)req completion:(KBNetworkCompletion)completion { NSURLSessionDataTask *task = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { if (completion) completion(nil, response, error); return; } if (!data) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"空响应"}]); return; } id json = nil; // 简单判断是否为 JSON NSString *ct = nil; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"]; } BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]); if (looksJSON) { json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (!json && 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 - Session - (NSURLSession *)session { if (!_session) { 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; } _session = [NSURLSession sessionWithConfiguration:cfg delegate:nil delegateQueue:nil]; } return _session; } #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