ios md5加密

ios md5加密
#import <CommonCrypto/CommonDigest.h>

+ (NSString *)md5:(NSString *)string {
const char *cStr = [string UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[result appendFormat:@”%02X”, digest[i]];
}
return result;
}

iOS 正则匹配常用方法

验证手机号
// 验证手机号
+ (BOOL)isValidatePhone:(NSString *)phone{
NSString *phoneRegex = @”^1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}$”;
NSPredicate *phoneTest = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, phoneRegex];
return [phoneTest evaluateWithObject:phone];
}

邮箱账号有效性判断
// 邮箱账号的有效性判断
+ (BOOL)isValidateEmail:(NSString *)email{
NSString * emailRegex = @”[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}”;
NSPredicate * emailTest = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, emailRegex];
return [emailTest evaluateWithObject:email];
}

匹配密码格式(长度6~20位,只能是数字、大小写字母)
// 匹配密码格式
+ (BOOL)isValidatePassword:(NSString *)password{
NSString * passwordRegex = @”[a-zA-Z0-9]{6,20}”;
NSPredicate * passwordTest = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, passwordRegex];
return [passwordTest evaluateWithObject:password];
}

车牌号码判断
// 车牌号码正则表达式
+ (BOOL)isValidateCarID:(NSString *)carID{
if (carID.length==7) {
//普通汽车,7位字符,不包含I和O,避免与数字1和0混淆
NSString *carRegex = @”^[\u4e00-\u9fa5]{1}[a-hj-np-zA-HJ-NP-Z]{1}[a-hj-np-zA-HJ-NP-Z0-9]{4}[a-hj-np-zA-HJ-NP-Z0-9\u4e00-\u9fa5]$”;
NSPredicate *carTest = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, carRegex];
return [carTest evaluateWithObject:carID];
}else if(carID.length==8){
//新能源车,8位字符,*位:省份简称(1位汉字),第二位:发牌机关代号(1位字母);
//小型车,第三位:只能用字母D或字母F,第四位:字母或者数字,后四位:必须使用数字;([DF][A-HJ-NP-Z0-9][0-9]{4})
//大型车3-7位:必须使用数字,后一位:只能用字母D或字母F。([0-9]{5}[DF])
NSString *carRegex = @”^[\u4e00-\u9fa5]{1}[a-hj-np-zA-HJ-NP-Z]{1}([0-9]{5}[d|f|D|F]|[d|f|D|F][a-hj-np-zA-HJ-NP-Z0-9][0-9]{4})$”;
NSPredicate *carTest = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, carRegex];
return [carTest evaluateWithObject:carID];
}
return NO;
}

身份证号判断
// 身份证号段正则表达式
+ (BOOL)isValidateIDCard:(NSString *)identityString{
if (identityString.length != 18) return NO;
// 正则表达式判断基本 身份证号是否满足格式
NSString *regex2 = @”^(^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$)|(^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])((\\d{4})|\\d{3}[Xx])$)$”;
NSPredicate *identityStringPredicate = [NSPredicate predicateWithFormat:@”SELF MATCHES %@”, regex2];
//如果通过该验证,说明身份证格式正确,但准确性还需计算
if(![identityStringPredicate evaluateWithObject:identityString]) return NO;
//** 开始进行校验 *//
//将前17位加权因子保存在数组里
NSArray *idCardWiArray = @[@”7″, @”9″, @”10″, @”5″, @”8″, @”4″, @”2″, @”1″, @”6″, @”3″, @”7″, @”9″, @”10″, @”5″, @”8″, @”4″, @”2″];
//这是除以11后,可能产生的11位余数、验证码,也保存成数组
NSArray *idCardYArray = @[@”1″, @”0″, @”10″, @”9″, @”8″, @”7″, @”6″, @”5″, @”4″, @”3″, @”2″];
//用来保存前17位各自乖以加权因子后的总和
NSInteger idCardWiSum = 0;
for(int i = 0;i < 17;i++) {
NSInteger subStrIndex = [[identityString substringWithRange:NSMakeRange(i, 1)] integerValue];
NSInteger idCardWiIndex = [[idCardWiArray objectAtIndex:i] integerValue];
idCardWiSum += subStrIndex * idCardWiIndex;
}
//计算出校验码所在数组的位置
NSInteger idCardMod=idCardWiSum%11;
//得到*后一位身份证号码
NSString *idCardLast= [identityString substringWithRange:NSMakeRange(17, 1)];
//如果等于2,则说明校验码是10,身份证号码*后一位应该是X
if(idCardMod==2) {
if(![idCardLast isEqualToString:@”X”]||[idCardLast isEqualToString:@”x”]) {
return NO;
}
}else{
//用计算出的验证码与*后一位身份证号码匹配,如果一致,说明通过,否则是无效的身份证号码
if(![idCardLast isEqualToString: [idCardYArray objectAtIndex:idCardMod]]) {
return NO;
}
}
return YES;
}

