iOS應用間跳轉:從Open in my app聊到Deeplink

就我個人所知,iOS中存在3種方式可以打開(喚醒)其它手機App(除開系統級應用),分別是:

  • 第三方登錄、分享、支付、導航等
  • Open in my app
  • Deeplink

三種方式原理一樣,均是注冊自定義URL Schemes,并處理URL請求。

URL schemes.jpeg

第三方


  1. 使用第三方用戶登錄,如微信,QQ,微博登錄,授權后返回用戶名和密碼
  2. 內容分享,跳轉到分享App的對應頁面,如分享給微信好友、分享給微信朋友圈、分享到微博
  3. 第三方支付,跳轉到第三方支付App,如支付寶支付,微信支付
  4. 顯示位置、地圖導航,跳轉到地圖應用,如高德地圖,百度地圖等
  5. 應用程序推廣,跳轉到iTunes并顯示指定App下載頁,或使用Safari打開指定網頁

1~4 第三方平臺均提供了相應SDK,具體不再闡述,5實際只需要一個網址,調用[[UIApplication sharedApplication] openURL:url]方法即可。

在iOS9中,如果使用canOpenURL:方法,該方法所涉及到的URL Schemes必須在"Info.plist"中將它們列為白名單,否則不能使用。key叫做LSApplicationQueriesSchemes,鍵值內容是對應應用程序的URL Schemes

Open in my app


iOS有個不常使用的功能,叫Open in my app,即在我的app中打開,此功能允許文件在其他app中打開。

常見如郵件的附件,輕觸蘋果會默認使用QLPreviewController打開預覽界面,而長按這時會彈出共享列表菜單,此菜單會列出所有添加了該類型文件的應用,它允許此文件在添加了對應Document Type支持的應用中打開,如下圖所示:

Open in my app

實現步驟

1. 修改Info.plist文件

1)在plist文件中添加URLTypes字段,指定程序的對外接口:

Info.plist

由于我之前已經集成了社會化分享,這一步就直接跳過。

2)添加一個Documents Type字段,該字段指定與程序關聯的文件類型,詳情參考System-Declared Uniform Type Identifiers

CFBundleTypeExtensions指定文件類型,例如pdfdocxlsppttxt等。
CFBundleTypeIconFiles指定用UIActionSheet向用戶提示打開應用時顯示的圖標。
DocumentTypeName可以自定,對應文件類型名。
Document Content Type UTIs指定官方指定的文件類型,UTIs 即 Uniform Type Identifiers。

  • PDF文件

    .pdf的配置

  • Doc文件

    .doc(s)的配置

  • Excel文件

    .xls(x)的配置

  • PPT文件

    .ppt的配置

注意:預支持PPT需要 額外配置增加public.data字段,被這個坑了好久!

  • TXT(或RTF)
    .txt的配置

2. 在 AppDelegate.m 中添加外部調用代碼

#import <QuickLook/QuickLook.h>

@interface AppDelegate ()  <QLPreviewControllerDataSource>
@property (nonatomic,strong) NSURL *openUrl; 
@end

注意:iOS 9之后請使用這個方法:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options;

-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  if (url && [url isFileURL]) {
    self.openUrl=url;       

    QLPreviewController* previewController = [[QLPreviewController alloc] init];
    [self.window.rootViewController presentViewController:previewController animated:YES completion:nil];    

    dispatch_async(dispatch_get_global_queue(0, 0), ^{      
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsPath=[[paths objectAtIndex:0] stringByAppendingPathComponent:@"downloadFile"];      
            BOOL dirHas;
            if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath isDirectory:&dirHas] ) {
                [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:NO attributes:nil error:nil];
            }  
            NSString *filePath = [documentsPath stringByAppendingPathComponent:[[url relativePath] lastPathComponent]];//Add the file name
            [data writeToFile:filePath atomically:YES];
            //寫入成功后再讀取刷新數據,避免跳轉界面時等待太久
            previewController.dataSource = self;
            [previewController reloadData];
        });
    });
    return YES;
  }
  return NO;
} 

