美文网首页iOS 面试题 解析
2020 年 4 月后上架APP新规则和iOS13适配

2020 年 4 月后上架APP新规则和iOS13适配

作者: 小白PK大牛 | 来源:发表于2020-03-30 09:09 被阅读0次

    iOS 13 支持适配的机型

    目前最新 iPhone 11、iPhone 11 Pro和iPhone 11 Pro Max
    iPhone X、iPhone XR、iPhone XS、iPhone XS Max
    iPhone 8、iPhone 8 Plus
    iPhone 7、iPhone 7 Plus
    iPhone 6s、iPhone 6s Plus
    iPhone SE
    iPod touch (第七代)

    适配要求
    Starting April, 2020, all iPhone and iPad apps submitted to the App Store will need to be built with the iOS 13 SDK or later. They must also support the all-screen design of iPhone XS Max or the 12.9-inch iPad Pro (3rd generation), or later.

    根据官网的说法,2020年4月之后所有提交到 App Store 的 iPhone 和 iPad 应用必须使用 iOS 13 以上的 SDK 进行编译,并支持 iPhone Xs Max 或 12.9 寸 iPad Pro (3代) 及以后版本的全屏幕设计。

    1.UIWebview替换成WKWebview

    第一步
    全局搜索UIWebview ,将相关代码删除或者替换
    但是第一步不能检测到.a 文件或者一些没有添加到项目目录中的文件中是否使用UIWebview api 现在就需要第二步
    第二步
    ①从终端cd到要检查的项目根目录
    ②grep -r UIWebView .
    以上两步就可以检测出项目中的.a文件中是否调用到UIWebview ,对其进行替换或者删除

    2.私有方法 KVC不让使用

    在 iOS 13 中不再允许使用 valueForKey、setValue:forKey: 等方法获取或设置私有属性,虽然编译可以通过,但是在运行时会直接崩溃。

    // 崩溃 api
    UITextField *textField = [searchBar valueForKey:@"_searchField"];
    
    // 替代方案 1,使用 iOS 13 的新属性 searchTextField
    searchBar.searchTextField.placeholder = @"search";
    
    // 替代方案 2,遍历获取指定类型的属性
    - (UIView *)findViewWithClassName:(NSString *)className inView:(UIView *)view{
        Class specificView = NSClassFromString(className);
        if ([view isKindOfClass:specificView]) {
            return view;
        }
       if (view.subviews.count > 0) {
            for (UIView *subView in view.subviews) {
                UIView *targetView = [self findViewWithClassName:className inView:subView];
                if (targetView != nil) {
                    return targetView;
                }
            }
        }
         return nil;
    }
    
    // 调用方法
     UITextField *textField = [self findViewWithClassName:@"UITextField" inView:_searchBar];
    
    // 崩溃 api
    [searchBar setValue:@"取消" forKey:@"_cancelButtonText"];
    // 替代方案,用同上的方法找到子类中 UIButton 类型的属性,然后设置其标题
    UIButton *cancelButton = [self findViewWithClassName:NSStringFromClass([UIButton class]) inView:searchBar];
    [cancelButton setTitle:@"取消" forState:UIControlStateNormal];
    
    // 崩溃 api。获取 _placeholderLabel 不会崩溃,但是获取 _placeholderLabel 里的属性就会
    [textField setValue:[UIColor blueColor] forKeyPath:@"_placeholderLabel.textColor"];
    [textField setValue:[UIFont systemFontOfSize:20] forKeyPath:@"_placeholderLabel.font"];
    
    // 替代方案 1,去掉下划线,访问 placeholderLabel
    [textField setValue:[UIColor blueColor] forKeyPath:@"placeholderLabel.textColor"];
    [textField setValue:[UIFont systemFontOfSize:20] forKeyPath:@"placeholderLabel.font"];
    
    // 替代方案 2
    textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"输入" attributes:@{
       NSForegroundColorAttributeName: [UIColor blueColor],
       NSFontAttributeName: [UIFont systemFontOfSize:20]
    }];
    

    3.使用蓝牙需要权限申请

    CBCentralManager,iOS13以前,使用蓝牙时可以直接用,不会出现权限提示,iOS13后,再使用就会提示了。 在info.plist里增加

    <key>NSBluetoothAlwaysUsageDescription</key> 
    <string>对使用蓝牙的说明文案</string>
    

    在iOS13中,蓝牙变成了和位置,通知服务等同样的可以针对单个app授权的服务。

    - (NSString*) getWifiSsid {
      if (@available(iOS 13.0, *)) {
        //用户明确拒绝,可以弹窗提示用户到设置中手动打开权限
        if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
          NSLog(@"User has explicitly denied authorization for this application, or location services are disabled in Settings.");
          //使用下面接口可以打开当前应用的设置页面
          //[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
          return nil;
        }
        CLLocationManager* cllocation = [[CLLocationManager alloc] init];
        if(![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){
          //弹框提示用户是否开启位置权限
          [cllocation requestWhenInUseAuthorization];
          usleep(50);
          //递归等待用户选选择
          return [self getWifiSsidWithCallback:callback];
        }
      }
      NSString *wifiName = nil;
      CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
      if (!wifiInterfaces) {
        return nil;
      }
      NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;
      for (NSString *interfaceName in interfaces) {
        CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
     
        if (dictRef) {
          NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
          NSLog(@"network info -> %@", networkInfo);
          wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
          CFRelease(dictRef);
        }
      }
      CFRelease(wifiInterfaces);
      return wifiName;
    }
    

    4.MPMoviePlayerController 被弃用

    在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'
    

    解决方案使用 AVFoundation 里的 AVPlayer。

    5.推送的 deviceToken 获取到的格式发生变化

    原本可以直接将 NSData 类型的 deviceToken 转换成 NSString 字符串,然后替换掉多余的符号即

    - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
       NSString *token = [deviceToken description];
       for (NSString *symbol in @[@" ", @"<", @">", @"-"]) {
           token = [token stringByReplacingOccurrencesOfString:symbol withString:@""];
       }
       NSLog(@"deviceToken:%@", token);
    }
    

    在 iOS 13 中,这种方法已经失效,NSData类型的 deviceToken 转换成的字符串变成了:

    {length = 32, bytes = 0xd7f9fe34 69be14d1 fa51be22 329ac80d ... 5ad13017 b8ad0736 }
    

    解决方案
    需要进行一次数据格式处理,参考友盟的做法,可以适配新旧系统,获取方式如下

    #include 
    - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
        if (![deviceToken isKindOfClass:[NSData class]]) return;
        const unsigned *tokenBytes = [deviceToken bytes];
        NSString *hexToken = [NSString stringWithFormat:@"xxxxxxxx",
                              ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
                              ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
                              ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
        NSLog(@"deviceToken:%@", hexToken);
    }
    

    6.UISearchBar 黑线处理导致崩溃

    之前为了处理搜索框的黑线问题,通常会遍历 searchBar 的 subViews,找到并删除 UISearchBarBackground。

    for (UIView *view in _searchBar.subviews.lastObject.subviews) {
        if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
            [view removeFromSuperview];
            break;
        }
    }
    

    在 iOS13 中这么做会导致 UI 渲染失败,然后直接崩溃,崩溃信息如下:

    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'
    

    解决方案
    设置 UISearchBarBackground 的 layer.contents 为 nil:

    for (UIView *view in _searchBar.subviews.lastObject.subviews) {
        if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
            view.layer.contents = nil;
            break;
        }
    }
    

    7.UINavigationBar 设置按钮边距导致崩溃

    从 iOS 11 开始,UINavigationBar使用了自动布局,左右两边的按钮到屏幕之间会有 16 或 20 的边距。
    为了避免点击到间距的空白处没有响应,通常做法是:定义一个 UINavigationBar 子类,重写 layoutSubviews 方法,在此方法里遍历 subviews 获取 _UINavigationBarContentView,并将其 layoutMargins 设置为 UIEdgeInsetsZero。

    - (void)layoutSubviews {
        [super layoutSubviews];
        
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass([subview class]) containsString:@"_UINavigationBarContentView"]) {
                subview.layoutMargins = UIEdgeInsetsZero;
                break;
            }
        }
    }
    

    然而,这种做法在 iOS 13 中会导致崩溃,崩溃信息如下:

    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Client error attempting to change layout margins of a private view'
    

    解决方案
    使用设置 frame 的方式,让 _UINavigationBarContentView 向两边伸展,从而抵消两边的边距。

    - (void)layoutSubviews {
        [super layoutSubviews];
        
        for (UIView *subview in self.subviews) {
            if ([NSStringFromClass([subview class]) containsString:@"_UINavigationBarContentView"]) {
                if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
                    UIEdgeInsets margins = subview.layoutMargins;
                    subview.frame = CGRectMake(-margins.left, -margins.top, margins.left + margins.right + subview.frame.size.width, margins.top + margins.bottom + subview.frame.size.height);
                } else {
                    subview.layoutMargins = UIEdgeInsetsZero;
                }
                break;
            }
        }
    }
    

    8.使用 UISearchDisplayController 导致崩溃

    在 iOS 8 之前,我们在 UITableView 上添加搜索框需要使用 UISearchBar + UISearchDisplayController 的组合方式,而在 iOS 8 之后,苹果就已经推出了 UISearchController 来代替这个组合方式。在 iOS 13 中,如果还继续使用 UISearchDisplayController 会直接导致崩溃,崩溃信息如下:

    *** Terminating app due to uncaught exception 'NSGenericException', reason: 'UISearchDisplayController is no longer supported when linking against this version of iOS. Please migrate your application to UISearchController.'
    

    解决方案
    使用 UISearchController 替换 UISearchBar + UISearchDisplayController 的组合方案。

    9.Xcode 11 创建的工程在低版本设备上运行黑屏

    使用 Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏。这是因为 Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate:



    这是为了 iPadOS 的多进程准备的,也就是说 UIWindow 不再是 UIApplication 中管理,但是旧版本根本没有 UIScene。
    解决方案
    在 AppDelegate 的头文件加上:

    @property (strong, nonatomic) UIWindow *window;
    

    10.使用 @available 导致旧版本 Xcode 编译出错。

    在 Xcode 11 的 SDK 工程的代码里面使用了 @available 判断当前系统版本,打出来的包放在 Xcode 10 中编译,会出现一下错误:

    Undefine symbols for architecture i386:
        "__isPlatformVersionAtLeast", referenced from:
            ...
    ld: symbol(s) not found for architecture i386
    

    从错误信息来看,是 __isPlatformVersionAtLeast 方法没有具体的实现,但是工程里根本没有这个方法。实际测试无论在哪里使用@available ,并使用 Xcode 11 打包成动态库或静态库,把打包的库添加到 Xcode 10 中编译都会出现这个错误,因此可以判断是 iOS 13 的 @available 的实现中使用了新的 api。
    解决方案
    如果你的 SDK 需要适配旧版本的 Xcode,那么需要避开此方法,通过获取系统版本来进行判断:

    if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
        ...
    }
    

    另外,在 Xcode 10 上打开 SDK 工程也应该可以正常编译,这就需要加上编译宏进行处理

    #ifndef __IPHONE_13_0
    #define __IPHONE_13_0 130000
    #endif
    
    #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
    ...
    #endif
    

    11.暗黑模式

    Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS。 如果不打算适配 Dark Mode,可以直接在 Info.plist 中添加一栏:User Interface Style : Light,即可在应用内禁用暗黑模式。不过即使设置了颜色方案,申请权限的系统弹窗还是会依据系统的颜色进行显示,自己创建的 UIAlertController 就不会

    12.UISegmentedControl 默认样式改变

    默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。
    原本设置选中颜色的 tintColor 已经失效,新增了 selectedSegmentTintColor 属性用以修改选中的颜色。
    Web Content适配

    13.textfield.leftview

    如下方式,直接给 textfield.leftView 赋值一个 UILabel 对象,他的宽高会被 sizeToFit,而不是创建时的值。

    // left view label
    UILabel *phoneLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 63, 50)];
    phoneLabel.text = @"手机号";
    phoneLabel.font = [UIFont systemFontOfSize:16];
    // set textfield left view
    self.textfieldName.leftView = phoneLabel;
    

    实际leftview的width为59,height为19
    解决方法:嵌套一个UIView

    // label
    UILabel *phoneLabel = [[UILabel alloc] init];
    phoneLabel.text = @"手机号";
    phoneLabel.font = [UIFont systemFontOfSize:16];
    [phoneLabel sizeToFit];
    phoneLabel.centerY = 50/2.f;
    // left view
    UIView *leftView = [[UIView alloc] initWithFrame:(CGRect){0, 0, 63, 50}];
    [leftView addSubview:phoneLabel];
    // set textfield left view
    self.textfieldName.leftView = leftView;
    

    14.NSAttributedString优化

    对于UILabel、UITextField、UITextView,在设置NSAttributedString时也要考虑适配Dark Mode,否则在切换模式时会与背景色融合,造成不好的体验
    不建议的做法

    NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16]};
    NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
    

    推荐的做法

    // 添加一个NSForegroundColorAttributeName属性
    NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor labelColor]};
    NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
    

    15.TabBar红点偏移

    如果之前有通过TabBar上图片位置来设置红点位置,在iOS13上会发现显示位置都在最左边去了。遍历UITabBarButton的subViews发现只有在TabBar选中状态下才能取到UITabBarSwappableImageView,解决办法是修改为通过UITabBarButton的位置来设置红点的frame

    16.WKWebView 中测量页面内容高度的方式变更

    iOS 13以前 document.body.scrollHeight
    iOS 13中 document.documentElement.scrollHeight 两者相差55 应该是浏览器定义高度变了

    17.StatusBar 与之前版本不同

    之前 Status Bar 有两种状态,default 和 lightContent
    现在 Status Bar 有三种状态,default, darkContent 和 lightContent
    现在的 darkContent 对应之前的 default,现在的 default 会根据情况自动选择 darkContent 和 lightContent

    18. UIActivityIndicatorView

    之前的 UIActivityIndicatorView 有三种 style 分别为 whiteLarge, white 和 gray,现在全部废弃。
    增加两种 style 分别为 medium 和 large,指示器颜色用 color 属性修改。

    19.使用MJExtension 中处理NSNull的不同

    这个直接会导致Crash的在将服务端数据字典转换为模型时,如果遇到服务端给的数据为NSNull时, mj_JSONObject,其中 class_copyPropertyList方法得到的属性里,多了一种EFSQLBinding类型的东西,而且属性数量也不准确, 这个组件没有更新的情况下,写了一个方法swizzling掉把当遇到 NSNull时,直接转为nil了。
    其实目前我们项目在ios13下面还没遇到这种情况,发一个之前项目处理null的方法,在项目里面写一个NSObject的分类,添加下面的方法就可以了。大家请根据项目情况、数据格式修改下这个方法,MJ库里面自己会进行替换的:

    - (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property {
        //为了解决json字符串先赋值给oc字典后,类型转换crash问题,如:
        //json->oldValue:0
        //model中值为NSString类型
        //如果先将json转为dic,dic中对应value值为NSNumber类型,则向oldValue发送isEqualToString消息会crash
        id tempValue = oldValue;
     
        if ([property.type.code isEqualToString:@"NSString"]) {
            tempValue = [NSString stringWithFormat:@"%@", tempValue];
     
            if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] ||  [tempValue isEqualToString:@"(null)"] ||  [tempValue isEqualToString:@"(\n)"] ) {
                return @"";
            }
        }
        if ([property.type.code isEqualToString:@"NSNumber"]) {
    //        tempValue = [NSNumber numberWithFloat:[tempValue floatValue]];
     
            if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] ||  [tempValue isEqualToString:@"(null)"] ||  [tempValue isEqualToString:@"(\n)"] ) {
                return @0;
            }
        }
        return tempValue;
    }
    

    20.Sign In with Apple

    在 iOS 13 中苹果推出一种在 App 和网站上快速、便捷登录的方式: Sign In With Apple。这是 iOS 13 新增的功能,因此需要使用 Xcode 11 进行开发。关于应用是否要求接入此登录方式,苹果在 App Store 应用审核指南 中提到:

    Apps that exclusively use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user’s primary account with the app must also offer Sign in with Apple as an equivalent option.
    

    如果你的应用使用了第三方或社交账号登录服务(如Facebook、Google、Twitter、LinkedIn、Amazon、微信等)来设置或验证用户的主账号,就必须把 Sign In With Apple 作为同等的选项添加到应用上。如果是下面这些类型的应用则不需要添加:

    • 仅仅使用公司内部账号来注册和登录的应用;
    • 要求用户使用现有的教育或企业账号进行登录的教育、企业或商务类型的应用;
    • 使用政府或业界支持的公民身份识别系统或电子标识对用户进行身份验证的应用;
    • 特定第三方服务的应用,用户需要直接登录其邮箱、社交媒体或其他第三方帐户才能访问其内容。
      另外需要注意,关于何时要求接入 Sign In With Apple,苹果在 News and Updates 中提到:
    Starting today, new apps submitted to the App Store must follow these guidelines. Existing apps and app updates must follow them by April 2020.
    

    2019 年 9 月 12 日 起,提交到 App Store 的新应用必须按照应用审核指南中的标准进行接入;现有应用和应用更新必须也在 2020 年 4 月前完成接入。

    21.LaunchImage 被弃用

    修改 App 启动画面时我们需要通过 Launch Images 进行修改。
    在 iOS 8 苹果引入了 LaunchScreen.storyboard,支持界面布局用的 AutoLayout + SizeClass。虽然两种方式都可以正常工作,但是苹果更希望开发者们能够使用 LaunchScreen.storyboard 进行操作。
    由于 Launch Images 需要在对应的 aseets 里面放入所有尺寸的 Launch Images ,这样每当出现一个屏幕尺寸不同的设备时,都需要做出对应的修改,操作复杂。
    因此在 2020 年 4 月之后,所有 App 都必须使用 LaunchScreen.storyboard 的方式操作启动画面,否则将无法提交到 App Store 进行审批。
    解决方法
    创建LaunchScreen.storyboard,拖入一个UIImageView,并设置约束为上下左右都为0,在info plist文件中添加Launch screen interface file base name:Launch Screen,在General->App Icons and Launch Images->Launch Screen File 选择为LaunchScreen,清理项目并删除已安装的app

    相关文章

      网友评论

        本文标题:2020 年 4 月后上架APP新规则和iOS13适配

        本文链接:https://www.haomeiwen.com/subject/uhdgyhtx.html