随机获取八位字符
– (NSString *)obtain8RandomCode {
NSArray *changeArray = [[NSArray alloc] initWithObjects:@”0″,@”1″,@”2″,@”3″,@”4″,@”5″,@”6″,@”7″,@”8″,@”9″,@”A”,@”B”,@”C”,@”D”,@”E”,@”F”,@”G”,@”H”,@”I”,@”J”,@”K”,@”L”,@”M”,@”N”,@”O”,@”P”,@”Q”,@”R”,@”S”,@”T”,@”U”,@”V”,@”W”,@”X”,@”Y”,@”Z”,@”a”,@”b”,@”c”,@”d”,@”e”,@”f”,@”g”,@”h”,@”i”,@”j”,@”k”,@”l”,@”m”,@”n”,@”o”,@”p”,@”q”,@”r”,@”s”,@”t”,@”u”,@”v”,@”w”,@”x”,@”y”,@”z”,@”!”,@”@”,@”#”,@”$”,@”^”,@”&”,@”*”,@”-“,@”+”,nil];
NSArray *specailArray = [[NSArray alloc] initWithObjects:@”!”,@”@”,@”#”,@”$”,@”^”,@”&”,@”*”,@”-“,@”+”, nil];
NSMutableString *changeString = [[NSMutableString alloc] initWithCapacity:8];

NSInteger specialIndex = arc4random()%7;
NSInteger specialArrayIndex = arc4random()%([specailArray count] – 1);
for(int i = 0; i < 8; i++){
if (i==specialIndex) {
changeString = (NSMutableString *)[changeString stringByAppendingString:[specailArray objectAtIndex:specialArrayIndex]];
continue;
}
NSInteger index = arc4random()%([changeArray count] – 1);
changeString = (NSMutableString *)[changeString stringByAppendingString:[changeArray objectAtIndex:index]];
}
return changeString;
}

iOS IAP支付常见问题汇总与解决

1. 获取不到商品信息的原因

沙盒的测试账号和你请求商品信息没有关系
iTunes Connect里面对应账号的协议、税务和银行业务信息有没有填完整,填好的应该是这个样子这个很容易疏忽,务必检查

%title插图%num

%title插图%num
确认证书是否添加IAP支付功能默认创建的证书是包含该项的
确定是真机测试且手机没有越狱大部分越狱手机也可以测试,深度越狱破坏系统的可能无法调起支付
确定内购商品添加到了需要内购功能的App中
确定当前运行的App的Bundle ID和后台配置的App的Bundle ID是一致的
可以尝试先删除旧App,再重新编译生成新的,避免新App未覆盖错误
如果上线后发现线上包请求不到商品信息,一般发生于首次提交App或添加新商品,可能是苹果缓存的bug,当你的App通过审核以后,你发现在生产环境下获取不到商品,这是因为App虽然过审核了,但是内购商品还没有正式添加到苹果的服务器里,耐心等待一段时间就可以啦,或者去苹果后台刷新配置商品信息列表,然后等待一天左右时间大概就可以了
2. 如果请求到了商品信息,也发送了购买请求,但是监听购买结果的方法就是不执行

可以检查一下,是否在工具类初始化的时候,添加了监听,添加监听代码如下
注:支付工具类一般用单例模式,避免创建多个对象或者对象提前释放,导致苹果回调不会调用支付失败,或者使用self全局化支付工具类对象,不可使支付工具类对象局部变量化
#pragma mark – 单例方法
static IAPPayManager* instance = nil;
+ (instancetype)sharedInstance{
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[IAPPayManager alloc] init];
});
return instance;
}

#pragma mark – 重载初始化方法,注册用于处理支付回调的Observer
– (instancetype)init{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

3. IAP审核环境

苹果在审核App时,只会在sandbox环境购买,其产生的购买凭证,也只能连接苹果的测试验证服务器,审核时后台要保证沙盒测试环境开放,以免服务器无法验证通过IAP购买,造成App审核被拒
TestFlight测试时也是走的sandbox环境购买
4. 只要不是红色的状态都是可以进行支付测试的,元数据丢失是因为,在增加内购项目的时候,没有填写完全,产品ID是唯一的,假如你删除了一个内购项目,那么这个产品ID就不能用了,所以填写要慎重

%title插图%num

5. 沙盒测试账号相关
用沙盒账号测试支付的包,只能是adhoc签名证书或者develop签名证书打的包,不能是从AppStore或者TestFlight上下载的,还没上线之前App并没有地区之分,沙盒账号随便哪个地区都可以用来测试,弹出的购买提示框会根据当前沙盒账号AppleID的地区显示语言的

注册沙盒测试账号时,提示报错Unknown Errors while creating Sandbox Tester, Please check Error Log, email=a***st@qq.com
解决方案:把你的密码设置的复杂点,比如包含数字、字母混大小写等
6. 支付时提示您已购买此App内购买项目。此项目将免费恢复问题

%title插图%num

此提示说明iTunes订单被卡住,属于苹果ID支付问题,暂时可先选择其他额度进行支付,也可联系苹果的客服人员删除你异常的订单,打开浏览器进入Apple官方支持

7. 验证服务器地址和需要的参数说明

Key Value 是否必须
receipt-data The base64 encoded receipt data 是
password Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string) 否,仅用于自动续订,获取方法见共享密钥附录
exclude-old-transactions Only used for iOS7 style app receipts that contain auto-renewable or non-renewing subscriptions. If value is true, response includes only the latest renewal transaction for any subscriptions 否,仅用于自动续订或非续订订阅的iOS 7样式的应用收据
在测试服务器中,发送receipt到苹果的测试服务器https://sandbox.itunes.apple.com/verifyReceipt验证
在正式服务器中已上线Appstore,发送receipt到苹果的正式服务器https://buy.itunes.apple.com/verifyReceipt验证
当我们把应用提交给苹果审核时,苹果也是在sandbox环境购买,其产生的购买凭证,也只能连接苹果的测试验证服务器,所以我们可以先发到苹果的正式服务器验证,如果苹果返回21007,则再一次连接测试服务器进行验证
8. 苹果返回状态码

Status 描述
0 App Store 验证成功
21000 App Store不能读取你提供的JSON对象
21002 receipt-data属性中的数据格式错误或丢失
21003 receipt无法通过验证
21004 提供的共享密码与帐户的文件共享密码不匹配
21005 receipt服务器当前不可用
21006 该收据有效,但订阅已过期,当此状态代码返回到您的服务器时,收据数据也会被解码并作为响应的一部分返回,仅针对自动续订的iOS 6样式交易收据返回
21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
21010 此收据无法授权,就像从未进行过购买一样对待
关于苹果服务器验证返回21004的问题说明
在购买类型是自动续订时,服务端做验证就要传入这个共享密钥,传入字段为password,共享密钥获取见附录,如果你们的商品不是自动续订,建议不要传入该字段,否则传入内容不正确可能会导致苹果返回21004

%title插图%num
9. 国内连接苹果服务器的稳定性
开发之初,苹果方就很负责的告知:我们的服务器不稳定。真正开发之后,发现苹果方果然是很负责的,不仅是不稳定,而且足够慢。app store server验证一个收据需要3-6s时间

10. 经验总结,如下内容已经过验证

程序加入支付队列使用SKMutablePayment和SKPayment的区别
两者拥有的属性一样,唯一区别是属性读写权限不同,SKMutablePayment属性具有读写权限,SKPayment属性只读,如果你要使用applicationUsername透传字段,那么就一定要使用SKMutablePayment加入支付队列

