iOS通过本地server创建桌面快捷方式
2021-07-10 本文已影响0人
野码道人
iOS14苹果添加本地网络权限
使用苹果的bonjour技术在iOS14开始被检测,并且会弹窗访问权限弹窗,很鸡肋,即便点击拒绝也是可以正常访问本地网络的,一旦苹果检测到NSNetService相关api,就会在app的设置界面出现一个选项叫做本地网络,很难受,GCDWebServer使用套接字实现,直接访问CFNetwork层,可以跳过这个权限的检测
下载链接
原理
GCDWebServer负责启动本地server,地址:@"http://127.0.0.1:6123",端口号随意写一个,只要避开主流协议端口如443、8080就行,避免端口被占用导致的server启动失败,然后通过openURL的方式访问本地服务,会跳转到浏览器,此时浏览器地址栏中的地址就是👆上面的地址,会访问到我们写的html,书签存储的就是地址栏中的内容,因此需要重定向修改地址栏中的内容,iOS14系统添加了本地网络权限的同时Safari禁止了以h5标签的形式重定向,呵呵😂,怎么办,只有由服务器直接重定向了,以@"data:text/html;charset=UTF-8"开头,后面拼接html存储到沙盒,然后重定向访问fileURL,这样地址栏中的地址就变成我们需要的地址了
流程
- 创建服务目录
- (instancetype)init {
if (self = [super init]) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = paths[0];
self.webRootDir = [documentsPath stringByAppendingPathComponent:@"server/web"];
BOOL isDirectory = YES;
BOOL exsit = [[NSFileManager defaultManager] fileExistsAtPath:_webRootDir isDirectory:&isDirectory];
if(!exsit){
[[NSFileManager defaultManager] createDirectoryAtPath:_webRootDir withIntermediateDirectories:YES attributes:nil error:nil];
}
return self;
}
+ (NSString *)webRedirectPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = paths[0];
NSString *webRootDir = [documentsPath stringByAppendingPathComponent:@"server/web"];
BOOL isDirectory = YES;
BOOL exsit = [[NSFileManager defaultManager] fileExistsAtPath:webRootDir isDirectory:&isDirectory];
if(!exsit){
[[NSFileManager defaultManager] createDirectoryAtPath:webRootDir withIntermediateDirectories:YES attributes:nil error:nil];
}
return [NSString stringWithFormat:@"%@/server/web/redirectPath",documentsPath];
}
- 把index.html写入目录
这里有个注意点显示的html与写入文件html有一点不同,写入到本地的html需要在显示的内容前面拼接@"data:text/html;charset=UTF-8"
+ (NSString *)createRedirectHtmlWithModel:(BookmarkModel *)model {
NSString *title = model.title;
NSString *urlScheme = model.urlScheme;
NSString *moduleID = model.moduleID;
NSString *imageName = model.imageName;
NSMutableString *taragerUrl = [NSMutableString stringWithFormat:@"<html><head><meta content=\"yes\" name=\"apple-mobile-web-app-capable\" /><meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\" /><title>%@</title></head><body bgcolor=\"#ffffff\">",title];
NSString *htmlUrlScheme = [NSString stringWithFormat:@"<a href=\"%@",urlScheme];
NSString *dataUrlStr = [NSString stringWithFormat:@"%@=%@&%@=%@\" id=\"qbt\" style=\"display: none;\"></a>",@"10000",moduleID,@"1",@(1)];
UIImage *image = [UIImage imageNamed:imageName];
NSData *imageData = UIImagePNGRepresentation(image);
NSString *base6ImageStr = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
NSString *imageUrlStr = [NSString stringWithFormat:@"<span id=\"msg\"></span></body><script>if (window.navigator.standalone == true) { var lnk = document.getElementById(\"qbt\"); var evt = document.createEvent('MouseEvent'); evt.initMouseEvent('click'); lnk.dispatchEvent(evt);}else{ var addObj=document.createElement(\"link\"); addObj.setAttribute('rel','apple-touch-icon-precomposed'); addObj.setAttribute('href','data:image/png;base64,%@');",base6ImageStr];
UIImage *bubbleImage = [UIImage imageNamed:@"bubble"];
NSData *bubbleImageData = UIImagePNGRepresentation(bubbleImage);
NSString *base6BubbleImageStr = [bubbleImageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
NSString *orientationchangeJS = @"<script>window.addEventListener('orientationchange',function () {if (window.orientation == 0) {document.getElementById('img-div').setAttribute('style', 'width:267px;position:fixed;bottom:8px;left:50vw;margin-left:-133.5px;');document.getElementById('img-bottom').setAttribute('style', 'width:267px;');} else {document.getElementById('img-div').setAttribute('style', 'width:160px;position:fixed;bottom:8px;left:50vw;margin-left:-80px;');document.getElementById('img-bottom').setAttribute('style', 'width:160px;');}},false);</script>";
NSString *lastHtmlStr = [NSString stringWithFormat:@"document.getElementsByTagName(\"head\")[0].appendChild(addObj); document.getElementById(\"msg\").innerHTML='<div style=\"font-size:14px;position:fixed;width:100vw;top: 30px;text-align:center;left:0;\"> <div style=\"width:75px;margin: 0 auto;border-radius:12px;margin-bottom:10px;overflow:hidden;box-shadow: 0 6px 14px 0 rgba(9,40,71,0.2);\"><img id=\"i\" src=\"data:image/png;base64,%@\" style=\"width:75px;\"></div> 添加快捷方式到主屏幕 </div><div id=\"img-div\" style=\"width:267px;position:fixed;bottom:8px;left:50vw;margin-left:-133.5px;\"><img id=\"img-bottom\" src=\"data:image/png;base64,%@\" style=\"width:267px;\"></div>';}</script>%@</html>", base6ImageStr, base6BubbleImageStr,orientationchangeJS];
[taragerUrl appendString:htmlUrlScheme];
[taragerUrl appendString:dataUrlStr];
NSString *dataUrlEncode = [[NSString stringWithFormat:@"data:text/html;charset=UTF-8,%@", taragerUrl] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString *imageUrlEncode = [imageUrlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString *lastHtmlStrEncode = [lastHtmlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSData *data = [[NSString stringWithFormat:@"%@%@%@",dataUrlEncode,imageUrlEncode,lastHtmlStrEncode] dataUsingEncoding:NSUTF8StringEncoding];
[data writeToFile:[DesktopBookmark webRedirectPath] atomically:YES];
NSString *finalHtml = [NSString stringWithFormat:@"%@%@%@",taragerUrl,imageUrlStr,lastHtmlStr];
return finalHtml;
}
- 前后台事件监听
GCDWebServer这里有个问题,默认内部监听了前后台事件,而且后台直接关闭了服务,可以配置options参数,保持服务后台运行,如果想自己控制,需要把内部的通知移除,[[NSNotificationCenter defaultCenter] removeObserver:_webServer],然后自己控制,如下,后台延时两秒关闭保证服务能被访问到,进入前台如果服务还在运行,直接关闭
- (void)applicationDidBecomeActive {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
if (self.webServer.isRunning) {
[self.webServer stop];
self.webServer = nil;
}
});
}
- (void)applicationDidEnterBackground {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
[self.webServer stop];
self.webServer = nil;
});
}
- 服务生命周期
由于服务需要持续运行,_webServer对象直到服务关闭才能允许被销毁,否则会异常,因此可以写一个单例来管理server
+ (instancetype)sharedInstance {
static DesktopBookmark *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- 重写server的response,把我们写好的html以字符串的形式配置进去
NSString *html = [DesktopBookmark createRedirectHtmlWithModel:model];
self.webServer = [[GCDWebServer alloc] init];
[_webServer addDefaultHandlerForMethod:@"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {
return [GCDWebServerDataResponse responseWithHTML:html];
}];
- 配置response的重定向地址
[_webServer addHandlerForMethod:@"GET" path:@"/" requestClass:[GCDWebServerRequest class] processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {
NSError *error;
NSString *redirectPath = [[NSString alloc] initWithContentsOfFile:[DesktopBookmark webRedirectPath] encoding:NSUTF8StringEncoding error:&error];
NSURL *url = [NSURL URLWithString:redirectPath];
return [GCDWebServerResponse responseWithRedirect:url permanent:NO];
}];
- 启动server
注意这里要用固定的端口号和bonjourName为nil,以成功绕过苹果的本地网络权限检测
[_webServer startWithPort:6123 bonjourName:nil];
- 跳转到浏览器
NSString *urlStrWithPort = @"http://127.0.0.1:6123";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlStrWithPort]];