Files
keyboard/CustomKeyboard/Network/KBNetworkManager.m
2025-10-28 15:59:09 +08:00

168 lines
7.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"
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<NSString *,NSString *> *)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<NSString *,NSString *> *)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<NSURLQueryItem *> *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<NSString *,NSString *> *)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