透传字段applicationUsername可能返回的是nil
在支付完成后,每笔订单都不调用finishTransaction,如此测试四五笔订单后,重新启动该应用,苹果自动补单会进行,在有些时候该字段就会为空,需要开发者注意

updatedTransactions:在App整个生命周期只会走一次,所以只要不把订单finishTransaction掉,重启App就会重新走苹果的补单流程(自动调用updatedTransactions:注意需要[[SKPaymentQueue defaultQueue] addTransactionObserver:instance];添加观察者才可以),逻辑需要自己根据项目实现

SKPaymentTransaction *transaction属性官方说明

transaction.transactionDate
将订单交易添加到服务器队列的日期,仅当状态为SKPaymentTransactionStatePurchased或SKPaymentTransactionStateRestored时有效

transaction.transactionIdentifier
transactionIdentifier是唯一标识交易支付成功的字符串,此值的格式与收据中的事务transaction_id相同,但是值可能不相同,仅当状态为SKPaymentTransactionStatePurchased或SKPaymentTransactionStateRestored时有效

transaction.originalTransaction
原始交易id,仅当状态为SKPaymentTransactionStateRestored时有效有值

transaction.payment.applicationUsername
获取之前设置的applicationUsername

注意:凭证验证后返回的original_transaction_id和transaction_id一般情况下是相同的,只会在恢复购买时不一样

transactionReceiptData可以无限验证通过,也就是说一个凭证可以被校验多次,这是刷单方法之一,需要开发者注意,苹果补单流程返回的transactionReceiptData即使同一笔订单也会变

transactionReceiptData验证解析后,in_app字段出现为空或者多个购买项目,只要不finishTransaction掉订单,下次再支付成功后,返回的transactionReceiptData凭证,就是包含之前的购买记录,*近购买的商品会在列表的*个

验证凭证,苹果服务器返回的数据

{
“receipt”: {
“receipt_type”: “ProductionSandbox”,
“adam_id”: 0,
“app_item_id”: 0,
“bundle_id”: “com.Yo***ights”,
“application_version”: “1”,
“download_id”: 0,
“version_external_identifier”: 0,
“receipt_creation_date”: “2020-06-01 09:37:57 Etc/GMT”,
“receipt_creation_date_ms”: “1591004277000”,
“receipt_creation_date_pst”: “2020-06-01 02:37:57 America/Los_Angeles”,
“request_date”: “2020-06-01 09:38:55 Etc/GMT”,
“request_date_ms”: “1591004335844”,
“request_date_pst”: “2020-06-01 02:38:55 America/Los_Angeles”,
“original_purchase_date”: “2013-08-01 07:00:00 Etc/GMT”,
“original_purchase_date_ms”: “1375340400000”,
“original_purchase_date_pst”: “2013-08-01 00:00:00 America/Los_Angeles”,
“original_application_version”: “1.0”,
“in_app”: [
{
“quantity”: “1”,
“product_id”: “com.yo***thlycard”,
“transaction_id”: “10***4780”,
“original_transaction_id”: “10***4780”,
“purchase_date”: “2020-06-01 09:36:56 Etc/GMT”,
“purchase_date_ms”: “1591004216000”,
“purchase_date_pst”: “2020-06-01 02:36:56 America/Los_Angeles”,
“original_purchase_date”: “2020-06-01 09:36:56 Etc/GMT”,
“original_purchase_date_ms”: “1591004216000”,
“original_purchase_date_pst”: “2020-06-01 02:36:56 America/Los_Angeles”,
“is_trial_period”: “false”
},
{
“quantity”: “1”,
“product_id”: “com.yo***iteprime1”,
“transaction_id”: “10***3950”,
“original_transaction_id”: “10***3950”,
“purchase_date”: “2020-06-01 09:35:30 Etc/GMT”,
“purchase_date_ms”: “1591004130000”,
“purchase_date_pst”: “2020-06-01 02:35:30 America/Los_Angeles”,
“original_purchase_date”: “2020-06-01 09:35:30 Etc/GMT”,
“original_purchase_date_ms”: “1591004130000”,
“original_purchase_date_pst”: “2020-06-01 02:35:30 America/Los_Angeles”,
“is_trial_period”: “false”
}
]
},
“status”: 0,
“environment”: “Sandbox”
}

Mac OS + Mac PE + Win PE 三合一 U盘制作教程

开始之前需要准备一下工具:

移动硬盘或者U盘一个
Mac OS原版安装文件
Mac PE
Win PE
DiskGenius分区工具
Win PE制作
下载好U盘魔术师V5全能版或者通用PE工具箱等Win PE制作软件,安装到电脑打开,然后插入U盘;一般保持默认设置就行,Win PE制作完成。

Mac OS分区制作
打开DiskGenius分区工具,找到刚刚制作好的U盘,然后选中这个U盘分区,右击菜单选中调整分区大小,如图:

%title插图%num

打开调整窗口,把鼠标放在分区的右边,出现拖拉箭头,然后往左拉,或者在下方直接填写你要分的空间大小,如图:

%title插图%num

Mac OS分区的空间一般8.5G,也可以设置更大,看个人。然后点击开始,弹出对话框,选中是;制作完成之后点击完成,就会出现空闲8.5G,

 

右击空闲的分区,选择建立新分区,选中NTFS格式,4K对齐,

 

选择确定,然后点击左上角的保存更改,提示你格式化分区,选择是,格式化完成之后,8.5G的Mac OS分区就制作好了。

制作Mac PE分区,分9G以上;方法跟制作Mac OS分区是一样的,这里不再重复,

分区基本做好了,现在转到苹果系统去写入系统文件。

格式化两个Mac分区
打开苹果电脑的磁盘工具,找到刚刚分出来的9G容量的分区,然后选择抹掉,名称为Mac PE,只为做区分用,可以随意命名;格式为Mac OS扩展 日志式;然后选择8.5G的Mac OS安装分区,步骤和上面一样,

Mac PE系统文件写入,打开下载好的iFen.OS X PE,解压的过程选择跳过,不跳过也行,目的是为了加载进磁盘工具里,回到磁盘工具的界面,会出现解压出来的镜像,然后选中Mac PE分区,选择菜单的恢复按钮,恢复来源选择刚刚解压出来的PE镜像,点击恢复

 

