pod 网络库

This commit is contained in:
2025-10-28 15:59:09 +08:00
parent c2859f888a
commit f28f7de49d
86 changed files with 2359 additions and 14882 deletions

View File

@@ -0,0 +1,57 @@
//
// KBNetworkManager.h
// CustomKeyboard
//
// 轻量网络层封装(扩展安全)。支持 GET/POST(JSON)。
// 注意:键盘扩展需要"允许完全访问"后才可联网,
// 建议由宿主控制器在确认后调用 `setEnabled:YES` 再发起请求。
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
extern NSErrorDomain const KBNetworkErrorDomain;
typedef NS_ERROR_ENUM(KBNetworkErrorDomain, KBNetworkError) {
KBNetworkErrorDisabled = 1, // 未启用网络(例如未开启完全访问)
KBNetworkErrorInvalidURL = 2,
KBNetworkErrorInvalidResponse = 3,
KBNetworkErrorDecodeFailed = 4,
};
/// 简单的 JSON 回调json 为 NSDictionary/NSArray 或者在非 JSON 情况下返回 NSData
typedef void(^KBNetworkCompletion)(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error);
@interface KBNetworkManager : NSObject
/// 单例
+ (instancetype)shared;
/// 是否允许网络(默认为 NO宿主在合适时机置 YES
@property (atomic, assign, getter=isEnabled) BOOL enabled;
/// 可选的基础域名,例如 https://api.example.com
@property (nonatomic, strong, nullable) NSURL *baseURL;
/// 全局默认请求头(每次请求会与局部 headers 合并,局部优先)
@property (nonatomic, copy) NSDictionary<NSString *, NSString *> *defaultHeaders;
/// 超时时间(默认 10s
@property (nonatomic, assign) NSTimeInterval timeout;
/// GET 请求parameters 会拼到 URL 上
- (nullable NSURLSessionDataTask *)GET:(NSString *)path
parameters:(nullable NSDictionary *)parameters
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
/// POST JSON 请求jsonBody 会以 application/json 发送
- (nullable NSURLSessionDataTask *)POST:(NSString *)path
jsonBody:(nullable id)jsonBody
headers:(nullable NSDictionary<NSString *, NSString *> *)headers
completion:(KBNetworkCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,167 @@
//
// 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