日期: 2021 年 6 月 7 日

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分钟,具体后端验证参考附录

125. 验证回文串(JS实现)

125. 验证回文串(JS实现)

1 题目
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: “A man, a plan, a canal: Panama”
输出: true
示例 2:
输入: “race a car”
输出: false

2 思路
这道题思路是通过双指针的方法来依次对比首尾各字母是否相等

3代码
/**
* @param {string} s
* @return {boolean}
*/
var isPalindrome = function(s) {
if (s.length === 0) return true;
let low = 0;
let high = s.length – 1;

let status = true;
let reg = /[0-9a-zA-Z]/;
while(low <= high) {
if (!reg.test(s[low])) {
low++;
continue;
}

if (!reg.test(s[high])) {
high–;
continue;
}

if (s[low].toLowerCase() != s[high].toLowerCase()) {
status = false;
break;
} else {
low++;
high–;
}
}

return status;
};

单词接龙(JS实现)

单词接龙(JS实现)

1 题目
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的*短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出: 5
解释: 一个*短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,
返回它的长度 5。
示例 2:
输入:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
输出: 0
解释: endWord “cog” 不在字典中,所以无法进行转换。

2 思路
这道题开始考察图的遍历,首先建立图结构,连接具有相同字母的单词,建立allWords,key为某种种形式,例如*ot,value为具有该形式的单词列表,例如hot、dot、lot,这样就可以快速得到指定形式的单词,随后从beginWord开始广度遍历整个图,当找到endWord时,表明得到了转换路径

3代码
/**
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/
var ladderLength = function(beginWord, endWord, wordList) {
if (!wordList.includes(endWord)) return 0;

let len = beginWord.length;
const allWords = {};

for (let word of wordList) { //遍历每个单词,建立图
for (let i=1; i<=len; i++) {
let key = `${word.slice(0,i-1)}*${word.slice(i,len)}`;
if (!allWords[key]) allWords[key] = [];
allWords[key].push(word);
}
}

const quque = [{word: beginWord, index: 1}]; //index用于记录走了多少步
const visited = {};

while(quque.length > 0) { //广度优先遍历整个图
let item = quque.shift();
for (let i=1; i<=len; i++) {
let key = `${item.word.slice(0,i-1)}*${item.word.slice(i,len)}`;
if (!allWords[key]) continue;
for (let j=0; j<allWords[key].length; j++) {
let word = allWords[key][j]
if (word === endWord) return item.index + 1;
if (visited[word]) continue;
quque.push({word, index: item.index + 1});
visited[word] = true;
}
}
}

return 0;
};

求根到叶子节点数字之和(JS实现)

求根到叶子节点数字之和(JS实现)

1 题目
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入: [1,2,3]
1
/
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:
输入: [4,9,0,5,1]
4
/
9 0
/
5 1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.

2 思路
这道题比较简单,直接用递归的方法深度遍历根节点到叶子节点的每条路径,并*后相加即可

3代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var sumNumbers = function(root) {
if (!root) return 0;
const result = [];
function d(p, num) {
if (!p) return;

num = num * 10 + p.val;
if (!p.left && !p.right) {
result.push(num);
} else {
d(p.left, num);
d(p.right, num);
}
}

d(root, 0);

return result.reduce((a,b) => a+b);
};

被围绕的区域(JS实现)

被围绕的区域(JS实现)

1 题目
给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ *终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

2 思路
这道题思路比较巧妙,首先遍历矩阵里边界的每个元素,若其为‘O’,则从其开始遍历上下左右4个方向,将其变为‘#’,*后剩下的‘O’就是没有连通外部边界的目标‘O’,将其变为‘X’,其余‘#’恢复为原来的‘O’就可以了

3代码
/**
* @param {character[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
var solve = function(board) {
if (board.length <= 2) return;
if (board[0].length <= 2) return;
let rows = board.length;
let cols = board[0].length;

for (let i=0; i<rows; i++) {
for (let j=0; j<cols; j++) {
if (board[i][j] === ‘O’ &&
(i === 0 || j === 0 || i === rows – 1 || j === cols – 1)
) {
d(i, j);
}
}
}

function d(i,j) {
if (i < 0 || j < 0 || i >= rows || j >= cols || board[i][j] === ‘X’ || board[i][j] === ‘#’) return;

board[i][j] = ‘#’;

d(i+1,j);
d(i-1,j);
d(i,j+1);
d(i,j-1);
}

for (let i=0; i<rows; i++) {
for (let j=0; j<cols; j++) {
if (board[i][j] === ‘#’) {
board[i][j] = ‘O’
} else if (board[i][j] === ‘O’) {
board[i][j] = ‘X’;
}
}
}

};

分割回文串(JS实现)

分割回文串(JS实现)

1 题目
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]

2 思路
这道题的主要思路还是用构造一棵树+剪枝的方法,来获取所有满足需求的子串组合,其实还有优化的地方,比如判断回文串是可以使用动态规划缓存结果

3代码
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
const result = [];

d([],s);

return result;

function d(arr, leftStr) {
if (leftStr.length === 0) result.push(arr);

for (let i=1;i<=leftStr.length;i++) { //遍历可能的子串,长度从1开始
let substr = leftStr.substr(0,i);
if (isPStr(substr)) {
let temp = arr.slice();
temp.push(substr);
d(temp, leftStr.slice(i));
}
}
}

function isPStr(str) {
if (str.length === 1) return true;

let low = 0;
let high = str.length – 1;

while(low <= high) {
if (str[low] !== str[high]) {
return false;
}

low++;
high–;
}

return true;
}
};

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速