注意:iFen.OS X PE是基于Mac OS 10.14制作的,所以要用Mac OS 10.14的电脑才能恢复,否则会恢复失败,恢复过程的快慢就要看U盘的速度了,大概5分钟的时间。

Mac OS系统文件写入,以Mac OS 10.14系统为例,不同的系统制作代码不同,代码中的MyVolume为上面命名的U盘名称,下载好Mac OS 10.14.1系统,并把它放在Mac的应用程序里备用,打开终端,在终端输入代码:sudo /Applications/Install\ macOS\ Mojave.app/Contents/Resources/createinstallmedia –volume /Volumes/MyVolume
等待制作完成,大概5分钟,完成之后;

iOS 唤起APP之Universal Link(通用链接)

iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是从iOS 9起可以使用Universal Links技术进行跳转页面,这是一种体验更加完美的解决方案

什么是Universal Link(通用链接)
Universal Link是Apple在iOS 9推出的一种能够方便的通过传统HTTPS链接来启动APP的功能。如果你的应用支持Universal Link,当用户点击一个链接时可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接

支持Universal Link(通用链接)
先决条件:必须有一个支持HTTPS的域名,并且拥有该域名下上传到根目录的权限(为了上传Apple指定文件)

集成步骤

开发者中心配置
找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了

%title插图%num
工程配置
targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀,如:applinks:domain

%title插图%num
配置指定文件
创建一个内容为json格式的文件,苹果将会在合适的时候,从我们在项目中填入的域名请求这个文件。这个文件名必须为apple-app-site-association,切记没有后缀名,文件内容大概是这样子:

