Files
keyboard/CustomKeyboard/Network/KBNetworkManager.m
2025-11-17 20:07:39 +08:00

196 lines
9.4 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBNetworkManager.m
// CustomKeyboard
//
#import "KBNetworkManager.h"
#import "AFNetworking.h"
#import "KBAuthManager.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;
// 默认接受任意类型,避免下载图片/二进制被服务端基于 Accept 拒绝
_defaultHeaders = @{ @"Accept": @"*/*" };
// 设置基础域名,路径可相对该地址拼接
_baseURL = [NSURL URLWithString:KB_BASE_URL];
}
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;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid URL")}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
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];
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: KBLocalized(@"Network disabled (Full Access may be off)")}];
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) {
// 统一为目录型 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;
}
return path; // 当无 baseURL 且 path 不是完整 URL 时,让 AFN 处理(可能失败)
}
- (void)applyHeaders:(NSDictionary<NSString *,NSString *> *)headers toMutableRequest:(NSMutableURLRequest *)req contentType:(NSString *)contentType {
// 合并默认头与局部头,并注入授权头(若可用)。局部覆盖优先。
NSMutableDictionary *all = [self.defaultHeaders mutableCopy] ?: [NSMutableDictionary new];
NSDictionary *auth = [[KBAuthManager shared] authorizationHeader];
[auth enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { all[key] = obj; }];
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) {
// AFN 默认对非 2xx 的状态码返回 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:KBLocalized(@"No data")}]);
return;
}
NSString *ct = nil;
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
}
// 更宽松的 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;
}
}
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:KBLocalized(@"Failed to parse 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;
// 不在会话级别设置超时,避免与 per-request 的 serializer.timeoutInterval 产生不一致
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 = 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);
}
@end