168 lines
7.4 KiB
Objective-C
168 lines
7.4 KiB
Objective-C
//
|
||
// 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
|