This commit is contained in:
2025-11-13 15:34:56 +08:00
parent debbe2777b
commit f406416698
9 changed files with 263 additions and 19 deletions

View File

@@ -15,6 +15,16 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
- (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion;
@end
#if DEBUG
// 使 Debug
// No visible @interface declares the selector
@interface KBNetworkManager (Debug)
- (NSString *)kb_prettyJSONStringFromObject:(id)obj;
- (NSString *)kb_textFromData:(NSData *)data;
- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen;
@end
#endif
@implementation KBNetworkManager
+ (instancetype)shared {
@@ -26,7 +36,8 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
if (self = [super init]) {
_enabled = NO; //
_timeout = 10.0;
_defaultHeaders = @{ @"Accept": @"application/json" };
// / Accept
_defaultHeaders = @{ @"Accept": @"*/*" };
//
_baseURL = [NSURL URLWithString:KB_BASE_URL];
}
@@ -45,11 +56,24 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
// 使 AFHTTPRequestSerializer
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
serializer.timeoutInterval = self.timeout;
NSError *serror = nil;
NSMutableURLRequest *req = [serializer requestWithMethod:@"GET"
URLString:urlString
parameters:parameters
error:NULL];
error:&serror];
if (serror || !req) {
if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: @"无效的URL"}]);
return nil;
}
[self applyHeaders:headers toMutableRequest:req contentType:nil];
#if DEBUG
// GET
NSString *paramStr = [self kb_prettyJSONStringFromObject:parameters] ?: @"(null)";
KBLOG(@"HTTP GET\nURL: %@\nHeaders: %@\n参数: %@",
req.URL.absoluteString,
req.allHTTPHeaderFields ?: @{},
paramStr);
#endif
return [self startAFTaskWithRequest:req completion:completion];
}
@@ -70,6 +94,14 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
error:&error];
if (error) { if (completion) completion(nil, nil, error); return nil; }
[self applyHeaders:headers toMutableRequest:req contentType:nil];
#if DEBUG
// POST JSON
NSString *bodyStr = [self kb_prettyJSONStringFromObject:jsonBody] ?: @"(null)";
KBLOG(@"HTTP POST\nURL: %@\nHeaders: %@\nJSON: %@",
req.URL.absoluteString,
req.allHTTPHeaderFields ?: @{},
bodyStr);
#endif
return [self startAFTaskWithRequest:req completion:completion];
}
@@ -109,9 +141,24 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
// 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; }
// AFN 2xx error
if (error) {
#if DEBUG
NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
KBLOG(@"请求失败\nURL: %@\n状态: %ld\n错误: %@\nUserInfo: %@",
req.URL.absoluteString,
(long)status,
error.localizedDescription,
error.userInfo ?: @{});
#endif
if (completion) completion(nil, response, error);
return;
}
NSData *data = (NSData *)responseObject;
if (![data isKindOfClass:[NSData class]]) {
#if DEBUG
KBLOG(@"无效响应\nURL: %@\n说明: %@", req.URL.absoluteString, @"未获取到数据");
#endif
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"无数据"}]);
return;
}
@@ -119,13 +166,53 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"];
}
BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]);
// 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:@"JSON解析失败"}]); return; }
if (jsonErr) {
#if DEBUG
KBLOG(@"响应解析失败(JSON)\nURL: %@\n错误: %@",
req.URL.absoluteString,
jsonErr.localizedDescription);
#endif
if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]);
return;
}
#if DEBUG
NSString *pretty = [self kb_prettyJSONStringFromObject:json] ?: @"(null)";
pretty = [self kb_trimmedString:pretty maxLength:4096];
NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
KBLOG(@"响应成功(JSON)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@",
req.URL.absoluteString,
(long)status,
ct ?: @"",
pretty);
#endif
if (completion) completion(json, response, nil);
} else {
#if DEBUG
NSString *text = [self kb_textFromData:data];
text = [self kb_trimmedString:text maxLength:4096];
NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
KBLOG(@"响应成功(Data)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@",
req.URL.absoluteString,
(long)status,
ct ?: @"",
text);
#endif
if (completion) completion(data, response, nil);
}
}];
@@ -139,8 +226,7 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
if (!_manager) {
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration];
cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
cfg.timeoutIntervalForRequest = self.timeout;
cfg.timeoutIntervalForResource = MAX(self.timeout, 30.0);
// per-request serializer.timeoutInterval
if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; }
_manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg];
// 使 JSON
@@ -167,3 +253,47 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network";
}
@end
#pragma mark - Debug helpers
#if DEBUG
@implementation KBNetworkManager (Debug)
// JSON description
- (NSString *)kb_prettyJSONStringFromObject:(id)obj {
if (!obj || obj == (id)kCFNull) return nil;
if (![NSJSONSerialization isValidJSONObject:obj]) {
// JSON description
return [obj description];
}
NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted error:NULL];
if (!data) return [obj description];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: [obj description];
}
//
- (NSString *)kb_textFromData:(NSData *)data {
if (!data) return @"(null)";
if (data.length == 0) return @"";
// UTF-8
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text.length > 0) return text;
//
NSArray *encs = @[@(NSASCIIStringEncoding), @(NSISOLatin1StringEncoding), @(NSUnicodeStringEncoding)];
for (NSNumber *n in encs) {
text = [[NSString alloc] initWithData:data encoding:n.unsignedIntegerValue];
if (text.length > 0) return text;
}
return [NSString stringWithFormat:@"<binary %lu bytes>", (unsigned long)data.length];
}
//
- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen {
if (!s) return @"";
if (s.length <= maxLen) return s;
NSString *head = [s substringToIndex:maxLen];
return [head stringByAppendingString:@"\n...<trimmed>..."];
}
@end
#endif