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之后双方获得奖励,省去了过去注册输入邀请码这一步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容