//使用QLPreviewController預覽
- (NSInteger) numberOfPreviewItemsInPreviewController: (QLPreviewController *) controller {
    return 1;
}

- (id <QLPreviewItem>)previewController: (QLPreviewController*)controller previewItemAtIndex:(NSInteger)index {
//    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
//    NSString *docDir = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"Inbox"];
    NSString *filePath=[getDocumentPath() stringByAppendingPathComponent:[[self.openUrl relativePath] lastPathComponent]];
     if ([[filePath pathExtension] isEqualToString:@"txt"]) {
        //處理txt格式內容顯示有亂碼的情況
        NSData *fileData = [NSData dataWithContentsOfFile:filePath];
        //判斷是UNICODE編碼
        NSString *isUNICODE = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
        //還是ANSI編碼(-2147483623,-2147482591,-2147482062,-2147481296)encoding 任選一個就可以了
        NSString *isANSI = [[NSString alloc] initWithData:fileDataencoding:-2147483623];
        if (isUNICODE) {
            NSString *retStr = [[NSString alloc]initWithCString:[isUNICODE UTF8String] encoding:NSUTF8StringEncoding];
            NSData *data = [retStr dataUsingEncoding:NSUTF16StringEncoding];
            [data writeToFile:filePath atomically:YES];
        } else if (isANSI) {
            NSData *data = [isANSI dataUsingEncoding:NSUTF16StringEncoding];
            [data writeToFile:filePath atomically:YES];
        }
    }
    NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
   return fileUrl;
}

至于為什么要下載?
我經過測試發現文件都是存在沙盒Documents/Inbox/目錄下的,獲取到的文件路徑例如:file:///private/var/mobile/Containers/Data/Application/A9D2A924-A1F8-4915-B31D-57127ED987BE/Documents/Inbox/工作日報_0113.xlsx,然而卻怎么也打不開,嘗試拼接取文件路徑也不成功,于是重新下載了一份放到我指定的目錄下,也可以直接使用copyItemAtPath: toPath: error:方法拷貝文件到指定的路徑下。

更多支持文件類型(如MP3,AIFF,WAV, etc.)

stack overflow給出了各個類型所需要添加的plist字段:
http://stackoverflow.com/questions/9266079/why-is-my-ios-app-not-showing-up-in-other-apps-open-in-dialog

Deeplink(深度鏈接)


Deeplink,簡單來說就是給定一個鏈接,即可跳轉到已安裝應用中的某一個頁面的技術。

通過Deep Link,App可以通過搜索引擎進行導流。可以通過 SEO 增加訪問量從而提高 app 下載量和開啟率,可以實現Web與App間切換保留上下文信息。

最初聽到Deeplink是在魔窗的宣講會,那時一臉懵,回來仔細了解了下,相比推送跳轉的方式(現在很多用戶都是直接關閉推送的),通過分享邀請和短信喚醒,對運營拉新、拉活、留存、轉化,提供了更多幫助。

技術要求:
  • APP要想被其他APP直接打開,自身得支持,讓自己具備被人打開的能力。(URL Schemes)
  • APP要想打開其他的APP,自身也得支持。(判斷設備是否安裝、各種跳轉的處理)

iOS 9以上的用戶,可以通過Universal Links通用鏈接無縫的重定向到一個App應用,而不需要通過Safari打開跳轉。
如果用戶沒有安裝這個app,則會在Safari中打開這個鏈接指向的網頁。
喚醒方法為:- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler;