{
“applinks”: {
“apps”: [],
“details”: [
{
“appID”: “9JA89QQLNQ.com.apple.wwdc”,
“paths”: [ “/wwdc/news/”, “/videos/wwdc/2015/*”]
},
{
“appID”: “ABCD1234.com.apple.wwdc”,
“paths”: [ “*” ]
}
]
}
}

appID:组成方式是TeamID.BundleID。如上面的9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID
paths:设定你的app支持的路径列表,只有这些指定路径的链接,才能被app所处理。*的写法代表了可识别域名下所有链接

上传该文件
上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件

代码中的相关支持
当点击某个链接,可以直接进我们的app,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容,我们需要在工程里实现AppDelegate对应的方法:

– (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
// NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]){
NSURL *webpageURL = userActivity.webpageURL;
NSString *host = webpageURL.host;
if ([host isEqualToString:@”api.r2games.com.cn”]){
//进行我们的处理
NSLog(@”TODO….”);
}else{
NSLog(@”openurl”);
[[UIApplication sharedApplication] openURL:webpageURL options:nil completionHandler:nil];
// [[UIApplication sharedApplication] openURL:webpageURL];
}
}
return YES;
}

苹果为了方便开发者,提供了一个网页验证我们编写的这个apple-app-site-association是否合法有效

Universal Link(通用链接)注意点
Universal Link跨域
Universal Link有跨域问题,Universal Link必须要求跨域,如果不跨域,就不会跳转(iOS 9.2之后的改动)
假如当前网页的域名是A,当前网页发起跳转的域名是B,必须要求B和A是不同域名才会触发Universal Link,如果B和A是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
Universal Link请求apple-app-site-association时机
当我们的App在设备上*次运行时,如果支持Associated Domains功能,那么iOS会自动去GET定义的Domain下的apple-app-site-association文件

iOS会先请求https://domain.com/.well-known/apple-app-site-association,如果此文件请求不到,再去请求https://domain.com/apple-app-site-association,所以如果想要避免服务器接收过多GET请求,可以直接把apple-app-site-association放在./well-known目录下

服务器上apple-app-site-association的更新不会让iOS本地的apple-app-site-association同步更新,即iOS只会在App*次启动时请求一次,以后除非App更新或重新安装,否则不会在每次打开时请求apple-app-site-association

Universal Link的好处

之前的Custom URL scheme是自定义的协议,因此在没有安装该app的情况下是无法直接打开的。而Universal Links本身就是一个能够指向web页面或者app内容页的标准web link,因此能够很好的兼容其他情况
Universal links是从服务器上查询是哪个app需要被打开,因此不存在Custom URL scheme那样名字被抢占、冲突的情况
Universal links支持从其他app中的UIWebView中跳转到目标app
提供Universal link给别的app进行app间的交流时,对方并不能够用这个方法去检测你的app是否被安装(之前的custom scheme URL的canOpenURL方法可以)

iOS 13-Sign In with Apple

*近了解了iOS 13新增功能之Sign In with Apple,Sign In with Apple是跨平台的,可以支持iOS、macOS、watchOS、tvOS、JS。本文主要内容为Sign In with Apple在iOS上的基础使用。详情参考WWDC 2019

审核备注
New Guidelines for Sign in with Apple
We’ve updated the App Store Review Guidelines to provide criteria for when apps are required to use Sign in with Apple. Starting today, new apps submitted to the App Store must follow these guidelines. Existing apps and app updates must follow them by April 2020. We’ve also provided new guidelines for using Sign in with Apple on the web and other platforms.
September 12, 2019
也就是说,所有已接入其它第三方登录的 App,Sign In with Apple 将被要求作为一种登录选择,否则就不给过。从今天开始(2019-9-12),提交到App Store的新应用必须遵循这些准则,现有应用程序和应用程序更新必须在2020年4月之前进行。详情参考App Store审核指南

%title插图%num

开发Sign In with Apple的注意事项
需要在苹果后台打开该选项,并且重新生成Profiles配置文件,并安装到Xcode,如下图

%title插图%num
服务端验证需要的文件,一个是私钥文件,一个是config.json文件
创建用于客户端身份验证的私钥
返回Certificates, Identifiers & Profiles主屏幕,从侧面导航中选择Keys

%title插图%num
单击Configure按钮,然后选择你先前创建的Primary App ID,保存之后,Apple将为你生成一个新的私钥,并让你仅下载一次,请确保你保存了此文件,因为以后你将无法再次将其取回!你下载的文件将以.p8结尾,可以将其重命名为key.txt以便在后续步骤中更轻松地使用

创建config.json新文件,格式、内容和参数说明如下
{
“client_id”: “实际上被称为“Service ID”,您将在“Identifiers”部分创建它,其实就是应用的bundleID”,
“team_id”: “后台账号的teamID”,
“redirect_uri”: “重定向url,网页登录需要,只是客服端登录可以不写”,
“key_id”: “在苹果后台获取,如下图”,
“scope”: “设置我们要从用户那里收集什么信息,我们可以设置email和name,或者也可以不写
}
%title插图%num

web使用Sign In with Apple的相关配置,不需要web登录的,以下配置可以忽略
创建Services ID

%title插图%num
在下一步中,你将定义用户在登录流程中将看到的应用程序的名称,并定义成为OAuth的标识符client_id,确保还选中Sign In with Apple复选框

%title插图%num

创建web Authentication Configuration,定义应用程序的重定向URL

%title插图%num

%title插图%num
iOS使用Sign In with Apple在Xcode的准备工作
在Xcode11 Signing & Capabilities中添加Sign In With Apple,如下图

%title插图%num
iOS Sign In with Apple流程
导入系统头文件#import <AuthenticationServices/AuthenticationServices.h>,添加Sign In with Apple登录按钮,设置ASAuthorizationAppleIDButton相关布局,并添加按钮点击响应事件
获取授权码
验证
导入系统头文件#import <AuthenticationServices/AuthenticationServices.h>,添加Sign In with Apple登录按钮,设置ASAuthorizationAppleIDButton相关布局,并添加按钮点击响应事件。当然苹果也允许自定义苹果登录按钮的样式,样式要求详见这个文档:Human Interface Guidelines
– (void)configUI{
// 使用系统提供的按钮,要注意不支持系统版本的处理
if (@available(iOS 13.0, *)) {
// Sign In With Apple Button
ASAuthorizationAppleIDButton *appleIDBtn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeDefault style:ASAuthorizationAppleIDButtonStyleWhite];
appleIDBtn.frame = CGRectMake(30, self.view.bounds.size.height – 180, self.view.bounds.size.width – 60, 100);
// appleBtn.cornerRadius = 22.f;
[appleIDBtn addTarget:self action:@selector(didAppleIDBtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:appleIDBtn];
}

// 或者自己用UIButton实现按钮样式
UIButton *addBtn = [UIButton buttonWithType:UIButtonTypeCustom];
addBtn.frame = CGRectMake(30, 80, self.view.bounds.size.width – 60, 44);
addBtn.backgroundColor = [UIColor orangeColor];
[addBtn setTitle:@”Sign in with Apple” forState:UIControlStateNormal];
[addBtn addTarget:self action:@selector(didCustomBtnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:addBtn];
}

// 自己用UIButton按钮调用处理授权的方法
– (void)didCustomBtnClicked{
// 封装Sign In with Apple 登录工具类,使用这个类时要把类对象设置为全局变量,或者直接把这个工具类做成单例,如果使用局部变量,和IAP支付工具类一样,会导致苹果回调不会执行
self.signInApple = [[SignInApple alloc] init];
[self.signInApple handleAuthorizationAppleIDButtonPress];
}

// 使用系统提供的按钮调用处理授权的方法
– (void)didAppleIDBtnClicked{
// 封装Sign In with Apple 登录工具类,使用这个类时要把类对象设置为全局变量,或者直接把这个工具类做成单例,如果使用局部变量,和IAP支付工具类一样,会导致苹果回调不会执行
self.signInApple = [[SignInApple alloc] init];
[self.signInApple handleAuthorizationAppleIDButtonPress];
}

// 处理授权
– (void)handleAuthorizationAppleIDButtonPress{
NSLog(@””);

if (@available(iOS 13.0, *)) {
// 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
// 创建新的AppleID 授权请求
ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
// 在用户授权期间请求的联系信息
appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self;
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self;
// 在控制器初始化期间启动授权流
[authorizationController performRequests];
}else{
// 处理不支持系统版本
NSLog(@”该系统版本不可用Apple登录”);
}
}

注意:封装Sign In with Apple 登录工具类,使用这个类时要把类对象设置为全局变量,或者直接把这个工具类做成单例,如果使用局部变量,和IAP支付工具类一样,会导致苹果回调不会执行
已经使用Sign In with Apple登录过app的用户
如果设备中存在iCloud Keychain凭证或者AppleID凭证,提示用户直接使用TouchID或FaceID登录即可,代码如下
// 如果存在iCloud Keychain 凭证或者AppleID 凭证提示用户
– (void)perfomExistingAccountSetupFlows{
NSLog(@”///已经认证过了/”);

if (@available(iOS 13.0, *)) {
// 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
// 授权请求AppleID
ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
// 为了执行钥匙串凭证分享生成请求的一种机制
ASAuthorizationPasswordProvider *passwordProvider = [[ASAuthorizationPasswordProvider alloc] init];
ASAuthorizationPasswordRequest *passwordRequest = [passwordProvider createRequest];
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest, passwordRequest]];
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self;
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self;
// 在控制器初始化期间启动授权流
[authorizationController performRequests];
}else{
// 处理不支持系统版本
NSLog(@”该系统版本不可用Apple登录”);
}
}

获取授权码
获取授权码需要在代码中实现两个代理回调ASAuthorizationControllerDelegate、ASAuthorizationControllerPresentationContextProviding分别用于处理授权登录成功和失败、以及提供用于展示授权页面的Window,代码如下
#pragma mark – delegate
//@optional 授权成功地回调
– (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){
NSLog(@”授权完成:::%@”, authorization.credential);
NSLog(@”%s”, __FUNCTION__);
NSLog(@”%@”, controller);
NSLog(@”%@”, authorization);

if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
// 用户登录使用ASAuthorizationAppleIDCredential
ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
NSString *user = appleIDCredential.user;
// 使用过授权的,可能获取不到以下三个参数
NSString *familyName = appleIDCredential.fullName.familyName;
NSString *givenName = appleIDCredential.fullName.givenName;
NSString *email = appleIDCredential.email;

NSData *identityToken = appleIDCredential.identityToken;
NSData *authorizationCode = appleIDCredential.authorizationCode;

// 服务器验证需要使用的参数
NSString *identityTokenStr = [[NSString alloc] initWithData:identityToken encoding:NSUTF8StringEncoding];
NSString *authorizationCodeStr = [[NSString alloc] initWithData:authorizationCode encoding:NSUTF8StringEncoding];
NSLog(@”%@\n\n%@”, identityTokenStr, authorizationCodeStr);

// Create an account in your system.
// For the purpose of this demo app, store the userIdentifier in the keychain.
// 需要使用钥匙串的方式保存用户的唯一信息
// [YostarKeychain save:KEYCHAIN_IDENTIFIER(@”userIdentifier”) data:user];

}else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]){
// 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略
// Sign in using an existing iCloud Keychain credential.
// 用户登录使用现有的密码凭证
ASPasswordCredential *passwordCredential = authorization.credential;
// 密码凭证对象的用户标识 用户的唯一标识
NSString *user = passwordCredential.user;
// 密码凭证对象的密码
NSString *password = passwordCredential.password;

}else{
NSLog(@”授权信息均不符”);

}
}

// 授权失败的回调
– (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){
// Handle error.
NSLog(@”Handle error:%@”, error);
NSString *errorMsg = nil;
switch (error.code) {
case ASAuthorizationErrorCanceled:
errorMsg = @”用户取消了授权请求”;
break;
case ASAuthorizationErrorFailed:
errorMsg = @”授权请求失败”;
break;
case ASAuthorizationErrorInvalidResponse:
errorMsg = @”授权请求响应无效”;
break;
case ASAuthorizationErrorNotHandled:
errorMsg = @”未能处理授权请求”;
break;
case ASAuthorizationErrorUnknown:
errorMsg = @”授权请求失败未知原因”;
break;

default:
break;
}

NSLog(@”%@”, errorMsg);
}

// 告诉代理应该在哪个window 展示内容给用户
– (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){
NSLog(@”88888888888″);
// 返回window
return [UIApplication sharedApplication].windows.lastObject;
}

在授权登录成功回调中,我们可以拿到以下几类数据

UserID:Unique, stable, team-scoped user ID,苹果用户唯一标识符,该值在同一个开发者账号下的所有App下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来(这与国内的微信、QQ、微博等第三方登录流程基本一致)
Verification data:Identity token, code,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证,本次授权登录请求数据的有效性和真实性,详见Sign In with Apple REST API
Account information:Name, verified email,苹果用户信息,包括全名、邮箱等,注意:如果玩家登录时拒*提供真实的邮箱账号,苹果会生成虚拟的邮箱账号,而且记录过的苹果账号再次登录这些参数拿不到
验证
关于验证的这一步,需要传递授权码给自己的服务端,自己的服务端调用苹果API去校验授权码Generate and validate tokens。如果验证成功,可以根据userIdentifier判断账号是否已存在,若存在,则返回自己账号系统的登录态,若不存在,则创建一个新的账号,并返回对应的登录状态给App
推荐验证步骤为:
服务端拿authorizationCode去苹果后台验证,验证地址https://appleid.apple.com/auth/token,苹果返回id_token,与客户端获取的identityToken值一样,格式如下
{
“access_token”: “一个token”,
“token_type”: “Bearer”,
“expires_in”: 3600,
“refresh_token”: “一个token”,
“id_token”: “结果是JWT,字符串形式,identityToken”
}

另外授权code是有时效性的,且使用一次即失效

服务器拿到相应结果后,其中id_token是JWT数据,解码id_token,得到如下内容
{
“iss”:”https://appleid.apple.com”,
“aud”:”这个是你的app的bundle identifier”,
“exp”:1567482337,
“iat”:1567481737,
“sub”:”这个字段和客户端获取的user字段是完全一样的”,
“c_hash”:”8KDzfalU5kygg5zxXiX7dA”,
“auth_time”:1567481737
}

其中aud与你app的bundleID一致,sub就是授权用户的唯一标识,与手机端获得的user一致,服务器端通过对比sub字段信息是否与手机端上传的user信息一致来确定是否成功登录
该token的有效期是10分钟,具体后端验证参考附录

Airtest iOS测试环境部署

简介

这个Airtest IDE是通过iOS-Tagent来操作iPhone的,你可以在Airtest IDE里录制脚本来实现自动化操作iPhone

前提

1. 得有个iOS开发者账号,然后才能在developer.apple.com的account下看到管理自己的证书入口,否则看不到

2. 将要下载的iOS-Tagent需要在xcode里启动,所以要保证iPhone连接的mac电脑上安装了xcode

3. 下载iOS-Tagent:https://github.com/AirtestProject/IOS-Tagent,这个readme上的步骤下文会用到

4. 下载Airtest IDE:http://airtest.netease.com/,看下官方中文文档,其中2.4章节关于iOS设备连接的方法下文会用到

部署步骤

1. 按github上的reademe启动iOS-Tagent,这样iPhone就和iOS-Tagent连接上了

2. 启动Airtest IDE并connect device(注意填入的ip和端口号要和xcode中iOS-Tagent server的ip地址和端口号一致)

遇到的问题

1. 如果遇到了让你输入登录钥匙密码的问题,记得点击始终允许,否则就会一直让你重复的输密码

2. 如果出现证书校验出错了相关的问题,在developer.apple.com的account下进入证书管理页,把之前的证书删掉,然后在xcode里就会重新生成一个

3. 涉及到删证书的操作,除了在开发者网站上删除证书,还要在本地删除provision文件:

cd ~/Library/MobileDevice/Provisioning\ Profiles/
rm *.mobileprovision

4. 真机安装好WebDriverAgent app后可能需要在手机上信任下安装的app

5. 在xcode下将iOS-Tagent test起来之后再在AirtestIDE里录脚本,看xcode日志输出在底部栏*左边按钮-Show the Debug Area

6. 在xcode的Debug Area中搜索ServerUrlHere关键词可以看到iOS-Tagent server的ip地址和端口号

airtest连接IOS设备过程记录

话不多说,直接步入正题,鉴于可能有从零开始的同学,所以这里的步骤可能写的繁琐了点~

步骤一(前期准备)
首先你得有个IOS手机,MAC电脑,Airtest IDE,以及基础的一些环境
Airtest官方的基本要求

版本需求:
Mac Xcode ≥ 9.3
iPhone iOS ≥ 9.3
在 9.3≤iOS≤10时,由于基于旧版xcode SDK,建议使用Xcode 版本≤10.1来启动iOS-Tagent,否则会出现手机截屏不全的问题。
Airtest是基于Python的,支持版本2.7或≤3.6,官方推荐使用3

Airtest下载地址
Xcode下载地址 这里给出的是官方的历史版本下载地址,从app store下载*新版本的xcode会存在一些问题
IOS-Tagent下载地址 使用git clone或者直接下载zip解压都可以
iproxy工具,做端口转发需要,具体安装方式使用brew install libimobiledevice即可

步骤二
使用Xcode打开步骤一中的IOS-Tagent项目,然后需要一些具体设置,详细的设置步骤可以参考我的上过一篇文章,上文中的步骤三开始到*后都与此处操作一致

关于Xcode版本
*终我使用成功的版本为Xcode10连接,IOS12.3以及IOS12.3.1设备
官方的IOS-Tagent说明的测试过版本为

IOS XCode
≥13 11.x
10.x~12.x 10.x
≥9.3 ≥9.3
关于高版本Xcode的操作经历
一开始不服,从app store直接下载了*新的Xcode11.5,使用IOS-Tagent,项目连启动都失败,执行Product -> Test过程中遇到了其他问题
仍然不服,于是想试试facebook的WebDriverAgent,毕竟IOS-Tagent是基于这个改造的,于是也就有了上篇文章WebDriverAgent踩坑-Xcode11.5
稍微不服,考虑到IOS-Tagent中在*次尝试时报错的位置是WebDriverAgentLib中的问题,而我在第二次尝试也就是上篇文章中成功通过WebDriverAgent连接了IOS设备,于是我就将IOS-Tagent中相关内容替换成了2中也就是上一篇文章中运行成功后的产物,也就是这部分文件

%title插图%num

替换之后项目果然能够成功运行(向前前进了一大步),但是还是无法连接到Airtest IDE,也没有任何明确的报错信息
不服不行了。。去步骤一中下载了Xcode10,然后继续搞起
Xcode10使用过程中遇到的问题
本机已经有了一个Xcode了,如何安装第二个
首先到步骤一中提供的网址下载对应的xip安装包,下载好后双击就会自动验证+解压(保证磁盘空间充足才行,否则自动验证步骤失败)
然后,将解压出来的Xcode.app重命名为Xcode10.app或其他名字,不冲突即可,拖入Application中
如何让Xcode10支持高版本的IOS设备
官方提供的经测试范围是Xcode 10.x可以操作IOS10.x~12.x之间的版本,我下周的版本是Xcode10,没有小版本,*高只能支持IOS12的操作,但是我的IOS设备室

12.3和12.3.1的,所以需要额外的操作

这就需要用到之前下载的高版本Xcode11.5了,
进入之前的应用包中如下位置:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport,可以看到很多版本号的数字,代表IOS版本,把你需要的包整个文件夹拷贝到对应的你需要使用的Xcode中,例如我需要拷贝到/Applications/Xcode10.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/中
重新启动Xcode10

步骤三
项目启动成功后使用iproxy做端口转发:iproxy 8100 8100,成功即可在浏览器中http://127.0.0.1/status看到一个json串,在http://127.0.0.1/inspector中看到手机屏幕的投影。注意此处与上篇文章中的区别,上篇文章中使用的是Xcode11.5,由于版本问题,所以上篇文章中即使链接成功了也不能在浏览器中看到手机屏幕的投影,而此次能够看到:

%title插图%num
步骤四
也是*后一步,在Airtest IDE中点击点击连接按钮即可连接上IOS设备了,终于可以愉快地直接截图操作了

%title插图%num
步骤五
终于大功告成,可以happy一下了

步骤六
继续写用例

iOS 13 SceneDelegate适配

Xcode 11新建工程
在Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏现象。
原因:

Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate
AppDelegate和SceneDelegate这是iPadOS带来的新的多窗口支持的结果,并且有效地将应用程序委托的工作分成两部分。
也就是说在我们用多窗口开发iPadOS中,从iOS 13开始,您的应用代表应该:

设置应用程序期间所需的任何数据。
响应任何专注于应用的事件,例如与您共享的文件。
注册外部服务,例如推送通知。
配置您的初始场景。
相比之下,在iOS 13中的新顶级对象是一个UIWindowScene,场景代表可以处理应用程序用户界面的一个实例。因此,如果用户创建了两个显示您的应用程序的窗口,则您有两个场景,均由同一个应用程序委托支持。
这些场景旨在彼此独立工作。因此,您的应用程序不再移动到后台,而是单个场景执行 – 用户可以将一个移动到后台,同时保持另一个打开。

我们可以看下info.plist文件和工程项目文件的变化如图:

%title插图%num

适配方案一
如果我们不开发iPadOS多窗口APP,SceneDelegate窗口管理我们可以不需要直接删掉就好了。

删除掉info.plist中Application Scene Manifest选项,同时,文件SceneDelegate可删除可不删
相关代码注释掉

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: “Default Configuration”, sessionRole: connectingSceneSession.role)
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
//注释掉这两个方法。

Appdelegate新增windows属性
var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//如果是用默认的storyboard,下面的代码可以不写
// window = UIWindow.init()
// window?.frame = UIScreen.main.bounds
// window?.makeKeyAndVisible()
// window?.rootViewController = UIStoryboard.init(name: “Main”, bundle: nil).instantiateInitialViewController()
return true
}
///做完这些就跟以前一样啦。

适配方案二
即要用iOS 13中新的SceneDelegate,又可以在iOS 13一下的设备中完美运行。那就添加版本判断,利用@available

步骤:

SceneDelegate中添加@available(iOS 13, *)
@available(iOS 13, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
//在类的头部@available(iOS 13, *)添加即可
……..
……..
…….
}

AppDelegate中同样声明window属性
var window: UIWindow?
///didFinishLaunchingWithOptions中添加版本判断
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 13, *) {
}else {
window = UIWindow.init()
window?.frame = UIScreen.main.bounds
window?.makeKeyAndVisible()
window?.rootViewController = UIStoryboard.init(name: “Main”, bundle: nil).instantiateInitialViewController()
}
return true
}

 

AppDelegate中两个关于Scene的类也添加版本控制,Swift中可以用扩展单独拎出来,如下:
@available(iOS 13.0, *)
extension AppDelegate {
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: “Default Configuration”, sessionRole: connectingSceneSession.role)
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}

///完工

切记:这种方式,AppDelegate中的有关程序的一下状态的方法,iOS 13设备是不会走的,iOS13一下的是会收到事件回调的。13以上的设备会走SceneDelegate对应的方法

func applicationWillResignActive(_ application: UIApplication) {
}
…..
…..
…..

iphone换机数据迁移_IPHONE数据迁移到安卓系统操作分析:

a49ef93016cab0cf75e3ed1f63f28c99.png

一、2016年之前的系统:

由于IOS系统(ISO即苹果操作系统)的限制,未越狱的情况下,苹果公司是不允许用户过多的操作手机里的内容的,这也是IOS系统一直比安卓系统安全的主要原因。所以,在多数通用的APP软件里,比如“QQ同步助手”、百度云、移动的“和彩云网盘”、联通的“沃云盘”,在安卓系统下,都是可以备份通讯录、短信和通话记录的,但是在IOS系统里,只能备份通讯录一项,特别是短信这一项,则不能被访问,也就是说,在官方IOS系统里,用户无法自行备份短信和通话记录

在IOS11出现之前,苹果的越狱系统一直被大家追捧,原因就是越狱之后,用户是可以通过软件,对在原版IOS下无法操作的某些东西进行操控,比如短信备份。特别是在ISO9以及9之前,越狱后的IOS系统,通过“QQ同步助手PRO版”,可以直接备份短信,然后在安卓系统上,直接进行恢复,傻瓜式操作,没有什么技术含量了。后来由于苹果公司限制了越狱,多数的越狱软件不能运行,或是已经没有了这些软件,使得很多苹果用户在切换到安卓系统时遇到了*恶心的问题:就是短信无法迁移支安卓系统上。

二、2020年将ISO13的数据迁移到安卓机上。

2019年底购了一台华为P30 PRO,由于原来的手机IOS系统已经升级到12,不能越狱。就某度了一下有没有解决方案。遗憾的是,搜索出来的结果多数都是复制粘贴的,都是同一种说法,甚至有些根本不懂IOS系统的就在瞎侃,说怎么怎么操作就可以了,其实就是胡扯。

先后下载了几款软件进行备份,发现要么是收费,要么是忽悠,根本没有操作的可能。

其中多数软件提到了一款iSMS2droid的软件,用它可以将备份在电脑端的ISO文件选择恢复到安卓上。但是搜索之后,发现*新的华为系统、小米系统、魅族系统下,这款软件都由于权限,恢复短信时提示恢复失败。

经过近一周的反复测试,终于把苹果系统下的文件成功迁移到了华为手机上,现将操作流程分享一下:

准备工作:买一个转换头(如图);电脑端下载“爱思助手7.0”,安卓手机下载“i换机大师”。

d6c755953ad428ef4ad425d615b45080.png
01d54800291338d6a753e47c9dfe56c7.png
238f8dc0f9757172669482d2f75e8de5.png

1、 用爱思助手7.0备份系统。

备份系统,对于一个IT人员来说,是至关重要的,因为无论什么时候,操作失败的情总下,必须进行数据的恢复,毕竟,手机有价,数据无价,这个理论无论什么情况下都是成立的。

备份的时候,选择全部备份(因为没法子进行部分备份,备份的目录选择非C盘)。提前检查一下自己的苹果手机用了多少空间,一般备份前先把微信的临时空间清理一下,特别是群多的,尽管很多群聊天内容不看,但是也会占用一部分空间,比如我媳妇的手机,整理一下微信,花了一个多小时,清理了20个G的临时空间,这为后期的备份省去了好多时间。

备份的时候,会和ITNUES一样,提示设置密码。如果设置了itnues密码,请牢牢记住。以后要用。备份的原因,是为了恢复失败时,可以进行恢复。如果坚持认为手机没有重要的内容,可以忽略,也要准备承担恢复失败的心理准备。现在的电脑,配置高些的话,备份40个G的话,大概需要1-1.5个小时左右,电脑配置低的话,就照着2小时甚至更长的时间去准备吧。

2、 在华为手机上下载“i换机大师”

在华为手机上下载“i换机大师”。小米的官主应用上没有这个软件,需要自行在浏览器里去搜索下载。这个软件是在测试了近十款之后确定的。其它的虽然也能部分的恢复数据和短信,但是功能和速度上,比这个还是有很大差距的。

85d61f92208ed5b99ee80505e9a468d5.png
e03a0f4fbf6d8be8e4beaec537732fc6.png

3、 用数据线和转换头将苹果手机和华为手机连接

所有的准备工作就绪之后,就在安卓手机上进行操作了。

运行“i换机大师”,会显示两个选项“通过USB线导入”和“通过iCloud导入”。本人测试了,使用“通过iCloud导入”,没有短信这个选项,达不到自己的需求,所以忽略。

“通过USB线导入”,这个过程会需要一段时间,偶尔也会出现闪退的情况,之后就会出现自己非常期待的界面,希望恢复的内容几乎都在里面,有照片,有通讯录,有短信,甚至有备忘录,照片和视频,自行选择恢复就行了。操作的过程中,会提示输入itunes备份密码。如果忘记密码的话,没法子,按照提示,将手机恢复默认设置才能解决(本人测试的,即使恢复默认设置,安卓端依然会提示输入密码,*后只得将恢复系统之后无密码的情况再用爱思助手备份一次之后,才不会提示输入密码的选项)。

提示:

1、 在WINDOWS下备份苹果系统时,会提到一个加密问题,在用ITNUES备份时,也是有这个选项的。友情提示,这个密码必须牢牢记住,否则后期非常的麻烦。

2、 即使提示你已经完全恢复成果,也不要全信它。自己再在两部手机中检测一下,看是否真的将全部数据导入过来。我*次系统提示成功,但是检测后发现通讯录少了300多个电话;第二次导入后再验证,电话才成功,但是短信的某些日期与实际不符。第三次才算成功。具体的情况,大家自已体验吧。