今天,我發現淘寶手機app可以把用戶喜歡的店鋪保存到app的桌面上,感覺很神奇,研究了下怎麼做,並記錄下來順便分享下心得。附上demo地址
下面是實際效果:
安裝描述文件
safari生成webclip
這種效果就是蘋果的webclip,app上要生成它主要有2種方式。
通過安裝描述文件的方式生成webclip
使用iphone configuration utility生成一個webclip描述文件。
下載iphone configuration utility後配置一個描述文件,導出即可。
使用safari安裝這個描述文件。
使用[UIApplication sharedApplication] openURL:的方式無法直接打開描述文件,UIWebView也不支持打開這種文件類型。
safari是可以直接安裝描述文件的,但是safari和應用是2個獨立的沙盒,所以這裡需要解決應用和safari共享文件的問題。這裡使用的思路是把app作為一個服務器,讓safari訪問這個服務器獲取到描述文件進行安裝,因為程序進入後台後還可以運行一段時間,所以這裡是可行的。
可以使用第三方庫CocoaHTTPServer在app端運行一個服務器。safari中訪問 loacalhost:端口號/目錄即可打開文件。
- (void)startServer { // Create server using our custom MyHTTPServer class _httpServer = [[RoutingHTTPServer alloc] init]; // Tell the server to broadcast its presence via Bonjour. // This allows browsers such as Safari to automatically discover our service. [_httpServer setType:@"_http._tcp."]; // Normally there's no need to run our server on any specific port. // Technologies like Bonjour allow clients to dynamically discover the server's port at runtime. // However, for easy testing you may want force a certain port so you can just hit the refresh button. [_httpServer setPort:12345]; NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; [_httpServer setDocumentRoot:documentsDirectory]; if (_httpServer.isRunning) [_httpServer stop]; NSError *error; if([_httpServer start:&error]) { NSLog(@"Started HTTP Server on port %hu", [_httpServer listeningPort]); } else { NSLog(@"Error starting HTTP Server: %@", error); // Probably should add an escape - but in practice never loops more than twice (bug filed on GitHub https://github.com/robbiehanson/CocoaHTTPServer/issues/88) [self startServer]; } } - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{ [application endBackgroundTask:backgroundTask]; backgroundTask = UIBackgroundTaskInvalid; }]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [self startServer]; return YES; }
safari中打開關鍵代碼
__weak AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; UInt16 port = appDelegate.httpServer.port; NSLog(@"%u", port); if (success) [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%u/profile.mobileconfig", port]]]; else NSLog(@"Error generating profile");
通過safari自帶功能生成webclip
safari帶有一個為當前網頁生成webclip的功能,現在我們就需要使用這個方式來生成webclip。
這種方式的工作流程是這樣的:先使用app打開safari並顯示指定的網頁內容(一般是指導用戶怎麼使用safari生成webclip,並打開safari的js控制),然後用戶打開js權限,保存webclip,下次用戶點擊桌面上的webclip後即可再次打開該網頁,觸發js,跳回app。
因為用戶點開webclip的時候需要獲取到網頁的所有信息,又因為我們的應用不是長時間在後台運行的,所以需要把所有網頁的內容以url的形式中保存在webclip中,這種技術叫做data-url技術。
我們需要app把網頁內容通過data-url的形式傳給safari,我嘗試過使用openurl的傳輸方式,這種方式不能傳輸太長的數據,行不通。所以這裡的思路也是把app變成一個服務器,safari訪問的時候返回302讓TA重定向,重定向的url返回我們要傳輸的data-url,safari重定向後即可顯示我們指定的網頁內容。這裡我們可以用基於CocoaHTTPServer之上封裝的庫RoutingHTTPServer。
配置並傳輸data-url
//配置返回值 [appDelegate.httpServer get:@"/old" withBlock:^(RouteRequest *request, RouteResponse *response) { [response setStatusCode:302]; // or 301 [response setHeader:@"Location" value:@"data:text/html;charset=UTF-8,"]; }]; //跳轉 UInt16 port = appDelegate.httpServer.port; NSLog(@"%u", port); [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%u/old", port]]];
用戶打開js
通過safari保存webclip
data-url中加入js
通過safari打開的html是處於safari mode,而直接通過webclip打開的html是處於app mode,可以理解為safari mode是嵌入在safari中的網頁,app mode的網頁是單獨的網頁,通過這個狀態我們可以控制什麼時候調用js,來控制最終是展示當前網頁還是跳轉到我們指定的app。這裡我寫的是 sample:// ,可以按照需要替換成app的scheme,即可跳轉到app。
其他可以做的細節
html和配置文件,我們都可以通過替換字符串等方式修改最終生成的內容,以此來針對不同用戶生成不同內容。
NSString *templatePath = [[NSBundle mainBundle] pathForResource:@"phone_template" ofType:@"mobileconfig"]; NSString *data = [NSString stringWithContentsOfFile:templatePath encoding:NSUTF8StringEncoding error:NULL]; data = [data stringByReplacingOccurrencesOfString:@"!!NAME!!" withString:name]; data = [data stringByReplacingOccurrencesOfString:@"!!PHONENUMBER!!" withString:phoneNumber]; NSLog(@"%@", data); BOOL success = [data writeToFile:[ProfileGenerator profilePath] atomically:YES encoding:NSUTF8StringEncoding error:nil]; return success;
端口號不一定要寫死,這裡僅僅是方便測試。
總結
2種方式都可以達到最終效果,選取哪種方式去實現,可以自己評估優劣。由於本人對服務端和前端不太熟悉,實現還有2點不足之處,希望有人能給出些比較好的方案。
可能由於浏覽器緩存的問題,如果之前safari打開過localhost:端口號,下次再進入時可能不會去重定向,導致webclip保存的不是重定向後的url,而是原本請求的url。
重定向返回的response header長度這裡也是有限制的,過長會造成截斷,這裡應該是可以通過代碼改進的。