栗子??(關于Universal Links的具體配置建議查看官方文檔 蘋果有個網址可以測試apple-app-site-association是否有效測試地址)

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler{
  if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
    NSURL *webUrl = userActivity.webpageURL;
    if ([webUrl.host isEqualToString:@"com.example.ios"]) {
        //打開對應頁面
//            NSString *paramStr = [[webUrl query] substringFromIndex:[[webUrl query] rangeOfString:@"?"].location+1];
        NSDictionary *paramDic = [[webUrl absoluteString] getURLParameters];
        NSLog(@"paramDic: %@",paramDic);
        //跳轉本地ViewController
    } else {
        //不能識別,safari打開
        [[UIApplication sharedApplication] openURL:webUrl];
    }
  }
  return YES;
}

//  NSString+UrlEncoding.h
/**
 *  截取URL中的參數
 *
 *  @return NSDictionary parameters
 */
- (NSDictionary *)getURLParameters {

// 查找參數
NSRange range = [self rangeOfString:@"?"];
if (range.location == NSNotFound) {
    return nil;
}

NSMutableDictionary *params = [NSMutableDictionary dictionary];

// 截取參數
NSString *parametersString = [self substringFromIndex:range.location + 1];

// 判斷參數是單個參數還是多個參數
if ([parametersString containsString:@"&"]) {
    
    // 多個參數,分割參數
    NSArray *urlComponents = [parametersString componentsSeparatedByString:@"&"];
    
    for (NSString *keyValuePair in urlComponents) {
        // 生成Key/Value
        NSArray *pairComponents = [keyValuePair componentsSeparatedByString:@"="];
        NSString *key = [pairComponents.firstObject stringByRemovingPercentEncoding];
        NSString *value = [pairComponents.lastObject stringByRemovingPercentEncoding];
        
        // Key不能為nil
        if (key == nil || value == nil) {
            continue;
        }
        
        id existValue = [params valueForKey:key];
        
        if (existValue != nil) {
            
            // 已存在的值,生成數組
            if ([existValue isKindOfClass:[NSArray class]]) {
                // 已存在的值生成數組
                NSMutableArray *items = [NSMutableArray arrayWithArray:existValue];
                [items addObject:value];
                
                [params setValue:items forKey:key];
            } else {
                // 非數組
                [params setValue:@[existValue, value] forKey:key];
            }
            
        } else {
            // 設置值
            [params setValue:value forKey:key];
        }
    }
} else {
    // 單個參數
    
    // 生成Key/Value
    NSArray *pairComponents = [parametersString componentsSeparatedByString:@"="];
    
    // 只有一個參數,沒有值
    if (pairComponents.count == 1) {
        return nil;
    }
    
    // 分隔值
    NSString *key = [pairComponents.firstObject stringByRemovingPercentEncoding];
    NSString *value = [pairComponents.lastObject stringByRemovingPercentEncoding];
    
    // Key不能為nil
    if (key == nil || value == nil) {
        return nil;
    }
    
    // 設置值
    [params setValue:value forKey:key];
}

return [NSDictionary dictionaryWithDictionary:params];
}

測試方法:在短信或備忘錄中輸入相應域名,若能跳轉到相應App即植入成功。直接在Safari中輸入鏈接是無效的,必須從一處跳入才可以(比如上一級網頁)。

Deferred Deeplink(延遲深度鏈接)

Deeplink只針對手機中已經安裝過App的用戶才有用。而升級版本的Deferred Deeplink卻可以解決這個問題:

Deferred Deeplink 可以先判斷用戶是否已經安裝了App應用,如果沒有則先引導至App應用商店中下載App, 在用戶安裝App后跳轉到指定App頁面 Deeplink 中。

Deferred Deeplink的應用場景:
  • 追蹤廣告效果
  • 追蹤用戶推薦/邀請鏈接
  • 在 app 內保持網頁瀏覽的上下文,如登錄信息,購物車等

App分享邀請好友,好友通過鏈接(只有通過此鏈接跳轉到App Store下載App才算有效)安裝App之后雙方獲得獎勵,省去了過去注冊輸入邀請碼這一步。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容